"""End-to-end proof of the GH-DEC-2026-003 chain against real OpenBao. Everything before this exercised the join against unit-level fakes. This drives the actual CLI gate through all four steps in order -- claim -> check -> consume -> OpenBao -- with a live throwaway OpenBao on the far end, and asserts both that a fully authorized action reaches the backend and that each failure mode stops before it. Skipped without the `bao`/`vault` CLI. The stub serves transport and sequencing only; the wire contracts are pinned against flex-auth's real fixtures in tests/test_decision_replay.py. """ import copy import os import shutil import socket import subprocess import time import pytest import yaml from secrets_engine import cli from secrets_engine.approval_consume import authorize_action from secrets_engine.catalog import get_entry from secrets_engine.config import Config from secrets_engine.errors import DecisionError from secrets_engine.openbao import OpenBaoClient from tests.authorization_stub import AuthorizationStub from tests.test_catalog import VALID pytestmark = pytest.mark.skipif( shutil.which("bao") is None and shutil.which("vault") is None, reason="no OpenBao/Vault CLI on PATH", ) APPROVAL_ID = "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f" PACKAGE = "secrets-engine.catalog-lane.lifecycle" VERSION = "v1" def _free_port(): s = socket.socket() s.bind(("127.0.0.1", 0)) port = s.getsockname()[1] s.close() return port @pytest.fixture() def bao_dev(): bao = shutil.which("bao") or shutil.which("vault") port = _free_port() token = "se-authz-root" proc = subprocess.Popen( [bao, "server", "-dev", "-dev-no-store-token", f"-dev-root-token-id={token}", f"-dev-listen-address=127.0.0.1:{port}"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) addr = f"http://127.0.0.1:{port}" client = OpenBaoClient(addr=addr, token=token, bao_bin=bao) for _ in range(50): if client.is_reachable(): break time.sleep(0.2) else: proc.kill() pytest.fail("dev OpenBao did not become reachable") try: yield addr, token, client finally: proc.kill() @pytest.fixture() def stub(): s = AuthorizationStub(approval_id=APPROVAL_ID, package=PACKAGE, version=VERSION).start() try: yield s finally: s.stop() @pytest.fixture() def lane(tmp_path): """A prod lane bound to the approval object, in a throwaway catalog.""" raw = copy.deepcopy(VALID) raw["stage"] = "prod" # bootstrap-only skips the *legacy* State Hub lane-approval lookup, which is # a separate older mechanism with its own repo-rooted fixture directory. # What is under test here is the GH-DEC-2026-003 chain, which still gates # fully: the lane is prod, so stance, claim, check and consume all apply. raw["approval"] = { "model": "bootstrap-only", "authorization_id": APPROVAL_ID, "purpose": "end-to-end authorization proof", } catalog_dir = tmp_path / "catalog" catalog_dir.mkdir() (catalog_dir / f"{raw['id']}.yaml").write_text(yaml.safe_dump(raw)) return catalog_dir, get_entry(catalog_dir, raw["id"]) def _cfg(tmp_path, catalog_dir, stub, bao_addr): token_file = tmp_path / "authz.token" token_file.write_text("stub-credential\n") os.chmod(token_file, 0o600) return Config( catalog_dir=catalog_dir, policy_dir=tmp_path / "policies", evidence_dir=tmp_path / "evidence", hub_url="", bao_addr=bao_addr, topic_id="t", approval_url=stub.url, approval_token_file=token_file, authorization_subject_id="secrets-engine", authorization_subject_type="service", authorization_policy_package=PACKAGE, authorization_policy_version=VERSION, pdp_url=stub.url, pdp_token_file=token_file, ) def _prime(stub, cfg, entry, action="apply"): """Tell the stub which exact request the engine will propose.""" from secrets_engine.approval_consume import _expected_request stub.bind_request(_expected_request(cfg, entry, action)) def test_full_authorization_chain_reaches_openbao(bao_dev, stub, lane, tmp_path): addr, root, client = bao_dev catalog_dir, entry = lane cfg = _cfg(tmp_path, catalog_dir, stub, addr) _prime(stub, cfg, entry) authorization = authorize_action(cfg, entry, "apply", None) assert authorization is not None assert authorization.decision_id == "decision:stub-apply" assert stub.calls == ["claim", "check"], "PIP then PDP, in that order" # The gate itself: stance satisfied, consume performed, then the backend. cli._require_lane_approval(cfg, entry, "apply") assert stub.consumed, "CAS consume must happen before any OpenBao call" assert stub.calls[-1] == "consume", "consume is the last step before OpenBao" # And the backend really is reachable with the authorized identity. assert client.is_reachable() def test_unreachable_pdp_stops_before_consume(bao_dev, stub, lane, tmp_path): addr, _root, _client = bao_dev catalog_dir, entry = lane cfg = _cfg(tmp_path, catalog_dir, stub, addr) _prime(stub, cfg, entry) cfg = Config(**{**cfg.__dict__, "pdp_url": "http://127.0.0.1:1"}) with pytest.raises(DecisionError, match="unreachable"): cli._require_lane_approval(cfg, entry, "apply") assert not stub.consumed, "an unreachable PDP must not consume the approval" def test_deny_decision_stops_before_consume(bao_dev, stub, lane, tmp_path): addr, _root, _client = bao_dev catalog_dir, entry = lane cfg = _cfg(tmp_path, catalog_dir, stub, addr) _prime(stub, cfg, entry) stub.effect = "deny" with pytest.raises(DecisionError, match="effect is not allow"): cli._require_lane_approval(cfg, entry, "apply") assert not stub.consumed def test_invalid_claim_stops_before_the_pdp(bao_dev, stub, lane, tmp_path): addr, _root, _client = bao_dev catalog_dir, entry = lane cfg = _cfg(tmp_path, catalog_dir, stub, addr) _prime(stub, cfg, entry) stub.claim_valid_now = False stub.claim_reason_code = "revoked" with pytest.raises(DecisionError, match="not valid now"): cli._require_lane_approval(cfg, entry, "apply") assert "check" not in stub.calls, "a bad claim must not reach the PDP" assert not stub.consumed def test_missing_pdp_digest_stops_before_the_pdp(bao_dev, stub, lane, tmp_path): addr, _root, _client = bao_dev catalog_dir, entry = lane cfg = _cfg(tmp_path, catalog_dir, stub, addr) _prime(stub, cfg, entry) stub.include_pdp_digest = False with pytest.raises(DecisionError, match="no published mapping"): cli._require_lane_approval(cfg, entry, "apply") assert not stub.consumed def test_consume_conflict_stops_before_openbao(bao_dev, stub, lane, tmp_path): addr, _root, _client = bao_dev catalog_dir, entry = lane cfg = _cfg(tmp_path, catalog_dir, stub, addr) _prime(stub, cfg, entry) stub.consume_status = 409 with pytest.raises(DecisionError): cli._require_lane_approval(cfg, entry, "apply") assert not stub.consumed def test_action_mismatch_is_caught_by_the_digest(bao_dev, stub, lane, tmp_path): """The claim was primed for apply; proposing destroy must not ride it.""" addr, _root, _client = bao_dev catalog_dir, entry = lane cfg = _cfg(tmp_path, catalog_dir, stub, addr) _prime(stub, cfg, entry, action="apply") with pytest.raises(DecisionError): cli._require_lane_approval(cfg, entry, "destroy") assert not stub.consumed def test_unconfigured_engine_still_fails_closed(bao_dev, lane, tmp_path): """The whole point of the default: no serving path means no production.""" addr, _root, _client = bao_dev catalog_dir, entry = lane cfg = Config( catalog_dir=catalog_dir, policy_dir=tmp_path / "p", evidence_dir=tmp_path / "e", hub_url="", bao_addr=addr, topic_id="t", ) with pytest.raises(DecisionError, match="live production remains disabled"): cli._require_lane_approval(cfg, entry, "apply")