Report tenant grouping from the authority record
tenant-engine has made grouping mutable through its own reclassification route, so a tenant created as tenant:small:acme can report grouping "large". The identifier's grouping segment is now historical and must not be parsed. TenantRecord dropped the field entirely, so the portal read discarded the one safe source of a tenant's classification and left an operator with nothing but the identifier to infer from — exactly the mistake the change creates. The record and adapter now carry grouping, the operator screen shows it with a note that the identifier segment is not the grouping, and the OpenAPI schema documents where to read it. Also corrects the UpdateTenant description, which still claimed grouping was immutable. It is mutable, but never as metadata, because it resolves a tenant's spend ceiling. No reclassification control is offered here: that route is not deployed yet and, per tenant-engine, wants its own permission rather than riding on rename. Full suite: 149 tests, 3 provider-gated skips. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4e0f25acf2
commit
47a58d5cc2
7 changed files with 31 additions and 2 deletions
|
|
@ -30,6 +30,7 @@
|
|||
| workplan | USER-WP-0020 | finished | — | workplans/USER-WP-0020-self-service-and-user-administration-portal.md |
|
||||
| workplan | USER-WP-0021 | active | — | workplans/USER-WP-0021-portal-product-expansion.md |
|
||||
| workplan | USER-WP-0022 | blocked | — | workplans/USER-WP-0022-public-registration-and-jit-application-profiles.md |
|
||||
| workplan | USER-WP-0023 | active | — | workplans/USER-WP-0023-flex-auth-caller-identity.md |
|
||||
| task | USER-WP-0001-T1 | done | — | workplans/USER-WP-0001-preparation-and-interface-adoption.md |
|
||||
| task | USER-WP-0001-T2 | done | — | workplans/USER-WP-0001-preparation-and-interface-adoption.md |
|
||||
| task | USER-WP-0001-T3 | done | — | workplans/USER-WP-0001-preparation-and-interface-adoption.md |
|
||||
|
|
@ -165,3 +166,6 @@
|
|||
| task | USER-WP-0022-T03 | cancel | — | workplans/USER-WP-0022-public-registration-and-jit-application-profiles.md |
|
||||
| task | USER-WP-0022-T04 | cancel | — | workplans/USER-WP-0022-public-registration-and-jit-application-profiles.md |
|
||||
| task | USER-WP-0022-T05 | progress | — | workplans/USER-WP-0022-public-registration-and-jit-application-profiles.md |
|
||||
| task | USER-WP-0023-T01 | done | — | workplans/USER-WP-0023-flex-auth-caller-identity.md |
|
||||
| task | USER-WP-0023-T02 | done | — | workplans/USER-WP-0023-flex-auth-caller-identity.md |
|
||||
| task | USER-WP-0023-T03 | wait | — | workplans/USER-WP-0023-flex-auth-caller-identity.md |
|
||||
|
|
|
|||
|
|
@ -379,8 +379,10 @@ components:
|
|||
metadata:
|
||||
type: object
|
||||
description: >-
|
||||
Mutable tenant metadata. tenant_id, identifier, and grouping are
|
||||
immutable at the authority and are rejected here.
|
||||
Mutable tenant metadata. tenant_id and identifier are immutable at
|
||||
the authority and are rejected here. grouping is mutable but only
|
||||
through the authority's own reclassification route, never as
|
||||
metadata, because it resolves a tenant's spend ceiling.
|
||||
minProperties: 1
|
||||
properties:
|
||||
display_name: {type: string, minLength: 1, maxLength: 200}
|
||||
|
|
@ -396,6 +398,13 @@ components:
|
|||
external_ref: {type: string}
|
||||
lifecycle: {type: string, enum: [active, retired, unknown]}
|
||||
version: {type: integer, minimum: 0}
|
||||
grouping:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >-
|
||||
Authoritative classification, reported by the tenant authority.
|
||||
Read it from here; never parse it from the identifier, whose own
|
||||
grouping segment is historical once a tenant is reclassified.
|
||||
display_name: {type: string, nullable: true}
|
||||
contact_email: {type: string, nullable: true}
|
||||
retired_at: {type: string, nullable: true}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ class HTTPTenantManagementAdapter:
|
|||
external_ref=str(payload.get("tenant_id") or identifier),
|
||||
lifecycle=str(payload.get("lifecycle") or "unknown"),
|
||||
version=int(payload.get("version") or 0),
|
||||
grouping=payload.get("grouping"),
|
||||
display_name=payload.get("display_name"),
|
||||
contact_email=payload.get("contact_email"),
|
||||
retired_at=payload.get("retired_at"),
|
||||
|
|
|
|||
|
|
@ -128,6 +128,9 @@ class TenantRecord:
|
|||
external_ref: str
|
||||
lifecycle: str
|
||||
version: int
|
||||
# Authoritative classification. The identifier's own grouping segment is
|
||||
# historical after a reclassification, so it must never be parsed for this.
|
||||
grouping: str | None = None
|
||||
display_name: str | None = None
|
||||
contact_email: str | None = None
|
||||
retired_at: str | None = None
|
||||
|
|
|
|||
|
|
@ -1694,6 +1694,7 @@ class PortalApplication:
|
|||
f"Tenant {record.tenant}",
|
||||
f"""<h1>{escape(record.tenant)}</h1>
|
||||
<p>Lifecycle <strong>{escape(record.lifecycle)}</strong> at version {record.version}.</p>
|
||||
<p>Grouping <strong>{escape(record.grouping or 'not reported')}</strong>, as reported by the tenant authority. The identifier's own segment is historical after a reclassification and is not the grouping.</p>
|
||||
{replayed}
|
||||
{metadata_form}
|
||||
<section aria-labelledby="tenant-lifecycle"><h2 id="tenant-lifecycle">Lifecycle</h2>
|
||||
|
|
|
|||
|
|
@ -96,6 +96,16 @@ class TenantLifecycleAdapterTests(unittest.TestCase):
|
|||
self.assertEqual(record.version, 1)
|
||||
self.assertFalse(record.replayed)
|
||||
|
||||
def test_read_reports_the_authoritative_grouping(self):
|
||||
"""Grouping must come from the record; the identifier segment is historical."""
|
||||
request, record = self._call(
|
||||
"tenant",
|
||||
response={**RECORD, "identifier": "tenant:small:acme", "grouping": "large"},
|
||||
tenant="tenant:small:acme", correlation_id="corr-1",
|
||||
)
|
||||
self.assertEqual(record.tenant, "tenant:small:acme")
|
||||
self.assertEqual(record.grouping, "large")
|
||||
|
||||
def test_update_sends_a_version_etag_and_the_allow_listed_change(self):
|
||||
request, record = self._call(
|
||||
"update_tenant", response={**RECORD, "display_name": "Binky Ltd", "version": 2},
|
||||
|
|
|
|||
|
|
@ -847,6 +847,7 @@ class PortalApplicationTests(unittest.TestCase):
|
|||
)
|
||||
self.assertEqual("200 OK", page["status"])
|
||||
self.assertIn(b"Retire tenant", html)
|
||||
self.assertIn(b"not reported", html)
|
||||
self.assertIn(b'name="version" value="1"', html)
|
||||
|
||||
forged, _ = invoke(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue