flex-auth/docs/action-bound-authorization-contract.md

135 lines
6.5 KiB
Markdown
Raw Normal View History

# Action-bound authorization contract
Accept ActionAuthorization deferral; fix the state-hub authority constant approval-engine filed APPROVAL-IN-0002: secrets-engine built its PEP validator against our ActionAuthorization schema, pointed it at GET /v1/approvals/{id}/claim, and it rejects every response. Both envelopes declare schema_version 0.1, so it fails late and reads like an approval-engine outage rather than a contract mismatch. FLEX-DEC-2026-006 accepts the deferral and argues against flex-auth's own proposal. The composed object had the PIP republish our decision, which crosses the same layer boundary we invoked to decline authentication evidence and to win section 17's schema. The claim-plus-DecisionEnvelope split drops no check; each verification lands on the layer that owns it. approval-engine asked, before the decision, whether the open G3 finding argues for ratifying now. It does not: G3 is already closed the other way. FLEX-WP-0019 added lifetime to the DecisionEnvelope itself, required on every allow by schema conditional, published 2026-09-02. The trigger resolved by adding a field rather than by composition, so the decision stands alone and needs no bundle. The provenance.authority == state-hub constant is our defect and is fixed at source. It came from examples/caring/action_authorization.json, which contradicted the same contract's ownership section. That fixture now names approval-engine as the approval fact's authority and flex-auth as the decision's, and its stale secrets-engine.lifecycle pin is corrected to the reserved coordinate from FLEX-DEC-2026-005. The contract doc and schema are marked deferred-not-withdrawn so no other consumer builds a validator against them. The execute-time half is untouched: /v1/check, binding, the canonical digest, and flex-auth.decision-record.v1 stay published. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTbVXpEiXA7mNJVpDnEPcB Assistant: claude-code Assistant-Model: opus Assistant-Process: 412054@bnt-lap001 Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 01:30:04 +02:00
Status: **execute-time request binding is implemented and published.** The
composed `ActionAuthorization` object in this document is **deferred, not
withdrawn** — see "Deferral of the composed object" below. Do not build a
validator against it.
Accept ActionAuthorization deferral; fix the state-hub authority constant approval-engine filed APPROVAL-IN-0002: secrets-engine built its PEP validator against our ActionAuthorization schema, pointed it at GET /v1/approvals/{id}/claim, and it rejects every response. Both envelopes declare schema_version 0.1, so it fails late and reads like an approval-engine outage rather than a contract mismatch. FLEX-DEC-2026-006 accepts the deferral and argues against flex-auth's own proposal. The composed object had the PIP republish our decision, which crosses the same layer boundary we invoked to decline authentication evidence and to win section 17's schema. The claim-plus-DecisionEnvelope split drops no check; each verification lands on the layer that owns it. approval-engine asked, before the decision, whether the open G3 finding argues for ratifying now. It does not: G3 is already closed the other way. FLEX-WP-0019 added lifetime to the DecisionEnvelope itself, required on every allow by schema conditional, published 2026-09-02. The trigger resolved by adding a field rather than by composition, so the decision stands alone and needs no bundle. The provenance.authority == state-hub constant is our defect and is fixed at source. It came from examples/caring/action_authorization.json, which contradicted the same contract's ownership section. That fixture now names approval-engine as the approval fact's authority and flex-auth as the decision's, and its stale secrets-engine.lifecycle pin is corrected to the reserved coordinate from FLEX-DEC-2026-005. The contract doc and schema are marked deferred-not-withdrawn so no other consumer builds a validator against them. The execute-time half is untouched: /v1/check, binding, the canonical digest, and flex-auth.decision-record.v1 stay published. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTbVXpEiXA7mNJVpDnEPcB Assistant: claude-code Assistant-Model: opus Assistant-Process: 412054@bnt-lap001 Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 01:30:04 +02:00
Date: 2026-08-23 (deferral recorded 2026-09-06, `FLEX-DEC-2026-006`)
## Ownership and endpoints
flex-auth owns authorization evaluation and the canonical request/decision
vocabulary. A protected system obtains a current decision from:
```text
POST /v1/check
```
The response now includes `binding`, the exact normalized subject, action,
resource, context, and full SHA-256 request digest evaluated by the policy.
How that digest is computed, what is excluded, and when replay is permitted
are in [`canonical-request-digest.md`](canonical-request-digest.md). Consumers
must compare structured fields; `reason`, `diagnostics`, titles, and other
prose are never an authorization contract.
The decision envelope itself is the published PDP contract
[`decision-record-contract.md`](decision-record-contract.md).
`approval-engine` owns the durable approval object, authenticated approval
evidence, storage, supersession, consumption, and availability. State Hub
decision records are coordination and provenance evidence; they are not the
runtime approval authority. Production consumers must not infer action binding
or dual control from State Hub prose or a free-form `decided_by` value.
Accept ActionAuthorization deferral; fix the state-hub authority constant approval-engine filed APPROVAL-IN-0002: secrets-engine built its PEP validator against our ActionAuthorization schema, pointed it at GET /v1/approvals/{id}/claim, and it rejects every response. Both envelopes declare schema_version 0.1, so it fails late and reads like an approval-engine outage rather than a contract mismatch. FLEX-DEC-2026-006 accepts the deferral and argues against flex-auth's own proposal. The composed object had the PIP republish our decision, which crosses the same layer boundary we invoked to decline authentication evidence and to win section 17's schema. The claim-plus-DecisionEnvelope split drops no check; each verification lands on the layer that owns it. approval-engine asked, before the decision, whether the open G3 finding argues for ratifying now. It does not: G3 is already closed the other way. FLEX-WP-0019 added lifetime to the DecisionEnvelope itself, required on every allow by schema conditional, published 2026-09-02. The trigger resolved by adding a field rather than by composition, so the decision stands alone and needs no bundle. The provenance.authority == state-hub constant is our defect and is fixed at source. It came from examples/caring/action_authorization.json, which contradicted the same contract's ownership section. That fixture now names approval-engine as the approval fact's authority and flex-auth as the decision's, and its stale secrets-engine.lifecycle pin is corrected to the reserved coordinate from FLEX-DEC-2026-005. The contract doc and schema are marked deferred-not-withdrawn so no other consumer builds a validator against them. The execute-time half is untouched: /v1/check, binding, the canonical digest, and flex-auth.decision-record.v1 stay published. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTbVXpEiXA7mNJVpDnEPcB Assistant: claude-code Assistant-Model: opus Assistant-Process: 412054@bnt-lap001 Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 01:30:04 +02:00
## Deferral of the composed object
`ActionAuthorization` was never ratified. `GH-DEC-2026-003` (2026-08-29) named
`approval-engine`'s **approval-claim** as the step-1 artifact, and flex-auth
accepts that split (`FLEX-DEC-2026-006`):
- **Step 1 — approval fact:** `approval-engine`'s approval-claim, served from
`GET /v1/approvals/{id}/claim`. `approval-engine` is its authority.
- **Step 2 — decision:** flex-auth's `DecisionEnvelope`, with exact
`CheckRequest` match via `binding.request_digest` and the policy pin.
A PEP validates **across both**. No check from the composed object is dropped;
each one lands on the layer that owns it. The composed object would have had
the PIP republish flex-auth's decision, which is the part that was wrong.
The `PROPOSED` framing below is retained as the record of what was proposed.
It is not a contract, and a consumer that validates a `GET /v1/approvals/{id}/claim`
response against it will reject every response — both envelopes declare
`schema_version: 0.1`, so the mismatch surfaces late and reads like an
`approval-engine` outage. That is exactly what happened to secrets-engine.
## Durable object (deferred — proposed, never ratified)
[`../schemas/action_authorization.schema.json`](../schemas/action_authorization.schema.json)
defines the proposed `ActionAuthorization` storage and transport object. It
combines:
- a canonical UUID and explicit lifecycle status;
- one complete `CheckRequest` containing the actor, action, resource, exact
target attributes, stage, purpose, and other policy inputs;
- a bounded validity window;
- approval count and individual approval evidence;
- the flex-auth `DecisionEnvelope` whose structured binding and digest match
that request.
The schema is generic. For secrets-engine, use this profile:
| Requirement | Canonical field |
| --- | --- |
| catalog id | `request.resource.id` |
| stage | `request.resource.attributes.stage` |
| lifecycle operation | `request.action` |
| exact secret fields | `request.resource.attributes.fields` |
| policy/auth targets | `request.resource.attributes.policy_targets` / `auth_targets` |
| requesting actor | `request.subject` |
| purpose | `request.context.purpose` |
| execution window | `validity.not_before` / `validity.expires_at` |
| dual control | `approvals.required_count` and distinct `entries[].subject_id` |
Array values that express exact targets are sets: producers sort and de-duplicate
them before evaluation; consumers require exact set equality. Wildcards are
policy inputs and should be denied for destructive production actions.
## Required verification
A production consumer may execute only when all of the following hold:
1. The durable object resolves from `approval-engine`, its canonical authority;
an outage fails the action closed.
2. `status` is `approved`, `superseded_by` is absent, and the current time is
inside the validity window.
3. The caller's proposed action exactly matches `request`, including resource
id, system, stage, target sets, actor, and purpose.
4. `decision.effect` is `allow`; `decision.binding` matches the request and its
digest; the policy package/version are accepted for the deployment.
5. Approval entries contain at least `required_count` distinct authenticated
subjects. Repeated entries from one subject count once.
Local fixtures, CCR labels, workplan ids, prose, and a caller-supplied flex-auth
decision id are provenance only. They cannot unlock a production action.
Accept ActionAuthorization deferral; fix the state-hub authority constant approval-engine filed APPROVAL-IN-0002: secrets-engine built its PEP validator against our ActionAuthorization schema, pointed it at GET /v1/approvals/{id}/claim, and it rejects every response. Both envelopes declare schema_version 0.1, so it fails late and reads like an approval-engine outage rather than a contract mismatch. FLEX-DEC-2026-006 accepts the deferral and argues against flex-auth's own proposal. The composed object had the PIP republish our decision, which crosses the same layer boundary we invoked to decline authentication evidence and to win section 17's schema. The claim-plus-DecisionEnvelope split drops no check; each verification lands on the layer that owns it. approval-engine asked, before the decision, whether the open G3 finding argues for ratifying now. It does not: G3 is already closed the other way. FLEX-WP-0019 added lifetime to the DecisionEnvelope itself, required on every allow by schema conditional, published 2026-09-02. The trigger resolved by adding a field rather than by composition, so the decision stands alone and needs no bundle. The provenance.authority == state-hub constant is our defect and is fixed at source. It came from examples/caring/action_authorization.json, which contradicted the same contract's ownership section. That fixture now names approval-engine as the approval fact's authority and flex-auth as the decision's, and its stale secrets-engine.lifecycle pin is corrected to the reserved coordinate from FLEX-DEC-2026-005. The contract doc and schema are marked deferred-not-withdrawn so no other consumer builds a validator against them. The execute-time half is untouched: /v1/check, binding, the canonical digest, and flex-auth.decision-record.v1 stay published. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTbVXpEiXA7mNJVpDnEPcB Assistant: claude-code Assistant-Model: opus Assistant-Process: 412054@bnt-lap001 Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 01:30:04 +02:00
**State Hub is never the authority here.** A validator must not require
`provenance.authority == "state-hub"`, or accept it as evidence. State Hub
decision records are coordination and provenance evidence, as the Ownership
section above states. The authority of the approval fact is `approval-engine`;
the authority of the decision is flex-auth. The stale `"authority": "state-hub"`
value in `examples/caring/action_authorization.json` was the source of that
constant and is corrected.
## Outage and supersession semantics
- `approval-engine` unreachable: privileged live
action denied; read-only inspection and dry-run may continue with a clear
non-authoritative status.
- flex-auth unavailable when a fresh decision is required: privileged live
action denied. A previously stored decision is usable only inside its bounded
validity window and only if the authoritative lifecycle object can still be
checked for supersession or revocation.
- `superseded`, `expired`, `revoked`, `denied`, or unknown status: deny.
- `superseded_by` present regardless of status: deny and resolve the replacement
explicitly; never follow it silently during execution.
## Remaining external work
`approval-engine` needs a structured object/endpoint capable of storing this
schema or equivalent fields, authenticated approval entries, atomic
supersession, and single-consumption semantics. Until that lands and the
consumer proof passes, secrets-engine is correct to keep live destructive
actions disabled. The standalone evaluator and the Topaz, relationship, rule,
and Keycloak delegated adapter boundaries now all populate the same binding.