Implement tenant update and reversible retirement API (TEN-WP-0005 T01-T04)

Add an explicit tenant lifecycle (active/retired), allow-listed mutable
metadata, record versioning, and lifecycle timestamps to the tenant authority.

- domain: TenantLifecycle, with_metadata/retire/reactivate, immutability and
  transition invariants. Identifier stays immutable -- it is the IAM Profile
  `tenant` claim key-cape mints into tokens.
- store: mutate_tenant() commits idempotency replay, version CAS, mutation,
  and audit event together; durable receipts survive restart. Retired tenants
  refuse new grants and plan changes but keep their history.
- sqlite: forward-only idempotent migration; existing rows default to active
  at version 1. Reads now take the write lock -- the concurrent-writer test
  caught unguarded reads on the shared connection observing mid-transaction
  state as a spurious tenant_not_found.
- api: GET/PATCH /tenants/{id}, POST retire|reactivate. Idempotency-Key and
  If-Match required, distinct flex-auth actions per operation, stable error
  schema, redacted 503s.
- docs/tenant-lifecycle-api.md: consumer contract for user-engine.

Implemented against SQLite, not PostgreSQL as the workplan assumed --
TEN-WP-0004 shipped SQLite on a PVC as the production store.

124 tests pass (was 66); no breaking change to existing endpoints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-10 20:00:43 +02:00
parent 7dcccafc03
commit d6fd73bd42
10 changed files with 1625 additions and 28 deletions

View file

@ -4,7 +4,7 @@ type: workplan
title: "Tenant metadata update and reversible retirement API"
domain: infotech
repo: tenant-engine
status: ready
status: active
owner: codex
topic_slug: tenant-lifecycle
created: "2026-08-10"
@ -27,7 +27,7 @@ and audit correlation; it is not a hard-delete endpoint.
```task
id: TEN-WP-0005-T01
status: todo
status: done
priority: high
state_hub_task_id: "5b7d9022-4c0f-4bc3-9f63-b268836c8efc"
```
@ -49,11 +49,18 @@ Document stable response/error schemas for user-engine and other consumers.
Done when the OpenAPI contract makes concurrency, idempotency, authorization,
and lifecycle semantics unambiguous without defining a hard-delete operation.
Done 2026-08-10: lifecycle (`active`/`retired`), allow-listed mutable metadata
(`display_name`, `contact_email`), record version, and lifecycle timestamps are
in the domain contract. All four routes exist; the allow-list is enforced by
the request schema (`extra: forbid`) so it is visible in the OpenAPI document.
Consumer contract written up in `docs/tenant-lifecycle-api.md`. No hard-delete
operation was defined.
## T02 - Implement durable lifecycle state and migration
```task
id: TEN-WP-0005-T02
status: todo
status: done
priority: high
state_hub_task_id: "3596d8f2-06cf-4bf9-bdab-00cf48a25956"
```
@ -68,11 +75,23 @@ result across process restarts.
Done when in-memory and PostgreSQL conformance prove atomic compare-and-swap,
restart-safe idempotency, and lossless migration of existing tenants.
Done 2026-08-10, against SQLite rather than PostgreSQL: this workplan was
drafted assuming Postgres, but TEN-WP-0004 shipped SQLite on a PVC as the
production store, so the migration and conformance target the store that
actually runs. Adding an unused Postgres path would have been dead code.
The `TenantStore` Protocol keeps the seam if the backend changes later.
`mutate_tenant()` carries idempotency replay, version CAS, mutation, and audit
event in one commit -- splitting them would leave a window where a crash
yields a bumped version with no receipt (a retry then double-applies). The
migration is forward-only and idempotent: existing rows default to `active` at
version 1 with identifiers, grants, and plans untouched.
## T03 - Implement authorized update and lifecycle endpoints
```task
id: TEN-WP-0005-T03
status: todo
status: done
priority: high
state_hub_task_id: "083bcd28-47f4-4337-9bbe-e2f9f67cc2d1"
```
@ -91,11 +110,19 @@ grants or invent plan state.
Done when all lifecycle mutations are authorized, versioned, idempotent,
correlated, and provider-neutral.
Done 2026-08-10: `tenant.update`, `tenant.retire`, and `tenant.reactivate` are
distinct flex-auth actions, so policy can permit a metadata edit without
permitting a retirement. Authorization runs before the store is touched, so an
unauthorized caller cannot probe which tenants exist. 503s are redacted.
One deviation, deliberate: role *revocation* stays available while retired --
it only reduces privilege, and blocking it would be fail-open.
## T04 - Add lifecycle security and compatibility conformance
```task
id: TEN-WP-0005-T04
status: todo
status: done
priority: high
state_hub_task_id: "b716d7bf-ef6b-4b1d-bac3-e3f716d1a0b8"
```
@ -111,6 +138,18 @@ compatible.
Done when unit, API, store-conformance, PostgreSQL, and flex-auth tests pass and
the existing API behavior has no unplanned breaking change.
Done 2026-08-10: 124 tests pass (was 66). The store-conformance suite is
parametrised over both backends so the durable store cannot silently diverge
from the reference semantics. All 66 pre-existing tests still pass unchanged;
`POST /tenants` returns a superset of its previous body.
The concurrent-writer test caught a real pre-existing defect: reads on the
shared SQLite connection ran unguarded and could observe a row mid-transaction
from another thread, producing a spurious `tenant_not_found`. Reads now take
the same lock as writes.
Not covered: no PostgreSQL tests exist, per the T02 note above.
## T05 - Integrate and verify the production authority
```task
@ -131,3 +170,10 @@ Done when production evidence confirms durable lifecycle behavior and the
consumer handoff names the immutable image, API version, and authorization
policy revision.
Status 2026-08-10: still open, and it is the only thing between user-engine and
USER-WP-0021. Blocked on work outside this repo: the three flex-auth actions
must be added to the policy package (until then every lifecycle check
correctly resolves to deny), and image build plus rollout need cluster access.
The consumer-facing contract is finalized and ready to hand over:
`docs/tenant-lifecycle-api.md`.