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
|
|
@ -13,6 +13,7 @@ from __future__ import annotations
|
|||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -97,21 +98,13 @@ def _parse_time(value: object, name: str) -> datetime:
|
|||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def validate_approval_claim(
|
||||
def _validate_observation(
|
||||
claim: object,
|
||||
*,
|
||||
approval_id: str,
|
||||
expected_binding_digest: str = "",
|
||||
expected_pdp_digest: str = "",
|
||||
now: datetime | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run the published consumer checks. Any failure means do not act.
|
||||
|
||||
Implements ``approval-claim.md`` "Required verification": issuer, valid_now,
|
||||
not consumed, binding digest match (native or PDP), freshness, reason_code.
|
||||
The distinct-approver threshold is folded into ``valid_now`` by the issuer;
|
||||
the claim does not expose approver entries, so it cannot be re-checked here.
|
||||
"""
|
||||
"""Validate issuer, shape, validity and freshness; no action correspondence."""
|
||||
if not isinstance(claim, dict):
|
||||
raise DecisionError("approval claim must be an object")
|
||||
if claim.get("schema_version") != SCHEMA_VERSION:
|
||||
|
|
@ -142,42 +135,6 @@ def validate_approval_claim(
|
|||
binding = claim.get("binding")
|
||||
if not isinstance(binding, dict):
|
||||
raise DecisionError("approval claim binding must be an object")
|
||||
native = str(binding.get("digest", "") or "")
|
||||
pdp = str(binding.get("pdp_digest", "") or "")
|
||||
# Prefer the PDP digest: it is expressed in flex-auth's vocabulary, which is
|
||||
# the one the caller actually proposed. The native digest is over
|
||||
# approval-engine's own vocabulary and is only comparable when the caller
|
||||
# supplies a binding built in that vocabulary (see the note in
|
||||
# resolve_consume_binding about the missing mapping).
|
||||
if expected_pdp_digest:
|
||||
# GH-DEC-2026-008 / approval-engine schema v3: pdp_path is the issuer's
|
||||
# DECLARATION that this approval was requested against a bound
|
||||
# CheckRequest, and it guarantees pdp_digest is non-null. Path intent is
|
||||
# never inferred from a pdp_digest that merely happens to be present --
|
||||
# a digest recorded for some other reason is not a declaration anybody
|
||||
# made, and approvals issued before schema v3 carry pdp_path false
|
||||
# regardless of any digest they hold.
|
||||
if binding.get("pdp_path") is not True:
|
||||
raise DecisionError(
|
||||
"approval claim does not declare binding.pdp_path; it was not "
|
||||
"issued against a bound CheckRequest and cannot authorize this "
|
||||
"action (GH-DEC-2026-008). Request an approval bound at issue."
|
||||
)
|
||||
if not pdp:
|
||||
raise DecisionError(
|
||||
"approval claim declares pdp_path but records no pdp_digest, and "
|
||||
"no published mapping exists between approval-engine and "
|
||||
"secrets-engine action/target vocabularies; the claim cannot be "
|
||||
"tied to this exact action"
|
||||
)
|
||||
if pdp != expected_pdp_digest:
|
||||
raise DecisionError("approval claim pdp digest does not match the request")
|
||||
elif expected_binding_digest:
|
||||
if native != expected_binding_digest:
|
||||
raise DecisionError("approval claim binding digest does not match the request")
|
||||
else:
|
||||
raise DecisionError("approval claim comparison requires an expected digest")
|
||||
|
||||
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
||||
freshness = claim.get("freshness")
|
||||
if not isinstance(freshness, dict):
|
||||
|
|
@ -194,3 +151,41 @@ def validate_approval_claim(
|
|||
if _parse_time(validity.get("not_before"), "validity.not_before") > current:
|
||||
raise DecisionError("approval claim is not yet valid")
|
||||
return claim
|
||||
|
||||
|
||||
def observe_pdp_approval_claim(claim: object, *, approval_id: str,
|
||||
now: datetime | None = None) -> dict[str, Any]:
|
||||
"""Validate a fresh fact for submission to the PDP, not authority to consume.
|
||||
|
||||
Action correspondence cannot be established until the evaluator returns its
|
||||
enriched approval binding. This function deliberately makes no such claim.
|
||||
"""
|
||||
observed = _validate_observation(claim, approval_id=approval_id, now=now)
|
||||
binding = observed["binding"]
|
||||
if binding.get("pdp_path") is not True:
|
||||
raise DecisionError("approval claim does not declare binding.pdp_path; request an approval bound at issue")
|
||||
pdp = binding.get("pdp_digest")
|
||||
if not isinstance(pdp, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", pdp):
|
||||
raise DecisionError("approval claim records no canonical pdp_digest; no published mapping permits a native fallback")
|
||||
return observed
|
||||
|
||||
|
||||
def validate_approval_claim(claim: object, *, approval_id: str,
|
||||
expected_binding_digest: str = "", expected_pdp_digest: str = "",
|
||||
now: datetime | None = None) -> dict[str, Any]:
|
||||
"""Validate the fact and compare an independently supplied binding.
|
||||
|
||||
A PDP digest supplied here must come from the evaluated decision, never a
|
||||
local reconstruction of the unenriched request.
|
||||
"""
|
||||
observed = _validate_observation(claim, approval_id=approval_id, now=now)
|
||||
if expected_pdp_digest:
|
||||
observe_pdp_approval_claim(observed, approval_id=approval_id, now=now)
|
||||
if observed["binding"]["pdp_digest"] != expected_pdp_digest:
|
||||
raise DecisionError("approval claim pdp digest does not match the request")
|
||||
elif expected_binding_digest:
|
||||
if observed["binding"].get("digest") != expected_binding_digest:
|
||||
raise DecisionError("approval claim binding digest does not match the request")
|
||||
else:
|
||||
raise DecisionError("approval claim comparison requires an expected digest")
|
||||
return observed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue