secrets-engine/tests/test_privileged_cli_evidence.py
tegwick 627810b478
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
feat: implement the PIP claim + validate authorization join
resolve_consume_binding was a `return None` stub, so protocol step 1 of
docs/approval-consumption.md (GET /v1/approvals/{id}/claim) and the
validation join never existed. validate_action_authorization had no caller
in src/ at all - it was reachable only from tests. Production fail-closed
was correct, but for an undocumented second reason, and WP-0007-T04's
"what remains is not local engine work" was wrong.

The join now reproduces the exact CheckRequest via build_action_request,
fetches the durable ActionAuthorization, and validates request binding,
digest, validity, authority, policy pin, and distinct-approver threshold
before offering a consume binding. _require_lane_approval threads the exact
field set for provision/rotate/verify/exec so the digest covers the real
proposed action.

Deliberate choices:
- The approval-engine object id is never inferred from a State Hub decision
  UUID; flex-auth stated GET /decisions/{uuid} is not the durable object.
- No default policy pin. flex-auth stated secrets-engine.lifecycle/v1 is
  example vocabulary, not a published package.
- A half-configured join raises rather than returning None, so a partial
  deployment cannot be mistaken for an unconfigured one.

Behavior is unchanged today: every new input is absent by default, so
production still fails closed and plan/--dry-run still work. 234 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 01:01:55 +02:00

132 lines
4.3 KiB
Python

import copy
import json
from types import SimpleNamespace
import pytest
from secrets_engine import cli
from secrets_engine.catalog import validate_entry
from secrets_engine.config import Config
from secrets_engine.errors import BackendError, DecisionError
from tests.test_catalog import VALID
def _config(tmp_path):
return Config(
catalog_dir=tmp_path,
policy_dir=tmp_path,
evidence_dir=tmp_path / "evidence",
hub_url="",
bao_addr="http://127.0.0.1:8200",
topic_id="test-topic",
)
def _records(tmp_path):
path = next((tmp_path / "evidence").glob("evidence-*.jsonl"))
return [json.loads(line) for line in path.read_text().splitlines()]
def _provision_args(entry):
return SimpleNamespace(
catalog_id=entry.id,
stage=entry.stage,
field="api_token",
generate=False,
from_file="/tmp/test-value-file",
bootstrap_token_file=None,
)
def test_provision_backend_exception_has_attempt_and_terminal_evidence(
tmp_path, monkeypatch
):
entry = validate_entry(copy.deepcopy(VALID))
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
monkeypatch.setattr(cli, "_require_lane_approval", lambda *_args, **_kwargs: None)
monkeypatch.setattr(cli.OpenBaoClient, "resolve", lambda *_args, **_kwargs: object())
monkeypatch.setattr(
cli,
"provision_from_file",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
BackendError("fake-SUPER-SECRET-backend-message")
),
)
with pytest.raises(BackendError):
cli.cmd_provision(_config(tmp_path), _provision_args(entry))
records = _records(tmp_path)
assert [record["result"] for record in records] == [
"attempt",
"failed-BackendError",
]
assert records[-1]["detail"]["approval_status"] == "not-required"
assert "fake-SUPER-SECRET" not in json.dumps(records)
def test_provision_decision_rejection_is_recorded_before_backend(
tmp_path, monkeypatch
):
data = copy.deepcopy(VALID)
data["approval"] = {
"model": "decision",
"decision_ref": "CCR-2026-0001",
}
entry = validate_entry(data)
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
monkeypatch.setattr(
cli,
"_require_lane_approval",
lambda *_args, **_kwargs: (_ for _ in ()).throw(DecisionError("not approved")),
)
monkeypatch.setattr(
cli.OpenBaoClient,
"resolve",
lambda *_args, **_kwargs: pytest.fail("backend must not be reached"),
)
with pytest.raises(DecisionError):
cli.cmd_provision(_config(tmp_path), _provision_args(entry))
records = _records(tmp_path)
assert [record["result"] for record in records] == [
"attempt",
"failed-DecisionError",
]
assert records[-1]["detail"]["approval_status"] == "rejected"
assert records[-1]["detail"]["decision_ref"] == "CCR-2026-0001"
def test_production_handler_fails_closed_before_backend(tmp_path, monkeypatch):
data = copy.deepcopy(VALID)
data.update(
stage="prod",
approval={"model": "decision", "decision_ref": "CCR-2026-0001"},
)
entry = validate_entry(data)
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
monkeypatch.delenv("SECRETS_ENGINE_UNSAFE_DEMO", raising=False)
monkeypatch.setattr(
cli.OpenBaoClient,
"resolve",
lambda *_args, **_kwargs: pytest.fail("backend must not be reached"),
)
with pytest.raises(DecisionError, match="production action 'provision'"):
cli.cmd_provision(_config(tmp_path), _provision_args(entry))
records = _records(tmp_path)
assert [record["result"] for record in records] == [
"attempt",
"failed-DecisionError",
]
terminal = records[-1]
assert terminal["detail"]["stance_stage"] == "prod"
assert terminal["detail"]["stance_failure_mode"] == "fail_closed"
assert terminal["detail"]["approval_status"] == "rejected"
assert "stance_decision_id" not in terminal["detail"]
assert terminal["completeness_claimed"] is False
assert "SUPER-SECRET" not in json.dumps(records)
outbox = list((tmp_path / "evidence" / "outbox").glob("*.json"))
assert outbox, "production provision refusal is load-bearing and must be queued"