179 lines
7.7 KiB
Markdown
179 lines
7.7 KiB
Markdown
|
|
# Application access readback
|
||
|
|
|
||
|
|
Status: proposed, for review with user-engine
|
||
|
|
Date: 2026-10-09
|
||
|
|
Workplan: `FLEX-WP-0035-T01`
|
||
|
|
Decision: `FLEX-DEC-2026-019`
|
||
|
|
Consumer: user-engine (`USER-WP-0026-T03`, `USER-WP-0028-T02`)
|
||
|
|
|
||
|
|
user-engine wants to show a signed-in person which applications they can use.
|
||
|
|
This document defines what access-engine answers to that request, what the
|
||
|
|
answer means, and what it must never be used for.
|
||
|
|
|
||
|
|
## 1. Constraints from the current runtime
|
||
|
|
|
||
|
|
1. **One pin evaluates one consumer.** Each `flex-auth-<consumer>` Deployment
|
||
|
|
loads one `--registry` snapshot and one `--policy` package. The
|
||
|
|
`flex-auth-user-engine` pin holds no rules for any other application. It
|
||
|
|
cannot answer for them, and widening it to do so would mix packages.
|
||
|
|
2. **Caller auth is per system.** `/v1/batch_check` authorizes the caller
|
||
|
|
token against every `resource.system` in the batch. A caller bound to
|
||
|
|
`user-engine` gets 403 for any other system. That is correct, and it stays.
|
||
|
|
3. **There is no common action.** Each package has its own action
|
||
|
|
vocabulary. "May use the application" has no meaning until a package says
|
||
|
|
what it means.
|
||
|
|
4. **Registry content is not in decision provenance** until G1
|
||
|
|
(`FLEX-WP-0019`). Under the 2026-08-19 boundary, anything that decides an
|
||
|
|
allow belongs in a versioned policy package, not in the registry.
|
||
|
|
5. **No key-cape client binding exists** in `protected_system_manifest`.
|
||
|
|
|
||
|
|
## 2. The readback action: `application.access`, opted into per package
|
||
|
|
|
||
|
|
A system takes part in the readback by declaring the action
|
||
|
|
`application.access` in its own policy package and giving it rules there.
|
||
|
|
|
||
|
|
- The rules are the system's own. They are reviewed, versioned and tested like
|
||
|
|
any other action, and the decision records `policy_package_digest`.
|
||
|
|
- A system whose package does not declare the action answers
|
||
|
|
`not_applicable`. user-engine renders that as `unavailable`.
|
||
|
|
- **No registry-level mapping.** Mapping "application access" onto an existing
|
||
|
|
action in the registry was considered and rejected. Under constraint 4 it
|
||
|
|
would let content outside provenance decide an allow.
|
||
|
|
|
||
|
|
`application.access` means: this subject, holding these claims, may begin to
|
||
|
|
use the application. It grants nothing inside the application. Every action
|
||
|
|
the application performs is still checked by that application's PEP against
|
||
|
|
its own pin.
|
||
|
|
|
||
|
|
## 3. The answer is advisory and cannot be transferred
|
||
|
|
|
||
|
|
A readback decision is a real flex-auth decision. It is evaluated, recorded
|
||
|
|
and attributable, but it cannot be accepted as an enforcement decision by any
|
||
|
|
PEP:
|
||
|
|
|
||
|
|
- The binding carries `purpose: "readback"`. The field is part of the hashed
|
||
|
|
request, so `request_digest` and `submitted_request_digest` can never equal
|
||
|
|
those of an enforcement request for the same subject and resource. Under
|
||
|
|
§6.4.2 a PEP's digest comparison therefore fails.
|
||
|
|
- The readback route evaluates only `application.access`. Any other action is
|
||
|
|
refused with 400 before evaluation. The readback surface cannot produce a
|
||
|
|
decision about a real operation.
|
||
|
|
- The decision is evaluated over the claims that user-engine presents for the
|
||
|
|
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.
|
||
|
|
|
||
|
|
## 4. Where it runs: a dedicated readback pin
|
||
|
|
|
||
|
|
The readback runs on a dedicated pin, `flex-auth-application-readback`. It
|
||
|
|
loads the participating systems' packages at **the same package digests**
|
||
|
|
their own pins run, plus a merged registry snapshot.
|
||
|
|
|
||
|
|
- Drift control: each decision states the `policy_package_digest` of the
|
||
|
|
package it used. A test compares every package digest in the readback pin's
|
||
|
|
values against that consumer's own pin values, and fails on any mismatch.
|
||
|
|
- The only admitted caller is a readback binding (T02), for example
|
||
|
|
`application-readback=system:serviceaccount:user-engine:user-engine`. That
|
||
|
|
binding authorizes the readback route only. It never authorizes `/v1/check`
|
||
|
|
or `/v1/batch_check` on any pin.
|
||
|
|
- Rejected alternative: fan out from user-engine, or from a readback service,
|
||
|
|
to each consumer pin. That needs a caller binding and an ingress rule on
|
||
|
|
every consumer pin, which is exactly the widening constraint 2 forbids. It
|
||
|
|
also makes per-page latency the sum of N pins.
|
||
|
|
|
||
|
|
Code needed: `serve` today takes one `--policy`. The readback pin needs a
|
||
|
|
multi-package mode keyed by `resource.system`, restricted to
|
||
|
|
`application.access`.
|
||
|
|
|
||
|
|
## 5. Request and response
|
||
|
|
|
||
|
|
```http
|
||
|
|
POST /v1/readback/application_access
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"subject": {"issuer": "https://key-cape…/realms/…", "sub": "…", "type": "user"},
|
||
|
|
"context": {"groups": ["…"], "acr": "…", "amr": ["pwd", "otp"], "auth_time": 0},
|
||
|
|
"systems": ["informed-decision", "markitect"]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Response, one entry per requested system and in request order:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"registry_snapshot_digest": "sha256:…",
|
||
|
|
"results": [
|
||
|
|
{
|
||
|
|
"system": "informed-decision",
|
||
|
|
"effect": "allow",
|
||
|
|
"decision_id": "decision:…",
|
||
|
|
"lifetime": {"expires_at": "…"},
|
||
|
|
"revocation_visibility": {
|
||
|
|
"claims": "immediate",
|
||
|
|
"registry": "reload_or_lifetime",
|
||
|
|
"policy_package": "reload_or_lifetime",
|
||
|
|
"directory": "next_check"
|
||
|
|
},
|
||
|
|
"policy_package": "informed-decision.lifecycle",
|
||
|
|
"policy_package_digest": "sha256:…"
|
||
|
|
},
|
||
|
|
{"system": "markitect", "effect": "not_applicable", "reason": "no application.access rule"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- `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`.
|
||
|
|
- `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.
|
||
|
|
|
||
|
|
## 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`.
|
||
|
|
|
||
|
|
## 7. Registry projection (T03)
|
||
|
|
|
||
|
|
```http
|
||
|
|
GET /v1/readback/systems
|
||
|
|
```
|
||
|
|
|
||
|
|
The response lists every system in the merged snapshot: `id`, `name`,
|
||
|
|
`client_binding` (`{issuer, client_id}` or absent), `readback` (whether its
|
||
|
|
package declares `application.access`), and the `registry_snapshot_digest`.
|
||
|
|
|
||
|
|
This needs one new optional field on `protected_system_manifest`:
|
||
|
|
`client_binding`. A system without one is still listed, and user-engine's rule
|
||
|
|
then excludes it from the workload catalogue. The projection authorizes
|
||
|
|
nothing. It describes what the readback pin knows.
|
||
|
|
|
||
|
|
## 8. Grants
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
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.
|