flex-auth/workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md
codex 1d90aac30f
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix(workplans): adopt ADR-007 derived identifiers for unregistered records
These workplans exist only in the retired local hub. Their random pre-ADR-007
identifiers are refused by C-06 as stale references, so they cannot be
registered. Deriving from the canonical record id takes no identity from
anything: central does not hold them and the old ids die with the cache.

Records central already holds were deliberately left untouched.

Refs CUST-WP-0068-T06

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 20:10:35 +02:00

243 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: FLEX-WP-0010
type: workplan
title: "Authorize tenant-engine lifecycle actions"
domain: infotech
repo: flex-auth
status: finished
owner: codex
topic_slug: netkingdom
planning_priority: P1
planning_order: 100
depends_on_workplans:
- FLEX-WP-0008
related_workplans:
- TEN-WP-0005
- USER-WP-0021
created: "2026-08-10"
updated: "2026-08-10"
state_hub_workstream_id: "fdabae84-dc9e-5ccd-81df-8ae7e66b90b8"
---
# FLEX-WP-0010 - Authorize tenant-engine lifecycle actions
Extend the existing `tenant-engine` policy package (FLEX-WP-0008-T02) with the
three tenant lifecycle actions `TEN-WP-0005` introduced. Until they exist here,
every lifecycle mutation resolves to `deny` with `unknown_action` — correct
fail-closed behaviour, not a defect, but it means tenant-engine's new update
and retirement endpoints cannot perform a single write in production.
**Requested by:** `tenant-engine` (TEN-WP-0005, T01T04 complete and merged).
**Blocks:** `TEN-WP-0005-T05` (production rollout), and through it
`USER-WP-0021` (user-engine platform operator UI/API).
## Requirements from tenant-engine
Three new actions, all on the existing `tenant` resource type, all against
`system: "tenant-engine"`:
| Action | Resource type | HTTP surface |
|---|---|---|
| `tenant.update` | `tenant` | `PATCH /tenants/{tenant_id}` |
| `tenant.retire` | `tenant` | `POST /tenants/{tenant_id}/retire` |
| `tenant.reactivate` | `tenant` | `POST /tenants/{tenant_id}/reactivate` |
These strings are already live in
`tenant-engine/src/tenant_engine/authz.py`'s `_RESOURCE_TYPES` mapping —
they are the exact values `FlexAuthWriteAuthorizer` sends. Match them
verbatim; do not let the two repos invent different strings for the same
action (the coordination rule FLEX-WP-0008-T01 already established).
The `CheckRequest` shape is unchanged from FLEX-WP-0008: same
`FlexAuthCheckClient`, same `subject_type: "service"`, `resource_id` =
`tenant_id`. No new request fields, no schema change.
**Why three actions rather than one `tenant.write`:** tenant-engine
deliberately separated them so policy *can* permit an operator to rename a
tenant without thereby permitting them to retire it. Consumer contract:
`tenant-engine/docs/tenant-lifecycle-api.md`.
**Honest caveat on what this buys today:** under FLEX-WP-0008-T02's current
model there is exactly one known operator (the `tenant-engine` service
identity) and no `assurance` claim, so all three actions will resolve
identically once added. The separation is a seam for later, not
differentiated authorization now. Adding the actions is what unblocks
production; differentiating them is T02 below, and may well close as "not
yet, and here is why".
## T01 - Extend the policy package with the lifecycle actions
```task
id: FLEX-WP-0010-T01
status: done
priority: high
state_hub_task_id: "8deb9564-658e-5eff-8ef7-94838bee05cc"
```
Add `tenant.update`, `tenant.retire`, and `tenant.reactivate` to
`examples/tenant-engine/policy_package.md`'s `valid_actions` set and to the
frontmatter `actions:` list. Extend the `caring.capabilities` list if the
existing vocabulary (`Create`/`Grant`/`Revoke`/`Bind`/`Audit`) does not
already cover an update/retire/reactivate shape — a judgement call for this
repo, not pre-specified here.
Update `examples/tenant-engine/subject_manifest.yaml`'s subject metadata
description, which currently enumerates the original four actions.
Update `docs/tenant-engine-action-vocabulary.md` with the three actions, their
resource type, and the HTTP surface each gates. Keep the existing note that
this policy governs *who may call tenant-engine's admin API*, not a tenant's
own `PLTF`/`IAM`/`VEN`/`CUS` capability roles — the lifecycle actions do not
change that boundary and should not be read as doing so.
Rebuild `examples/tenant-engine/registry_snapshot.json`.
Done when `valid_actions` carries all seven actions and the vocabulary doc
and subject manifest no longer describe only four.
Done 2026-08-10: all seven actions are in `valid_actions`, the frontmatter
`actions:` list, and the protected-system manifest. CARING capabilities were
extended with `EditAny` (update), `Archive` (retire), and `Restore`
(reactivate) — the existing `Create`/`Grant`/`Revoke`/`Bind` set had no
update or lifecycle shape, and `Archive`/`Restore` name the reversible pair
exactly as tenant-engine implements it. `registry_snapshot.json` was rebuilt
from the two manifests, the subject metadata now enumerates all seven, and
`docs/tenant-engine-action-vocabulary.md` carries the three HTTP surfaces
plus an explicit note that `tenant.retire` asks whether *this caller* may
retire a tenant, not what a *retired tenant* may do.
## T02 - Decide whether retirement warrants stricter authorization
```task
id: FLEX-WP-0010-T02
status: done
priority: medium
state_hub_task_id: "82d13a89-a344-5797-b10f-390d17b11b73"
```
`tenant.retire` is the highest-consequence action in the set: it suspends a
tenant's ability to receive new capability grants and plan changes across the
whole platform. Decide whether it should require anything `tenant.update` does
not — a distinct subject, group, or (once available) assurance level.
Record the decision with its reasoning, in the style of FLEX-WP-0008-T02's
closure note. "No, not until `KEY-WP-0005` gives callers a real
`assurance`-bearing identity to check" is a perfectly good outcome — the point
is that the decision is explicit and recorded rather than defaulted into by
treating all seven actions as interchangeable.
Note that retirement is **reversible** by design (`tenant.reactivate`), and
that tenant-engine hard-deletes nothing — so the blast radius of a wrongly
allowed retirement is recoverable, which is legitimate input to this decision.
Done when the decision and its reasoning are recorded in
`policy_package.md`, and implemented if the answer is "yes, stricter".
Done 2026-08-10: **no — `tenant.retire` is authorized by the same rule as the
other six.** Recorded with full reasoning under "Decision: `tenant.retire`
does not require stricter authorization yet" in
`examples/tenant-engine/policy_package.md`. The load-bearing reason is that
there is nothing stricter to check: one service subject, no second operator,
no `assurance` field in the `CheckRequest`. A condition the sole caller
always satisfies would read to a later reviewer as though retirement were
separately controlled when it is not — a false assurance in a package whose
job is inspectability. Retirement being reversible and non-destructive makes
that acceptable. Revisit when `KEY-WP-0005` supplies an assurance-bearing
identity, or when a second `tenant-engine` operator subject is registered.
## T03 - Fixtures and verification
```task
id: FLEX-WP-0010-T03
status: done
priority: high
state_hub_task_id: "2fbceaf5-514b-5ded-adb1-f4e63ce96160"
```
Add `allow`/`deny` fixture pairs to
`examples/tenant-engine/policy_fixtures.yaml` for each of the three actions,
mirroring the existing eight pairs' shape. Cover at minimum: authorized
service subject → `allow`; unknown subject → `deny unknown_subject`; and a
misspelled lifecycle action → `deny unknown_action` (the case that guards
against the two repos drifting apart on action strings).
Verify the way FLEX-WP-0008-T02 did, against the real binary rather than in
isolation: `go build ./cmd/flex-auth`, then `test-policy` (all Rego tests and
fixtures), `load-registry`, and `check` against standalone request files.
Then close the loop end-to-end, which is what `TEN-WP-0005-T05` actually needs
evidence of: run a live `flex-auth serve` with this registry and point a real
`tenant-engine` at it (`TENANT_ENGINE_FLEX_AUTH_URL`), then exercise
`PATCH /tenants/{id}`, `POST .../retire`, and `POST .../reactivate` through
the unmodified `FlexAuthWriteAuthorizer`. tenant-engine's lifecycle endpoints
require `Idempotency-Key` and `If-Match` headers — read the record first and
echo its `ETag` back; `tenant-engine/docs/tenant-lifecycle-api.md` has the
call shapes.
Done when all three lifecycle mutations return a real `allow` from the real
Rego engine over real HTTP, `go test ./...` is green across the repo, and
`gofmt`/`go vet` are clean.
Done 2026-08-10: eight fixture pairs added (allow for each of the three
actions, `unknown_subject` deny for each, and two action-drift denies —
`tenant.retired` and `tenant_update`, both `unknown_action`). Five embedded
Rego tests added. `test-policy` reports 11/11 tests and 16/16 fixtures
passing; `load-registry` loads clean; `check` returns the expected decisions
for the two new standalone request files.
End-to-end against a live `flex-auth serve` (127.0.0.1:9098) with a real
`tenant-engine` on `TENANT_ENGINE_FLEX_AUTH_URL`, driven through the
unmodified `FlexAuthWriteAuthorizer` with `Idempotency-Key` and an `If-Match`
echoed from a prior `GET`:
| Call | tenant-engine | flex-auth decision |
| --- | --- | --- |
| `POST /tenants` | 201 | `decision:174dc9ecb03ed9e5` allow |
| `PATCH /tenants/t-e2e-1` | 200 `active` | `decision:6176c39c2f4d7b15` allow |
| `POST .../retire` | 200 `retired` | `decision:8a801b8ee8455080` allow |
| `POST .../reactivate` | 200 `active` | `decision:c64cf3713cecd970` allow |
| `POST .../retire` as `actor: ops` | 403 `write_denied` | `decision:59d3e99c6416be89` deny `unknown_subject` |
`go test ./...` green across all packages; `gofmt -l` empty; `go vet ./...`
clean. Evidence table mirrored into `examples/tenant-engine/README.md`.
## T04 - Closure and handoff
```task
id: FLEX-WP-0010-T04
status: done
priority: low
state_hub_task_id: "eb2579cb-b54f-5834-abcc-81954ac34889"
```
Confirm T01T03. Run `statehub fix-consistency`. Notify `tenant-engine` that
`TEN-WP-0005-T05` is unblocked, naming the policy revision — T05's own
done-criteria require the handoff to name the authorization policy revision
alongside the immutable image and API version.
Done 2026-08-10: T01T03 confirmed. Policy revision for the handoff is
package `tenant-engine.write-api.mutate` **version `v1`**, status `ready`,
profile `caring-0.4.0-rc2`, now carrying all seven actions — the version
string is unchanged because the package is additive and `v1` remains the
activated revision; the identifying fact for TEN-WP-0005-T05 is that `v1` as
of this commit contains `tenant.update`, `tenant.retire`, and
`tenant.reactivate`.
**Production is not yet serving this policy — stated plainly because
TEN-WP-0005-T05 depends on it.** The policy package is baked into the
flex-auth container image, not mounted from a ConfigMap: the deployed
`flex-auth-tenant-engine` Deployment in namespace `flex-auth` runs image
`@sha256:c25fc34a6cd7e64d955f8723ec70e176a583d5ae71d76280c4e2d89fba0fe0aa`
with `--policy /opt/flex-auth/examples/tenant-engine/policy_package.md`
from that image. Probed live on 2026-08-10 via port-forward:
`tenant.retire` still returns **deny `unknown_action`**, while
`tenant.create` returns allow — i.e. the running instance is the
pre-FLEX-WP-0010 four-action policy.
So `TEN-WP-0005-T05` is unblocked **in source** but not **in the cluster**
until a new image is built from commit `8e127e5` and rolled out. That
rollout is a production deploy that this workplan does not scope (contrast
FLEX-WP-0007 and FLEX-WP-0009-T03, which carried explicit deployment
tasks) and it is deliberately left for an owner decision rather than done
implicitly under a policy-authoring workplan. It is the single remaining
step between this commit and a live lifecycle mutation.