flex-auth/docs/action-bound-authorization-contract.md
tegwick c473f1971d
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 37s
feat(authz): bind decisions to exact actions
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02e47-6aac-7ee1-914d-0584c75d3c81
2026-08-23 13:18:26 +02:00

103 lines
4.4 KiB
Markdown

# Action-bound authorization contract
Status: implemented for execute-time request binding; durable approval storage
requires State Hub support.
Date: 2026-08-23
## 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.
Consumers must compare structured fields; `reason`, `diagnostics`, titles, and
other prose are never an authorization contract.
State Hub owns durable organizational-decision identity, resolution,
supersession, and availability. Its current endpoint is:
```text
GET /decisions/{uuid}
```
That object is not yet sufficient for privileged action execution. As of
2026-08-23 it has lifecycle fields and `superseded_by`, but action details live
only in prose and it records only one free-form `decided_by` value. Production
consumers must not infer action binding or dual control from it.
## Durable object
[`../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 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.
## Outage and supersession semantics
- State Hub or future authorization-object store 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
State Hub needs a structured object/endpoint capable of storing this schema or
equivalent fields, authenticated approval entries, and atomic supersession.
Until that lands, 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.