fix: bind approval consumption to actual Flex Auth submissions
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-09 08:55:46 +02:00
parent 89bc31460f
commit ee4e901611
23 changed files with 612 additions and 1023 deletions

View file

@ -17,7 +17,8 @@ from secrets_engine.approval_claim import (
binding_from_check_request,
claim_binding_digest,
)
from secrets_engine.approval_consume import resolve_consume_binding
from secrets_engine.approval_consume import resolve_approval_observation, authorize_action
from tests.authorization_stub import AuthorizationStub
from secrets_engine.authorization import build_action_request, request_digest
from secrets_engine.catalog import validate_entry
from secrets_engine.errors import DecisionError
@ -34,6 +35,8 @@ class _Cfg:
self.authorization_subject_type = "Human"
self.authorization_policy_package = "secrets-engine.lifecycle"
self.authorization_policy_version = "v1"
self.pdp_url = "http://127.0.0.1:1234"
self.pdp_token_file = token_file
self.authorization_min_approvals = 2
for k, v in over.items():
setattr(self, k, v)
@ -113,19 +116,19 @@ def _opener(envelope, status=200):
def _resolve(cfg, entry, envelope, action="deactivate"):
return resolve_consume_binding(
cfg, entry, action, None,
fields=("api_token",),
policy_targets=(entry.policy_name,),
auth_targets=(entry.role_name,),
opener=_opener(envelope),
)
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
def check(request, timeout=None):
submitted = json.loads(request.data)
return _opener(pdp.decision(submitted))(request)
return authorize_action(cfg, entry, action, None, fields=("api_token",),
policy_targets=(entry.policy_name,), auth_targets=(entry.role_name,),
opener=_opener(envelope), pdp_opener=check)
def test_unconfigured_serving_path_stays_fail_closed(tmp_path):
"""No URL/token/authorization id: None, exactly as before the join existed."""
cfg = _Cfg(None, approval_url="", approval_token_file=None)
assert resolve_consume_binding(cfg, _entry(), "deactivate", None) is None
assert resolve_approval_observation(cfg, _entry(), "deactivate", None) is None
def test_valid_authorization_yields_binding_with_canonical_digest(tmp_path):
@ -133,8 +136,8 @@ def test_valid_authorization_yields_binding_with_canonical_digest(tmp_path):
cfg = _Cfg(_token(tmp_path))
binding = _resolve(cfg, entry, _served())
assert binding is not None
assert binding.approval_id == AUTH_ID
assert binding.request_digest == request_digest(_expected_request(entry))
assert binding.binding.approval_id == AUTH_ID
assert binding.binding.request_digest != request_digest(_expected_request(entry))
def test_missing_subject_raises_instead_of_returning_none(tmp_path):
@ -149,21 +152,13 @@ def test_policy_pin_is_not_enforced_on_the_claim_path(tmp_path):
# The pin is a step-2 (DecisionEnvelope) concern after GH-DEC-2026-005 and
# is asserted in tests/test_action_authorization.py, not on the claim path.
cfg = _Cfg(_token(tmp_path), authorization_policy_package="")
assert _resolve(cfg, _entry(), _served()) is not None
assert resolve_approval_observation(cfg, _entry(), "deactivate", None, opener=_opener(_served())) is not None
def test_wrong_field_set_fails_closed(tmp_path):
"""A different proposed field set must not match the served digest."""
entry = _entry()
cfg = _Cfg(_token(tmp_path))
with pytest.raises(DecisionError):
resolve_consume_binding(
cfg, entry, "deactivate", None,
fields=("some_other_field",),
policy_targets=(entry.policy_name,),
auth_targets=(entry.role_name,),
opener=_opener(_served()),
)
claim = _served(fields=("other_field",))
with pytest.raises(DecisionError, match="pdp digest does not match"):
_resolve(_Cfg(_token(tmp_path)), _entry(), claim)
def test_action_mismatch_fails_closed(tmp_path):
@ -181,7 +176,7 @@ def test_unreachable_approval_engine_fails_closed(tmp_path):
raise URLError("no route")
with pytest.raises(DecisionError, match="unreachable"):
resolve_consume_binding(
resolve_approval_observation(
_Cfg(_token(tmp_path)), _entry(), "deactivate", None,
fields=("api_token",), opener=_boom,
)
@ -236,3 +231,24 @@ def test_wrong_pdp_digest_fails_closed(tmp_path):
claim["binding"]["pdp_digest"] = "sha256:" + "c" * 64
with pytest.raises(DecisionError, match="pdp digest does not match"):
_resolve(_Cfg(_token(tmp_path)), _entry(), claim)
def test_observation_is_not_a_consume_binding(tmp_path):
claim = _served()
claim["binding"]["pdp_digest"] = "sha256:" + "e" * 64
observation = resolve_approval_observation(_Cfg(_token(tmp_path)), _entry(), "deactivate", None,
opener=_opener(claim))
assert observation.claim == claim
assert observation.request["context"]["approval"] == claim
assert not hasattr(observation, "request_digest")
assert not hasattr(observation, "decision_id")
def test_claim_that_expires_during_check_cannot_yield_consume_binding(tmp_path, monkeypatch):
import secrets_engine.approval_consume as consumer
from secrets_engine.approval_claim import validate_approval_claim
def after_check(*args, **kwargs):
return validate_approval_claim(*args, **kwargs, now=datetime.now(timezone.utc) + timedelta(seconds=31))
monkeypatch.setattr(consumer, "validate_approval_claim", after_check)
with pytest.raises(DecisionError, match="observation is stale"):
_resolve(_Cfg(_token(tmp_path)), _entry(), _served())