diff --git a/workplans/FLEX-WP-0012-credential-grant-authorization-surface.md b/workplans/FLEX-WP-0012-credential-grant-authorization-surface.md new file mode 100644 index 0000000..73eb283 --- /dev/null +++ b/workplans/FLEX-WP-0012-credential-grant-authorization-surface.md @@ -0,0 +1,213 @@ +--- +id: FLEX-WP-0012 +type: workplan +title: "Authorize railiance-platform credential-grant requests" +domain: infotech +repo: flex-auth +status: proposed +owner: codex +topic_slug: netkingdom +planning_priority: P3 +planning_order: 120 +related_workplans: + - RAILIANCE-WP-0005 +created: "2026-08-11" +updated: "2026-08-11" +--- + +# FLEX-WP-0012 - Authorize railiance-platform credential-grant requests + +`railiance-platform`'s credential broker (`scripts/credential.py`) can call +flex-auth as a preflight before issuing a credential lease, posting non-secret +grant metadata and expecting an allow/deny back. It has been able to do this +since 2026-06-27. flex-auth has never answered, because nothing in this repo +serves the endpoint the helper posts to or understands the payload it sends. + +Opened from State Hub capability request `893ff109`, filed 2026-07-02 and +unanswered since. **Read the two corrections below before planning any work: +the request as filed rests on a premise that is not true, and the urgency it +describes has since evaporated.** + +## Correction 1: this is not blocking anyone + +The capability request says `RAILIANCE-WP-0005-T07` is blocked. It is not. +Later the same day, railiance-platform re-scoped and closed T07 on its own +scope, and `RAILIANCE-WP-0005` is now `finished`. Their closure note records +that the flex-auth call is optional by T07's own exit criteria, and that +**the grant catalog already enforces TTL, actor-type, purpose, and +delivery-mode bounds locally**. + +So this workplan buys *defence in depth and central, auditable policy* — a +second, independently-owned opinion on credential issuance, with decision +records — not the unblocking of stalled work. Priority is set accordingly. +If it is never done, railiance-platform keeps enforcing the same bounds +locally; what is lost is central policy and the decision log, not safety. + +Their closure note also says: "Decision taken autonomously (operator away); +revert to `wait` if Bernd prefers to hold WP-0005 open on flex-auth." That +choice is still open and should be confirmed rather than assumed. + +## Correction 2: the stated technical blocker is wrong + +The request says the CARING `/v1/check` schema "cannot express TTL bounds, +purpose, or delivery-mode checks." That is not correct, and the +counter-example already ships in this repo. + +`examples/ops-warden/policy_package.md` denies `ttl_out_of_bounds` today by +reading `input.context.ttl_hours` and comparing it against +`input.resource.attributes.max_ttl_hours`. `CheckRequest` carries open +`context` and `resource.attributes` maps; Rego reads whatever is in them. +The CARING profile constrains *descriptor* vocabulary — canonical roles, +planes, capabilities — not the request payload a policy may inspect. A +credential-grant preflight is the same shape as the ops-warden SSH signing +gate that has been in production since `FLEX-WP-0007`: bounded TTL, an actor +class, and a purpose. + +What is *actually* missing is narrower and worth stating precisely: + +1. No credential-grant policy package or protected-system manifest exists for + `railiance-platform`. +2. The helper posts to `/credential-grants/authorize`, which flex-auth does + not serve. **This is not the obstacle it appears to be** — the path is + configurable via `--flex-auth-path` / `FLEX_AUTH_PATH`, so it can be + pointed at `/v1/check` without a code change on their side. +3. The payload and response shapes genuinely do differ, and this is the one + real gap. The helper posts a flat object — `grant_id`, `actor`, + `actor_type`, `subject`, `purpose`, `requested_ttl`, `delivery_mode`, + `audience`, `issuer`, `credential_type` — and reads `allowed` / `decision` + / `status` plus optional `decision_id` and `reason`. `/v1/check` expects a + `CheckRequest` (`subject` / `action` / `resource` / `context`) and returns + `effect`, `reason`, `id`. Something has to translate. + +## T01 - Decide where the translation lives + +```task +id: FLEX-WP-0012-T01 +status: todo +priority: medium +``` + +Two honest options, and the choice is a charter question rather than a +technical one: + +**A. railiance-platform emits a `CheckRequest`.** No new flex-auth API +surface. `FLEX_AUTH_PATH=/v1/check`, the helper maps its own fields into +`subject`/`action`/`resource`/`context` and reads `effect`. Keeps flex-auth's +decision surface singular, which is what `SCOPE.md` describes. Costs a change +in a repo whose workplan is already `finished`, and pushes mapping knowledge +into the consumer — the thing `FLEX-WP-0008-T01` established should be +*coordinated*, not independently invented. + +**B. flex-auth serves a thin `/v1/credential-grants/authorize` adapter.** It +translates the flat payload into a `CheckRequest`, evaluates the +credential-grant package, and answers in the helper's vocabulary. No consumer +change beyond setting `FLEX_AUTH_URL`. Costs a second decision surface, and +risks the pattern `SCOPE.md` warns about — consumer-specific assumptions +leaking into the generic model — if the adapter grows policy logic instead of +staying a pure shape translation. + +Recommendation to argue with, not to accept by default: **B, on the strict +condition that the adapter contains no authorization logic whatsoever** — it +maps fields, delegates to the same engine, and returns the same decision id. +The ops-warden gate is the precedent for the *policy*; the adapter is only +about a consumer's wire format, and confining it to that is what keeps the +generic model clean. + +Record the decision and its reasoning in the policy package, in the style of +`FLEX-WP-0010-T02`. + +Done when the decision is recorded, and railiance-platform has agreed to it — +this one cannot be decided unilaterally, because either option changes +something they own. + +## T02 - Credential-grant policy package and fixtures + +```task +id: FLEX-WP-0012-T02 +status: todo +priority: medium +``` + +Add `examples/railiance-platform/` with a protected-system manifest, subject +manifest, registry snapshot, policy package, and fixtures. Model it on +`examples/ops-warden/`, which already encodes the hard part — bounded TTL +against a per-resource maximum. + +The four denials the capability request asks for, and their shapes: + +| Denial | Reads | +|---|---| +| overbroad TTL | requested TTL vs the grant's max, as ops-warden does | +| wrong actor type | `actor_type` against the grant's allowed classes (`human-operator`, `approved-agent`, `ci-runner`) | +| wrong purpose | purpose required and matching the grant's permitted purposes | +| disallowed delivery mode | `delivery_mode` against the grant's allowed modes | + +Note the TTL unit mismatch: the helper sends `requested_ttl` as a duration +string (`15m`), while the ops-warden package compares a numeric +`ttl_hours`. Decide where normalisation happens and make it explicit — a +policy that silently mis-parses `15m` as 15 would be a fail-*open* bug in the +one check most worth getting right. Prove the parse with a fixture, not an +assumption. + +Fixtures must cover allow plus one deny per row above, and an unknown grant +id. Verify against the real binary: `test-policy`, `load-registry`, and +`check` against standalone request files. + +Done when all Rego tests and fixtures pass and default-deny is demonstrated +for an unregistered grant. + +## T03 - Implement the decided integration and prove it end to end + +```task +id: FLEX-WP-0012-T03 +status: todo +priority: medium +``` + +Implement whichever shape T01 chose. If B, the adapter must reuse the +existing engine and emit the same decision id and provenance as `/v1/check` — +no parallel evaluation path, no second decision log. + +Then close the loop against the real helper, the way `FLEX-WP-0010-T03` did +rather than in isolation: run `flex-auth serve` with this registry, point +`scripts/credential.py` at it via `FLEX_AUTH_URL`, and exercise an allow plus +each of the four denials with `--require-flex-auth` set, so a deny actually +stops issuance rather than falling through to local preauthorization. + +Also verify the degraded path deliberately: with `--require-flex-auth` +**unset** and flex-auth unreachable, the helper is documented to continue by +local preauthorization. Confirm that is still what happens, and record it — +it means flex-auth is an *additional* gate here, not the deciding one, which +is a materially different security posture from the tenant-engine and +ops-warden gates and should not be quietly assumed to match them. + +**No secret material is involved anywhere in this flow.** The helper posts +request metadata only. Nothing in this workplan should read, log, or persist a +credential value; if a task appears to require one, that is a signal the +design has drifted, not a reason to handle secrets here. + +Done when allow and all four denials are demonstrated against the real helper +over real HTTP, `go test ./...` is green, and `gofmt`/`go vet` are clean. + +## T04 - Closure and handoff + +```task +id: FLEX-WP-0012-T04 +status: todo +priority: low +``` + +Reply to capability request `893ff109` — it has been open since 2026-07-02 +and deserves an answer either way, including if the answer is "declined, use +`/v1/check` directly." + +If the work lands, ship an image (`.forgejo/workflows/image.yaml` builds it) +and state plainly whether the credential-grant policy is *deployed* or only +*in source* — the distinction `FLEX-WP-0010` learned the hard way. Note that +the helper's flex-auth URL points at a local instance (`127.0.0.1:18090`), +not the cluster Services, so deployment here may mean something different +from the tenant-engine and user-engine rollouts; establish which runtime is +actually meant before promising an endpoint. + +Update `docs/` with a credential-grant action vocabulary, matching the +existing per-consumer vocabulary docs.