Delegate tenant lifecycle to the tenant authority
TEN-WP-0005 landed the authoritative metadata update and reversible retirement contract, so USER-WP-0021-T01's deferred tenant operations are now implementable without user-engine inventing lifecycle semantics. TenantManagementPort gains read, update, retire, and reactivate. The HTTP adapter echoes the record version as an If-Match ETag (never `*`), sends an Idempotency-Key plus actor/reason/correlation_id, and surfaces Idempotent-Replay. Authority failures map to redacted domain errors carrying only the contract's stable error_code; its detail text never crosses the boundary. Platform operators get the matching API routes and a CSRF-protected browser screen that reads the record before mutating it and hides the metadata form for a retired tenant. Portal OpenAPI moves to 0.3.0 with TenantRecord, UpdateTenant, and TenantLifecycleChange. Full suite: 145 tests, 3 external-provider skips. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
db8769cc8c
commit
667ea694c2
8 changed files with 952 additions and 11 deletions
|
|
@ -1,7 +1,7 @@
|
|||
openapi: 3.1.0
|
||||
info:
|
||||
title: user-engine portal API
|
||||
version: 0.2.0
|
||||
version: 0.3.0
|
||||
servers:
|
||||
- url: /api/v1
|
||||
security:
|
||||
|
|
@ -177,6 +177,93 @@ paths:
|
|||
responses:
|
||||
"201": {description: Tenant created or resumed and first administrator prepared}
|
||||
"403": {$ref: "#/components/responses/Denied"}
|
||||
/platform/tenants/{tenant}:
|
||||
get:
|
||||
operationId: readPlatformTenant
|
||||
description: >-
|
||||
Reads the authoritative tenant record from the tenant authority.
|
||||
user-engine keeps no tenant table; echo the returned version as If-Match
|
||||
on any mutation.
|
||||
parameters: [{$ref: "#/components/parameters/Tenant"}]
|
||||
responses:
|
||||
"200":
|
||||
description: Authoritative tenant record
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: "#/components/schemas/TenantRecord"}
|
||||
"403": {$ref: "#/components/responses/Denied"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
patch:
|
||||
operationId: updatePlatformTenant
|
||||
description: >-
|
||||
Changes allow-listed tenant metadata under an atomic compare-and-swap.
|
||||
The identifier is immutable because it is minted into issued tokens.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Tenant"
|
||||
- $ref: "#/components/parameters/IdempotencyKey"
|
||||
- $ref: "#/components/parameters/IfMatch"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: "#/components/schemas/UpdateTenant"}
|
||||
responses:
|
||||
"200":
|
||||
description: Updated tenant record
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: "#/components/schemas/TenantRecord"}
|
||||
"403": {$ref: "#/components/responses/Denied"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"409": {$ref: "#/components/responses/Conflict"}
|
||||
/platform/tenants/{tenant}/retire:
|
||||
post:
|
||||
operationId: retirePlatformTenant
|
||||
description: >-
|
||||
Reversibly retires a tenant through the tenant authority. There is no
|
||||
hard delete: grant and plan history are preserved for audit correlation.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Tenant"
|
||||
- $ref: "#/components/parameters/IdempotencyKey"
|
||||
- $ref: "#/components/parameters/IfMatch"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: "#/components/schemas/TenantLifecycleChange"}
|
||||
responses:
|
||||
"200":
|
||||
description: Retired tenant record
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: "#/components/schemas/TenantRecord"}
|
||||
"403": {$ref: "#/components/responses/Denied"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"409": {$ref: "#/components/responses/Conflict"}
|
||||
/platform/tenants/{tenant}/reactivate:
|
||||
post:
|
||||
operationId: reactivatePlatformTenant
|
||||
description: >-
|
||||
Restores a retired tenant. Revoked grants and plan state are deliberately
|
||||
not resurrected.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Tenant"
|
||||
- $ref: "#/components/parameters/IdempotencyKey"
|
||||
- $ref: "#/components/parameters/IfMatch"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: "#/components/schemas/TenantLifecycleChange"}
|
||||
responses:
|
||||
"200":
|
||||
description: Reactivated tenant record
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: "#/components/schemas/TenantRecord"}
|
||||
"403": {$ref: "#/components/responses/Denied"}
|
||||
"404": {$ref: "#/components/responses/NotFound"}
|
||||
"409": {$ref: "#/components/responses/Conflict"}
|
||||
/platform/tenants/{tenant}/users/{userId}/recover:
|
||||
post:
|
||||
operationId: recoverTenantUser
|
||||
|
|
@ -278,6 +365,45 @@ components:
|
|||
display_name: {type: string, maxLength: 200}
|
||||
additionalProperties: false
|
||||
additionalProperties: false
|
||||
TenantLifecycleChange:
|
||||
type: object
|
||||
required: [reason]
|
||||
properties:
|
||||
reason: {type: string, minLength: 1, maxLength: 200}
|
||||
additionalProperties: false
|
||||
UpdateTenant:
|
||||
type: object
|
||||
required: [metadata, reason]
|
||||
properties:
|
||||
reason: {type: string, minLength: 1, maxLength: 200}
|
||||
metadata:
|
||||
type: object
|
||||
description: >-
|
||||
Mutable tenant metadata. tenant_id, identifier, and grouping are
|
||||
immutable at the authority and are rejected here.
|
||||
minProperties: 1
|
||||
properties:
|
||||
display_name: {type: string, minLength: 1, maxLength: 200}
|
||||
contact_email: {type: string, format: email}
|
||||
additionalProperties: false
|
||||
additionalProperties: false
|
||||
TenantRecord:
|
||||
type: object
|
||||
description: Authoritative record owned by the tenant authority, not by user-engine.
|
||||
required: [tenant, external_ref, lifecycle, version]
|
||||
properties:
|
||||
tenant: {type: string, pattern: '^tenant:'}
|
||||
external_ref: {type: string}
|
||||
lifecycle: {type: string, enum: [active, retired, unknown]}
|
||||
version: {type: integer, minimum: 0}
|
||||
display_name: {type: string, nullable: true}
|
||||
contact_email: {type: string, nullable: true}
|
||||
retired_at: {type: string, nullable: true}
|
||||
reactivated_at: {type: string, nullable: true}
|
||||
replayed:
|
||||
type: boolean
|
||||
description: True when the authority replayed a durable idempotency receipt.
|
||||
additionalProperties: false
|
||||
UpdateSelfProfile:
|
||||
type: object
|
||||
required: [display_name, consent_accepted, consent_version]
|
||||
|
|
@ -304,3 +430,8 @@ components:
|
|||
content:
|
||||
application/json:
|
||||
schema: {$ref: "#/components/schemas/Error"}
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: "#/components/schemas/Error"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue