"""A declared human-control lane cannot use an undeclared approval fact.""" import copy import json from dataclasses import replace from pathlib import Path from types import SimpleNamespace import pytest from secrets_engine import cli from secrets_engine.approval_consume import ( ConsumeBinding, authorize_action, require_production_consume, ) from secrets_engine.authorization import build_action_request, request_digest from secrets_engine.catalog import load_entry, validate_entry from secrets_engine.errors import CatalogError, DecisionError from tests.authorization_stub import AuthorizationStub from tests.test_catalog import VALID from tests.test_consume_binding_join import ( AUTH_ID, _Cfg, _entry, _opener, _resolve, _served, _token, ) from tests.test_exec_owner import bound from tests.test_privileged_cli_evidence import _config, _records def controlled_entry(): entry = _entry() entry.approval.update(model="decision", human_control=True) return entry @pytest.mark.parametrize("value", [None, 0, 1, "true", "false", [], {}]) def test_catalog_declaration_is_strictly_boolean(value): raw = copy.deepcopy(VALID) raw["approval"].update(model="decision", human_control=value) with pytest.raises(CatalogError, match="human_control"): validate_entry(raw) def test_bootstrap_only_cannot_declare_a_human_control(): raw = copy.deepcopy(VALID) raw["approval"]["human_control"] = True with pytest.raises(CatalogError, match="human_control"): validate_entry(raw) @pytest.mark.parametrize("value", ["missing", False, None, 1, "true", [], {}]) def test_undeclared_or_malformed_claim_refuses_before_pdp(tmp_path, value): entry = controlled_entry() claim = _served(entry) if value != "missing": claim["binding"]["human_control"] = value claim["entries"] = [{"subject_id": "human:fixture", "principal_type": "human"}] with pytest.raises(DecisionError, match="human_control"): authorize_action(_Cfg(_token(tmp_path)), entry, "deactivate", fields=("api_token",), opener=_opener(claim), pdp_opener=lambda *a, **k: pytest.fail("must not call PDP")) def test_human_control_intent_changes_the_submitted_request_binding(): ordinary = _entry() controlled = controlled_entry() def request(entry): return build_action_request(entry, "deactivate", subject_id="service:fixture", subject_type="Service", purpose="same-action", fields=("api_token",)) before, after = request(ordinary), request(controlled) assert "human_control" not in before["context"] assert after["context"]["human_control"] is True assert request_digest(before) != request_digest(after) def test_declared_fact_yields_a_human_control_consume_binding(tmp_path): entry = controlled_entry() claim = _served(entry) claim["binding"]["human_control"] = True authorized = _resolve(_Cfg(_token(tmp_path)), entry, claim) assert authorized.binding.human_control is True assert authorized.as_evidence()["approval_human_control"] is True def test_legacy_lane_still_accepts_undeclared_claim(tmp_path): entry = _entry() result = _resolve(_Cfg(_token(tmp_path)), entry, _served(entry)) assert result is not None assert result.binding.human_control is False @pytest.mark.parametrize("stage", ["test", "prod"]) def test_missing_serving_path_cannot_fall_back_to_lane_or_demo_approval(tmp_path, monkeypatch, stage): entry = replace(controlled_entry(), stage=stage) monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1") with pytest.raises(DecisionError, match="human_control"): authorize_action(_Cfg(None, approval_url="", approval_token_file=None), entry, "exec") @pytest.mark.parametrize("stage", ["test", "prod"]) def test_consume_requires_observed_human_control_even_with_demo_enabled(tmp_path, monkeypatch, stage): entry = replace(controlled_entry(), stage=stage) monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1") with pytest.raises(DecisionError, match="human_control"): require_production_consume(_config(tmp_path), entry, binding=ConsumeBinding(AUTH_ID, "sha256:"+"a"*64), opener=lambda *a, **k: pytest.fail("must not consume")) @pytest.mark.parametrize("stage", ["test", "prod"]) def test_declared_control_still_consumes_with_demo_enabled(tmp_path, monkeypatch, stage): entry = replace(controlled_entry(), stage=stage) monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1") cfg = replace(_config(tmp_path), approval_url="https://approval.example", approval_token_file=_token(tmp_path)) binding = ConsumeBinding(AUTH_ID, "sha256:"+"a"*64, human_control=True) calls = [] def consume(request, **kwargs): calls.append(request.full_url) response = _opener({"status": "consumed", "request_digest": binding.request_digest})(request) response.getcode = lambda: 200 return response result = require_production_consume(cfg, entry, binding=binding, opener=consume) assert result.approval_id == AUTH_ID assert calls == [f"https://approval.example/v1/approvals/{AUTH_ID}/consume"] def test_human_declaration_is_rechecked_after_pdp(tmp_path, monkeypatch): from secrets_engine import approval_consume entry = controlled_entry() claim = _served(entry) claim["binding"]["human_control"] = True monkeypatch.setattr(approval_consume, "fetch_approval_claim", lambda **k: claim) cfg = _Cfg(_token(tmp_path)) pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version) original_validate = approval_consume.validate_decision_envelope def invalidate_after_check(*args, **kwargs): result = original_validate(*args, **kwargs) claim["binding"]["human_control"] = False return result monkeypatch.setattr(approval_consume, "validate_decision_envelope", invalidate_after_check) def check(request, **kwargs): return _opener(pdp.decision(json.loads(request.data)))(request) with pytest.raises(DecisionError, match="human_control"): authorize_action(cfg, entry, "deactivate", fields=("api_token",), policy_targets=(entry.policy_name,), auth_targets=(entry.role_name,), pdp_opener=check) def test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child(bound, tmp_path, monkeypatch): raw, _, _ = bound raw["stage"] = "prod" raw["approval"].update(model="decision", authorization_id=AUTH_ID, purpose="contract-test", human_control=True) entry = validate_entry(raw) cfg = replace(_config(tmp_path), approval_url="https://approval.example", approval_token_file=_token(tmp_path), authorization_subject_id="user:alice", authorization_subject_type="Human") claim = _served(entry, action="exec") monkeypatch.setattr(cli, "get_entry", lambda *a: entry) monkeypatch.setattr(cli, "authorize_action", lambda *a, **k: authorize_action(*a, **k, opener=_opener(claim), pdp_opener=lambda *a, **k: pytest.fail("must not check"))) for name in ("require_production_consume", "_open_backend"): monkeypatch.setattr(cli, name, lambda *a, **k: pytest.fail("must not consume or open backend")) with pytest.raises(DecisionError, match="human_control"): cli.cmd_exec(cfg, SimpleNamespace(catalog=entry.id, field="api_token", mode="exec-env", command=raw["delivery_config"]["exec_owner"]["command"])) records = _records(tmp_path) assert [r["result"] for r in records] == ["attempt", "failed-DecisionError"] def test_factory_catalog_declares_human_control_and_keeps_owner_pending(): entry = load_entry(Path(__file__).resolve().parents[1]/"catalog/glas-claude-agent-dev-anthropic.yaml") assert entry.approval["human_control"] is True assert entry.delivery_config["exec_owner"]["status"] == "pending"