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
|
|
@ -120,6 +120,21 @@ class TenantProvisioningResult:
|
|||
external_ref: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TenantRecord:
|
||||
"""Authoritative tenant state read back from the tenant authority."""
|
||||
|
||||
tenant: str
|
||||
external_ref: str
|
||||
lifecycle: str
|
||||
version: int
|
||||
display_name: str | None = None
|
||||
contact_email: str | None = None
|
||||
retired_at: str | None = None
|
||||
reactivated_at: str | None = None
|
||||
replayed: bool = False
|
||||
|
||||
|
||||
class TenantManagementPort(Protocol):
|
||||
"""Provider-neutral seam to the tenant authority (normally tenant-engine)."""
|
||||
|
||||
|
|
@ -129,6 +144,27 @@ class TenantManagementPort(Protocol):
|
|||
) -> TenantProvisioningResult:
|
||||
"""Create or resume a tenant without making user-engine authoritative."""
|
||||
|
||||
def tenant(self, *, tenant: str, correlation_id: str) -> TenantRecord:
|
||||
"""Read the authoritative record and the version to echo on a mutation."""
|
||||
|
||||
def update_tenant(
|
||||
self, *, tenant: str, metadata: Mapping[str, str], expected_version: int,
|
||||
reason: str, idempotency_key: str, correlation_id: str,
|
||||
) -> TenantRecord:
|
||||
"""Change allow-listed metadata under an atomic compare-and-swap."""
|
||||
|
||||
def retire_tenant(
|
||||
self, *, tenant: str, expected_version: int, reason: str,
|
||||
idempotency_key: str, correlation_id: str,
|
||||
) -> TenantRecord:
|
||||
"""Reversibly retire a tenant; the authority never hard-deletes."""
|
||||
|
||||
def reactivate_tenant(
|
||||
self, *, tenant: str, expected_version: int, reason: str,
|
||||
idempotency_key: str, correlation_id: str,
|
||||
) -> TenantRecord:
|
||||
"""Restore a retired tenant without resurrecting revoked grants."""
|
||||
|
||||
|
||||
class IdentityProvisioningPort(Protocol):
|
||||
"""Lifecycle seam owned by NetKingdom adapters, not the user domain."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue