From 8ba1755718966e47b628fdf10313438fcedc69b4 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 10 Aug 2026 20:11:30 +0200 Subject: [PATCH] Plan tenant-engine lifecycle policy actions TEN-WP-0005 added tenant.update / tenant.retire / tenant.reactivate to tenant-engine's write API. FLEX-WP-0008's policy package predates them, so every lifecycle mutation resolves to deny unknown_action -- correct fail-closed, but it blocks TEN-WP-0005-T05 rollout and USER-WP-0021. Requested by tenant-engine; action strings taken verbatim from the live _RESOURCE_TYPES mapping. Co-Authored-By: Claude Opus 5 --- ...WP-0010-tenant-lifecycle-policy-actions.md | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md diff --git a/workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md b/workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md new file mode 100644 index 0000000..b6102f7 --- /dev/null +++ b/workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md @@ -0,0 +1,165 @@ +--- +id: FLEX-WP-0010 +type: workplan +title: "Authorize tenant-engine lifecycle actions" +domain: infotech +repo: flex-auth +status: ready +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" +--- + +# 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, T01–T04 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: todo +priority: high +``` + +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. + +## T02 - Decide whether retirement warrants stricter authorization + +```task +id: FLEX-WP-0010-T02 +status: todo +priority: medium +``` + +`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". + +## T03 - Fixtures and verification + +```task +id: FLEX-WP-0010-T03 +status: todo +priority: high +``` + +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. + +## T04 - Closure and handoff + +```task +id: FLEX-WP-0010-T04 +status: todo +priority: low +``` + +Confirm T01–T03. 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.