FLEX-WP-0035 T01-T03: application access readback pin
Contract accepted by user-engine; FLEX-DEC-2026-019 accepted. - serve-readback: dedicated pin, no enforcement routes, own caller binding - CheckRequest.purpose hashed into every digest when set; /v1/check refuses it - POST /v1/readback/application_access and GET /v1/readback/systems - optional client_binding on protected systems; readback schemas - schemaguard implements maxItems - T05 added: informed-decision needs an application.access rule over realm-level claims (its lifecycle rules read authentication_audience) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 609675@bnt-lap001 Assistant-Session: 4987a36e-eb38-4b10-803c-15a29cd78212
This commit is contained in:
parent
b6d6bff85e
commit
8f2ba75e4a
23 changed files with 1314 additions and 44 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Application access readback
|
||||
|
||||
Status: proposed, for review with user-engine
|
||||
Status: accepted 2026-10-09 (user-engine review, message `f54df082`); implemented in `serve-readback`
|
||||
Date: 2026-10-09
|
||||
Workplan: `FLEX-WP-0035-T01`
|
||||
Decision: `FLEX-DEC-2026-019`
|
||||
|
|
@ -62,6 +62,15 @@ PEP:
|
|||
signed-in person, taken from its key-cape session. It is only as current as
|
||||
those claims. The target application's PEP receives its own claims on its
|
||||
own path.
|
||||
- **`application.access` rules may read only realm-level IAM Profile claims.**
|
||||
The claims user-engine forwards were issued for user-engine's own client
|
||||
(`aud`/`azp` = user-engine). A rule that reads the audience, the authorized
|
||||
party, or client-scoped roles would answer for the wrong client. This is a
|
||||
package-review checklist item for every package that declares the action.
|
||||
It applies today: `informed-decision.lifecycle` reads
|
||||
`input.subject.attributes.authentication_audience`, so its existing rules
|
||||
cannot be reused as-is for `application.access`. That package needs a
|
||||
separate rule over realm-level claims (`FLEX-WP-0035-T05`).
|
||||
|
||||
## 4. Where it runs: a dedicated readback pin
|
||||
|
||||
|
|
@ -91,10 +100,29 @@ multi-package mode keyed by `resource.system`, restricted to
|
|||
POST /v1/readback/application_access
|
||||
```
|
||||
|
||||
Schemas: `schemas/readback_request.schema.json`,
|
||||
`schemas/readback_response.schema.json`.
|
||||
|
||||
`subject` and `tenant` have exactly the `/v1/check` shape. user-engine sends
|
||||
what `FlexAuthHTTPAdapter` already sends there: IAM Profile v0.3 normalized,
|
||||
with `assurance` as the normalized form of `acr`/`amr`. There is no `action`,
|
||||
`resource` or `purpose` field. The decoder rejects unknown fields, the pin
|
||||
evaluates only `application.access`, and it sets `purpose` itself. Each row is
|
||||
evaluated against the resource
|
||||
`{"id": <system>, "type": "application", "system": <system>}`.
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": {"issuer": "https://key-cape…/realms/…", "sub": "…", "type": "user"},
|
||||
"context": {"groups": ["…"], "acr": "…", "amr": ["pwd", "otp"], "auth_time": 0},
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {
|
||||
"id": "…", "type": "human", "tenant": "tenant:platform",
|
||||
"attributes": {
|
||||
"issuer": "https://key-cape…/realms/…",
|
||||
"roles": [], "tenant_roles": [], "groups": ["…"], "scopes": [],
|
||||
"assurance": {"level": "aal2", "mfa": true},
|
||||
"auth_time": 0
|
||||
}
|
||||
},
|
||||
"systems": ["informed-decision", "markitect"]
|
||||
}
|
||||
```
|
||||
|
|
@ -119,28 +147,43 @@ Response, one entry per requested system and in request order:
|
|||
"policy_package": "informed-decision.lifecycle",
|
||||
"policy_package_digest": "sha256:…"
|
||||
},
|
||||
{"system": "markitect", "effect": "not_applicable", "reason": "no application.access rule"}
|
||||
{"system": "markitect", "effect": "not_applicable", "reason": "application_access_not_declared"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `effect` is one of `allow`, `deny`, `not_applicable`. A system the pin does
|
||||
not know answers `not_applicable`; it is never omitted. A whole-request
|
||||
failure is a non-2xx response, and user-engine renders every row
|
||||
`unavailable`.
|
||||
not know answers `not_applicable`; it is never omitted. Any evaluation or
|
||||
record failure fails the whole request with a non-2xx response, and
|
||||
user-engine renders every row `unavailable`.
|
||||
- Requests carry 1 to 64 unique systems. Anything else is a 400.
|
||||
- `revocation_visibility` restates per input class the deadlines published
|
||||
in `docs/decision-input-freshness.md`. It contains nothing new. The
|
||||
readback commits to no more than §9.7 already does.
|
||||
- user-engine must not display an allow past `lifetime.expires_at`, and must
|
||||
re-read rather than extend.
|
||||
|
||||
Rendering (user-engine, agreed 2026-10-09). No row is ever inferred as allow:
|
||||
|
||||
| Outcome | Rendered |
|
||||
| --- | --- |
|
||||
| `allow`, before `expires_at` | available |
|
||||
| `deny` | not available |
|
||||
| `not_applicable` | not available: "not offered here" |
|
||||
| non-2xx, or `allow` past `expires_at` | not available: "could not be checked" |
|
||||
|
||||
## 6. Records
|
||||
|
||||
Readback decisions are recorded like any decision. They form their own event
|
||||
class, `readback`, declared in `cadence.yaml` as not load-bearing: a readback
|
||||
`allow` authorizes nothing. Under `FLEX-DEC-2026-018`, a failed commit still
|
||||
withholds an allow (503, rendered `unavailable`) and still releases
|
||||
`deny`/`not_applicable`.
|
||||
Readback decisions are recorded like any decision. Under `FLEX-DEC-2026-018`,
|
||||
an allow whose record fails to commit is withheld (503, rendered "could not
|
||||
be checked"), and `deny`/`not_applicable` are still released. Both cases are
|
||||
tested.
|
||||
|
||||
user-engine reads on account-page view only. It reuses each row until
|
||||
`expires_at`, per session and subject, and drops the cache on logout or
|
||||
identity switch, which bounds record volume. Declaring a `readback` event class
|
||||
in `cadence.yaml` waits on the emission work (`FLEX-WP-0031`). Until then,
|
||||
`serve-readback` takes only `--log`, like today's production pins.
|
||||
|
||||
## 7. Registry projection (T03)
|
||||
|
||||
|
|
@ -163,16 +206,35 @@ Grants and revocations stay inside access-engine, in packages and registry
|
|||
content under their normal review. This work gives user-domain admins no grant
|
||||
or revoke surface.
|
||||
|
||||
## 9. Open questions for user-engine review
|
||||
## 9. Review outcome (user-engine, 2026-10-09)
|
||||
|
||||
1. **Claim set.** Is forwarding the session's verified claims (groups, `acr`,
|
||||
`amr`, `auth_time`) acceptable to you? Or do you want the readback to
|
||||
resolve groups itself? Resolving them itself would add a directory
|
||||
dependency.
|
||||
2. **First participants.** The proposal is informed-decision first: a
|
||||
human-facing web app with KeyCape AAL2 rules already in its packages. Which
|
||||
applications do you need in your first release?
|
||||
3. **Batch size.** The ceiling is 64 systems per request. Is that enough?
|
||||
4. **Record volume.** Each page view produces N decision records. If that is
|
||||
too much, the alternative is a shorter TTL plus client-side reuse up to
|
||||
`expires_at`, not unrecorded decisions.
|
||||
1. **Claims.** user-engine forwards verified claims in the `/v1/check` subject
|
||||
shape. There is no directory dependency. The realm-level-only rule is in
|
||||
§3.
|
||||
2. **First participant.** informed-decision. user-engine requests only systems
|
||||
that have a readback-pin registration with `client_binding`, a key-cape
|
||||
client, and a user-engine application record.
|
||||
3. **Batch size.** 64 is enough.
|
||||
4. **Record volume.** Account-page reads with reuse until `expires_at` (§6).
|
||||
5. **Caller token.** user-engine reads the readback token from its own file,
|
||||
`USER_ENGINE_READBACK_TOKEN_FILE`, separate from
|
||||
`USER_ENGINE_FLEX_AUTH_TOKEN_FILE`, with the same rotation pattern. OpenBao
|
||||
custody stays operator-owned.
|
||||
|
||||
## 10. Running it
|
||||
|
||||
```bash
|
||||
# --registry and --package repeat once per participating system;
|
||||
# system ids must not repeat across registries.
|
||||
flex-auth serve-readback \
|
||||
--registry examples/<system>/registry_snapshot.json \
|
||||
--package <system>=examples/<system>/policy_package.md \
|
||||
--caller-auth-mode enforce \
|
||||
--readback-caller system:serviceaccount:user-engine:user-engine
|
||||
```
|
||||
|
||||
The pin mounts `/healthz`, `POST /v1/readback/application_access` and
|
||||
`GET /v1/readback/systems`, and no enforcement route. `--readback-caller` is
|
||||
bound under the key `application-readback`. That key is not a protected
|
||||
system, so the principal can never represent a system on any pin's
|
||||
`/v1/check`. Tests: `cmd/flex-auth/readback_test.go`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue