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
7.7 KiB
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
- One pin evaluates one consumer. Each
flex-auth-<consumer>Deployment loads one--registrysnapshot and one--policypackage. Theflex-auth-user-enginepin holds no rules for any other application. It cannot answer for them, and widening it to do so would mix packages. - Caller auth is per system.
/v1/batch_checkauthorizes the caller token against everyresource.systemin the batch. A caller bound touser-enginegets 403 for any other system. That is correct, and it stays. - 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.
- 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. - 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 asunavailable. - 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, sorequest_digestandsubmitted_request_digestcan 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_digestof 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/checkor/v1/batch_checkon 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
POST /v1/readback/application_access
{
"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:
{
"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"}
]
}
effectis one ofallow,deny,not_applicable. A system the pin does not know answersnot_applicable; it is never omitted. A whole-request failure is a non-2xx response, and user-engine renders every rowunavailable.revocation_visibilityrestates per input class the deadlines published indocs/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)
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
- 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. - 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?
- Batch size. The ceiling is 64 systems per request. Is that enough?
- 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.