feat: split the validator by owning layer per GH-DEC-2026-005
gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 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
This commit is contained in:
parent
dbd3694f71
commit
7b4b9e386e
8 changed files with 694 additions and 325 deletions
|
|
@ -19,19 +19,43 @@ does not redefine it.
|
|||
4. PEP OpenBao call → only after consume succeeds
|
||||
```
|
||||
|
||||
Step 1 is `resolve_consume_binding` (`approval_consume.py`): it reproduces the
|
||||
exact CheckRequest with `build_action_request`, fetches the durable
|
||||
`ActionAuthorization` from `GET /v1/approvals/{id}/claim`, and validates it with
|
||||
`validate_action_authorization` before returning a consume binding. Until
|
||||
2026-09-06 that function was a `return None` stub and the validator was
|
||||
unreachable from `src/`; the join now exists.
|
||||
**Each artifact is validated by the layer that owns its data**
|
||||
(`GH-DEC-2026-005`). There is no single bundled object:
|
||||
|
||||
| Step | Artifact | Owner | Validated by |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | approval-claim | approval-engine | `approval_claim.validate_approval_claim` |
|
||||
| 2 | DecisionEnvelope | flex-auth | `authorization.validate_decision_envelope` |
|
||||
|
||||
The claim carries the *approval fact*: issuer, `valid_now`, consumption state,
|
||||
binding digest, freshness, `reason_code`. The envelope carries the *decision*:
|
||||
effect, exact CheckRequest binding, canonical request digest, lifetime, and the
|
||||
policy package/version pin. Neither republishes the other's data.
|
||||
|
||||
`ActionAuthorization` is **deferred and was never ratified**
|
||||
(`FLEX-DEC-2026-006`). It cannot be served from a step-1 call, and nothing here
|
||||
validates it. A ratified post-decision form remains a deferred option.
|
||||
|
||||
There are **two different digests** over the same proposed action, and they are
|
||||
never compared to each other:
|
||||
|
||||
- `claim.binding.digest` — approval-engine native, `sha256` over canonical JSON
|
||||
of `{action, actor, principal, purpose, target}`.
|
||||
- `decision.binding.request_digest` — flex-auth canonical CheckRequest digest.
|
||||
|
||||
When the issuer recorded `claim.binding.pdp_digest`, that comparison is
|
||||
preferred. The CheckRequest mapping onto the claim binding is published in
|
||||
`approval-engine/docs/approval-claim.md`.
|
||||
|
||||
The approval-engine object id is never inferred from a State Hub decision UUID.
|
||||
It comes from catalog `approval.authorization_id` or a served
|
||||
`decision.authorization_id`. The Check request `id` is an opaque correlator the
|
||||
PEP cannot regenerate, so the served one is adopted; every security-relevant
|
||||
field is still compared exactly and the binding digest is recomputed against the
|
||||
served request.
|
||||
It comes from catalog `approval.authorization_id`, and is the value in the
|
||||
`{id}` path segment echoed back as `approval_id`.
|
||||
|
||||
**There is no authority constant.** The engine previously required
|
||||
`provenance.authority == "state-hub"`, which contradicted its own source: State
|
||||
Hub is a read model and holds no runtime approval authority, so that check
|
||||
failed closed against every correctly issued record. The approval fact's
|
||||
authority is approval-engine (checked as `issuer`); the decision's is flex-auth.
|
||||
|
||||
Every live privileged production handler passes `_require_lane_approval`,
|
||||
which calls `require_production_consume` before `OpenBaoClient.resolve`.
|
||||
|
|
@ -67,13 +91,23 @@ did before. Production additionally needs:
|
|||
| `SECRETS_ENGINE_APPROVAL_URL` | approval-engine base URL (claim + consume) |
|
||||
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | mode-0600 credential, outside Git |
|
||||
| `SECRETS_ENGINE_AUTHORIZATION_SUBJECT_ID` / `_SUBJECT_TYPE` | the acting principal |
|
||||
| `SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` / `_VERSION` | the live pin |
|
||||
| `SECRETS_ENGINE_AUTHORIZATION_MIN_APPROVALS` | distinct-approver threshold |
|
||||
| `SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` / `_VERSION` | the live pin (step 2) |
|
||||
|
||||
There is deliberately no default policy pin. flex-auth stated that the published
|
||||
`secrets-engine.lifecycle` / `v1` names are example vocabulary on the envelope,
|
||||
not a live package, so treating them as a default would pin production to a
|
||||
package nobody publishes.
|
||||
The distinct-approver threshold is no longer a consumer-side check. The claim
|
||||
does not expose approver entries; approval-engine folds that requirement into
|
||||
`valid_now`, which is true only when enough distinct authenticated approvers
|
||||
have been recorded and the object is not consumed, superseded, revoked, or
|
||||
expired.
|
||||
|
||||
There is deliberately no default policy pin. The reserved coordinate is
|
||||
`secrets-engine.catalog-lane.lifecycle` / `v1`, but that is a *reservation, not
|
||||
a publication* (`FLEX-DEC-2026-005`) and must not be configured until
|
||||
`FLEX-WP-0021-T02` publishes the package. `docs/gated-actions.md` supplies the
|
||||
action vocabulary that package is built from.
|
||||
|
||||
Step 2 additionally needs the `flex-auth-secrets-engine` Service DNS. No
|
||||
estate-wide PDP exists by design — flex-auth runs per-consumer cluster-local
|
||||
pins — and that pin has not been created, so the PDP call is not wired yet.
|
||||
|
||||
## What this does not do
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue