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

11 KiB
Raw Blame History

id type title domain repo status owner topic_slug planning_priority planning_order depends_on_workplans related_workplans created updated state_hub_workstream_id
FLEX-WP-0010 workplan Authorize tenant-engine lifecycle actions infotech flex-auth finished codex netkingdom P1 100
FLEX-WP-0008
TEN-WP-0005
USER-WP-0021
2026-08-10 2026-08-10 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

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

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

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

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.