fix: bind approval consumption to actual Flex Auth submissions
Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
89bc31460f
commit
ee4e901611
23 changed files with 612 additions and 1023 deletions
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue