"""APPROVAL-WP-0007-T02: opt-in consumer = actor, declared at issue. Also GH-DEC-2026-026 Ruling 6: under human_control the actor never approves its own approval. """ import dataclasses import json from pathlib import Path import jsonschema import pytest from approval_engine.api import App, call from approval_engine.auth import Identity, StaticTokenAuthenticator from approval_engine.errors import Conflict, Forbidden, Unprocessable from approval_engine.store import Engine, LATEST_SCHEMA_VERSION from tests.conftest import FROZEN, binding, validity SCHEMA = json.loads( (Path(__file__).parents[1] / "schemas" / "approval_claim.schema.json").read_text() ) ACTOR = "agt-secrets-engine" # binding() default actor def _identity(subject, principal_type="service"): return Identity( subject=subject, issuer="https://keycape.example", audiences=("approval-engine",), principal_type=principal_type, tenant="tenant:platform", roles=frozenset(), scopes=frozenset({"approval:create", "approval:read", "approval:approve", "approval:consume", "approval:supersede"}), assurance={"level": "aal2", "amr": ["pwd", "otp"], "source": "key-cape"}, evidence_ref=f"test-identity:{subject}", ) @pytest.fixture def multi_app(engine): return App(engine, StaticTokenAuthenticator({ "actor": _identity(ACTOR), "other": _identity("svc-other"), "human": _identity("user:admin", "human"), })) def _approved(engine, **kwargs): obj = engine.create(binding(), validity(), **kwargs) return engine.add_entry(obj.id, "user:admin", principal_type="human") def test_undeclared_keeps_any_consumer_and_exact_shape(engine): obj = _approved(engine) assert obj.actor_consumes is False assert "actor_consumes" not in obj.as_dict()["binding"] claim = engine.claim(obj.id) assert "actor_consumes" not in claim["binding"] jsonschema.validate(claim, SCHEMA) result = engine.consume(obj.id, obj.binding_digest, consumer=_identity("svc-other")) assert result["status"] == "consumed" def test_declared_false_is_the_same_as_undeclared(engine): obj = _approved(engine, actor_consumes=False) assert "actor_consumes" not in engine.claim(obj.id)["binding"] engine.consume(obj.id, obj.binding_digest, consumer=_identity("svc-other")) @pytest.mark.parametrize("value", ["true", 1, None, [], {}]) def test_declaration_must_be_a_boolean(multi_app, engine, value): status, _ = call(multi_app, "POST", "/v1/approvals", { "binding": binding(), "validity": validity(), "actor_consumes": value, }, authorization="Bearer actor") assert status == 422 with pytest.raises(Unprocessable, match="actor_consumes"): engine.create(binding(), validity(), actor_consumes=value) def test_wrong_consumer_is_refused_and_actor_consumes_over_http(multi_app, engine): status, created = call(multi_app, "POST", "/v1/approvals", { "binding": binding(), "validity": validity(), "human_control": True, "actor_consumes": True, }, authorization="Bearer actor") assert status == 201, created assert created["binding"]["actor_consumes"] is True status, _ = call(multi_app, "POST", f"/v1/approvals/{created['id']}/entries", {}, authorization="Bearer human") assert status == 200 claim = engine.claim(created["id"]) assert claim["binding"]["actor_consumes"] is True and claim["valid_now"] jsonschema.validate(claim, SCHEMA) digest = created["binding"]["digest"] status, refused = call(multi_app, "POST", f"/v1/approvals/{created['id']}/consume", {"request_digest": digest}, authorization="Bearer other") assert status == 403, refused assert engine.get(created["id"]).status == "approved" status, spent = call(multi_app, "POST", f"/v1/approvals/{created['id']}/consume", {"request_digest": digest}, authorization="Bearer actor") assert status == 200 and spent["status"] == "consumed" and spent["idempotent"] is False # Another service gets no receipt from an idempotent retry either. status, _ = call(multi_app, "POST", f"/v1/approvals/{created['id']}/consume", {"request_digest": digest}, authorization="Bearer other") assert status == 403 status, again = call(multi_app, "POST", f"/v1/approvals/{created['id']}/consume", {"request_digest": digest}, authorization="Bearer actor") assert status == 200 and again["idempotent"] is True def test_internal_call_without_consumer_fails_closed(engine): obj = _approved(engine, actor_consumes=True) with pytest.raises(Forbidden, match="actor_consumes"): engine.consume(obj.id, obj.binding_digest) assert engine.get(obj.id).status == "approved" assert engine.consume(obj.id, obj.binding_digest, consumer=_identity(ACTOR))["status"] == "consumed" def test_supersession_inherits_and_refuses_existing_mismatch(engine): parent = engine.create(binding(), validity(), actor_consumes=True) plain = engine.create(binding(), validity()) with pytest.raises(Conflict, match="actor_consumes"): engine.supersede(parent.id, plain.id) assert engine.get(parent.id).status == "requested" child = engine.get(engine.supersede(parent.id)["successor_id"]) assert child.actor_consumes is True assert engine.claim(child.id)["binding"]["actor_consumes"] is True engine.add_entry(child.id, "user:admin", principal_type="human") with pytest.raises(Forbidden): engine.consume(child.id, child.binding_digest, consumer=_identity("svc-other")) def test_declaration_is_on_audit_events_and_outside_the_act_digest(engine): plain = engine.create(binding(), validity()) declared = engine.create(binding(), validity(), actor_consumes=True) assert plain.binding_digest == declared.binding_digest engine.add_entry(declared.id, "user:admin", principal_type="human") event = json.loads(engine._conn().execute( "SELECT payload_json FROM outbox WHERE approval_id=?", (declared.id,)).fetchone()[0]) assert event["details"]["actor_consumes"] is True plain_event_rows = engine._conn().execute( "SELECT payload_json FROM outbox WHERE approval_id=?", (plain.id,)).fetchall() assert all("actor_consumes" not in json.loads(r[0])["details"] for r in plain_event_rows) def test_legacy_v7_rows_migrate_without_the_flag(tmp_path): path = tmp_path / "v7.sqlite" eng = Engine(path, clock=lambda: FROZEN) obj = eng.create(binding(), validity()) eng.add_entry(obj.id, "user:admin", principal_type="human") eng._conn().execute("ALTER TABLE approvals DROP COLUMN actor_consumes") eng._conn().execute("PRAGMA user_version=7") eng._conn().commit() eng.close() upgraded = Engine(path, clock=lambda: FROZEN) survivor = upgraded.get(obj.id) assert survivor.actor_consumes is False assert "actor_consumes" not in upgraded.claim(obj.id)["binding"] assert upgraded.storage_status()["schema_version"] == LATEST_SCHEMA_VERSION == 8 upgraded.consume(obj.id, obj.binding_digest, consumer=_identity("svc-other")) upgraded.close() def test_human_control_refuses_the_actor_as_approver(engine): """GH-DEC-2026-026 Ruling 6. A human actor may create a human_control request but may not approve it; another human may.""" obj = engine.create(binding(actor="user:operator"), validity(), human_control=True) with pytest.raises(Forbidden, match="actor"): engine.add_entry(obj.id, "user:operator", principal_type="human") assert engine.get(obj.id).entries == [] assert engine.add_entry(obj.id, "user:admin", principal_type="human").status == "approved" def test_actor_may_still_approve_without_human_control(engine): obj = engine.create(binding(actor="user:operator"), validity()) assert engine.add_entry(obj.id, "user:operator", principal_type="human").status == "approved" def test_human_actor_self_approval_refused_over_http(engine): app = App(engine, StaticTokenAuthenticator({"op": _identity("user:operator", "human")})) status, created = call(app, "POST", "/v1/approvals", { "binding": binding(actor="user:operator"), "validity": validity(), "human_control": True, }, authorization="Bearer op") assert status == 201 status, refused = call(app, "POST", f"/v1/approvals/{created['id']}/entries", {}, authorization="Bearer op") assert status == 403, refused