flex-auth/workplans/FLEX-WP-0012-credential-grant-authorization-surface.md
tegwick acbaa4a7c9
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(policy): add credential grant authorization package
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02e47-6aac-7ee1-914d-0584c75d3c81
2026-08-23 13:59:03 +02:00

244 lines
11 KiB
Markdown

---
id: FLEX-WP-0012
type: workplan
title: "Authorize railiance-platform credential-grant requests"
domain: infotech
repo: flex-auth
status: active
owner: codex
topic_slug: netkingdom
planning_priority: P3
planning_order: 120
related_workplans:
- RAILIANCE-WP-0005
created: "2026-08-11"
updated: "2026-08-23"
state_hub_workstream_id: "89ecdb1f-ceb0-4a50-b72d-c3dfd5fa7c73"
---
# 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: wait
priority: medium
state_hub_task_id: "a1c9ba0c-3413-4823-9f9e-ccee09cf5d74"
```
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.
Progress 2026-08-23: revalidated both repositories. The helper still emits the
flat payload and defaults to `/credential-grants/authorize`; flex-auth still
serves only `/v1/check`. The translation choice is therefore current, not stale.
Request railiance-platform agreement before implementing either wire shape.
State Hub decision `1f9f257d-c9f2-4a5e-a018-8058a3f2a51a` records the two
options and was automatically escalated for human resolution; coordination
message `338e54d9-7313-433f-a8fe-8ad572d00365` asks the consumer to choose and
place duration normalization explicitly.
## T02 - Credential-grant policy package and fixtures
```task
id: FLEX-WP-0012-T02
status: done
priority: medium
state_hub_task_id: "7e9c4e59-e59c-4617-a87f-99142952fe78"
```
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.
Completed 2026-08-23. `examples/railiance-platform/` registers all three
current catalog grants and representative requester subjects. The policy uses
numeric `requested_ttl_seconds`; duration parsing belongs to the selected wire
translator, and strings are explicitly denied. Three embedded Rego tests and
six external fixtures pass, covering allow, TTL, actor type, purpose, delivery
mode, and unknown-grant default deny. Real `load-registry` and `check` commands
returned `credential_grant_allowed` for the registered runtime grant and
`unknown_grant` for an unregistered id.
## T03 - Implement the decided integration and prove it end to end
```task
id: FLEX-WP-0012-T03
status: wait
priority: medium
state_hub_task_id: "3335b2b7-cf1a-411d-95b3-03c4b4c35659"
```
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.
Waiting on T01's wire-translation decision. The policy-side normalized request
contract is implemented; either integration option must produce its numeric
TTL field without changing policy semantics.
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: wait
priority: low
state_hub_task_id: "40015a87-040c-4d48-b360-fd5566dbc552"
```
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.
The source-side action vocabulary is now present at
`docs/railiance-platform-action-vocabulary.md`; final consumer/deployment
handoff waits on T01 and T03.