Pin tenant-engine to the CI-built digest from 7e68cc8, record rollback
digests, and close T05 after live create/update/retire/replay/reactivate
evidence against a disposable production tenant. Hands the 0.1.0 contract
to USER-WP-0021.
205 lines
8.9 KiB
Markdown
205 lines
8.9 KiB
Markdown
---
|
|
id: TEN-WP-0005
|
|
type: workplan
|
|
title: "Tenant metadata update and reversible retirement API"
|
|
domain: infotech
|
|
repo: tenant-engine
|
|
status: finished
|
|
owner: codex
|
|
topic_slug: tenant-lifecycle
|
|
created: "2026-08-10"
|
|
updated: "2026-08-14"
|
|
depends_on:
|
|
- TEN-WP-0004
|
|
unblocks:
|
|
- USER-WP-0021
|
|
state_hub_workstream_id: "bc1bd413-381a-4aca-aded-73cb3d0af529"
|
|
---
|
|
|
|
# TEN-WP-0005 - Tenant update and retirement API
|
|
|
|
Extend tenant-engine, the canonical tenant authority, with safe metadata update
|
|
and reversible retirement operations. Tenant identifiers remain immutable.
|
|
Retirement must preserve referential integrity, grant history, plan history,
|
|
and audit correlation; it is not a hard-delete endpoint.
|
|
|
|
## T01 - Specify tenant lifecycle and HTTP contracts
|
|
|
|
```task
|
|
id: TEN-WP-0005-T01
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "5b7d9022-4c0f-4bc3-9f63-b268836c8efc"
|
|
```
|
|
|
|
Add an explicit tenant lifecycle (`active` and `retired`), mutable display
|
|
metadata, record version, and lifecycle timestamps to the domain contract.
|
|
Specify:
|
|
|
|
- `GET /tenants/{tenant_id}` for authoritative lifecycle and metadata reads;
|
|
- `PATCH /tenants/{tenant_id}` for allow-listed metadata changes;
|
|
- `POST /tenants/{tenant_id}/retire` for reversible retirement;
|
|
- `POST /tenants/{tenant_id}/reactivate` for controlled recovery.
|
|
|
|
Every mutation requires `Idempotency-Key`, `If-Match`, actor, reason, and
|
|
correlation ID. Tenant IDs and identifiers are immutable. Unknown fields,
|
|
empty updates, stale versions, and invalid lifecycle transitions fail closed.
|
|
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: done
|
|
priority: high
|
|
state_hub_task_id: "3596d8f2-06cf-4bf9-bdab-00cf48a25956"
|
|
```
|
|
|
|
Extend the domain and both store implementations with display metadata,
|
|
lifecycle state, version, updated/retired/reactivated timestamps, and mutation
|
|
reason/correlation evidence. Add a forward-only PostgreSQL migration that
|
|
defaults existing tenants to active and preserves current identifiers, roles,
|
|
and plan assignments. Persist idempotency receipts so retries return the same
|
|
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: done
|
|
priority: high
|
|
state_hub_task_id: "083bcd28-47f4-4337-9bbe-e2f9f67cc2d1"
|
|
```
|
|
|
|
Implement the read, patch, retire, and reactivate handlers through the store
|
|
abstraction. Authorize mutations with distinct flex-auth actions:
|
|
`tenant.update`, `tenant.retire`, and `tenant.reactivate`. Return 409 for stale
|
|
versions or invalid transitions, 404 for unknown tenants, and redacted 503
|
|
responses for authority/store failures. Never reflect policy or database
|
|
details.
|
|
|
|
Retirement prevents new role grants and plan changes while retaining existing
|
|
history for audit and recovery. Reactivation does not silently restore revoked
|
|
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: done
|
|
priority: high
|
|
state_hub_task_id: "b716d7bf-ef6b-4b1d-bac3-e3f716d1a0b8"
|
|
```
|
|
|
|
Cover successful update, no-op/unknown-field rejection, identifier mutation
|
|
denial, stale-version conflict, duplicate idempotency replay, conflicting-key
|
|
reuse, double retirement, reactivation, update-after-retirement denial,
|
|
role/plan mutation denial while retired, cross-tenant authorization denial,
|
|
store outage, and concurrent writers. Verify error redaction and correlation.
|
|
Prove existing create, role-read, grant/revoke, and plan clients remain
|
|
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
|
|
id: TEN-WP-0005-T05
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "9b21782a-1b26-4fac-81a1-06bd7e8cf70f"
|
|
```
|
|
|
|
Publish an immutable tenant-engine image, apply the migration, add the three
|
|
flex-auth actions, and roll out with rollback evidence. Exercise create,
|
|
update, retire, denied mutation while retired, reactivate, and idempotent replay
|
|
against a disposable production tenant. Hand the finalized contract to
|
|
user-engine so USER-WP-0021 can implement its platform operator UI/API without
|
|
duplicating tenant authority.
|
|
|
|
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`.
|
|
|
|
Status 2026-08-14: flex-auth reports the seven-action policy live on
|
|
`flex-auth-tenant-engine` (`@sha256:9320df394a642eff24da8af4a0ee8886a7bb78b0f14d8ee1deeb30ea8eeeaba7`,
|
|
commit `e9911eb`). Production tenant-engine still runs the TEN-WP-0004 image
|
|
(`@sha256:2249e8c6…85149207`); its OpenAPI has no lifecycle routes. Recovered
|
|
the live manifests into `deploy/` and added the fleet CI image workflow so the
|
|
lifecycle image is built from a forge revision rather than a workstation.
|
|
|
|
Done 2026-08-14. Consumer handoff:
|
|
|
|
| Fact | Value |
|
|
| --- | --- |
|
|
| Immutable image | `forgejo.coulomb.social/coulomb/tenant-engine@sha256:08be0b1dcdc65575592b7be665c28e09a82316ea3d4c9b551ccb753f25360612` (CI, `main-7e68cc8`) |
|
|
| API version | `0.1.0` |
|
|
| Authorization policy | `tenant-engine.write-api.mutate` v1, seven actions, flex-auth `@sha256:9320df394a642eff24da8af4a0ee8886a7bb78b0f14d8ee1deeb30ea8eeeaba7` (`e9911eb`) |
|
|
| Contract | `docs/tenant-lifecycle-api.md` |
|
|
|
|
Live against disposable `tenant:trial:ten-wp-0005-t05` (left `retired`):
|
|
create 201 → update 200 v2 → retire 200 v3 → update-while-retired 409
|
|
`invalid_lifecycle_transition` → grant-while-retired 409 `tenant_retired` →
|
|
retire replay `Idempotent-Replay: true` v3 unchanged → reactivate 200 v4 →
|
|
actor `ops` 403 `write_denied`. Existing `tenant:trial:portalcheck` survived
|
|
the forward-only SQLite migration (`active`, version 1). Rollback is
|
|
`kubectl -n tenant-engine rollout undo`; last-known-good is the TEN-WP-0004
|
|
digest `2249e8c6…85149207` (ReplicaSet still present). USER-WP-0021 is
|
|
unblocked for update/retirement UI; this workplan has no leftover residual.
|
|
|