Fix: resolve tenants by identifier, not only internal tenant_id

Found via a real cross-service check while implementing key-cape's
KEY-WP-0005-T02: key-cape's Go adapter called GET /tenants/tenant:coulomb/roles
and got a genuine 404 for a tenant that existed. External callers
(key-cape, flex-auth) only ever have a tenant's profile identifier, never
tenant-engine's internal tenant_id (caller-chosen at creation, otherwise
opaque). Every existing test happened to use identical strings for both
fields, so this was invisible until a real, independent second caller
exercised the documented contract.

InMemoryTenantStore gained a _by_identifier index and a _resolve() helper
every method calls first; create_tenant now also rejects a duplicate
identifier under a different internal id (an oversight the same fix
surfaced). 5 new tests, including the exact HTTP-level scenario with colon
characters in the URL path. 65 total, all 60 pre-existing tests unaffected.

Re-verified end-to-end for real: fresh flex-auth + tenant-engine + key-cape's
actual adapter code, over real HTTP -- roles=[IAM] ok=true resolving by
identifier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-24 00:15:26 +02:00
parent 18f510070f
commit 31e237cfa6
4 changed files with 193 additions and 17 deletions

View file

@ -64,6 +64,19 @@ def test_cache_read_roles_returns_active_roles() -> None:
assert response.json() == {"tenant_id": "t-binky", "roles": ["CUS"]}
def test_cache_read_roles_resolves_by_identifier_not_only_internal_id() -> None:
"""The real scenario key-cape's KEY-WP-0005-T02 hits: it only knows the
tenant's profile identifier (the IAM Profile `tenant` claim value), via
a URL path segment containing colons -- never the internal tenant_id.
"""
client = TestClient(create_app(store=_seeded_store()))
response = client.get("/tenants/tenant:friendly:binky/roles")
assert response.status_code == 200
assert response.json() == {"tenant_id": "tenant:friendly:binky", "roles": ["CUS"]}
def test_cache_read_roles_unknown_tenant_is_404() -> None:
client = TestClient(create_app(store=_seeded_store()))
response = client.get("/tenants/does-not-exist/roles")

View file

@ -104,6 +104,69 @@ def test_assign_plan() -> None:
assert any(event.event_type == "plan_assigned" and event.payload["plan_id"] == "plan-x" for event in events)
def test_get_tenant_resolves_by_identifier_not_only_internal_id() -> None:
"""Found via a real cross-service check (KEY-WP-0005-T02 against a live
tenant-engine): external callers like key-cape and flex-auth only ever
have the tenant's profile identifier (the IAM Profile `tenant` claim
value), never its internal tenant_id, which is caller-chosen at
creation and otherwise opaque.
"""
store, tenant = _store_with_tenant()
by_internal_id = store.get_tenant(tenant.tenant_id)
by_identifier = store.get_tenant(tenant.identifier)
assert by_internal_id == by_identifier == tenant
def test_active_roles_resolves_by_identifier() -> None:
store, tenant = _store_with_tenant()
store.grant_role(
create_role_grant(
tenant=tenant,
grant_id="g-1",
role=CapabilityRole.CUS,
grant_reason="manual_grant",
plan_id=None,
granted_by="ops",
correlation_id="corr-1",
granted_at=datetime.now(UTC),
)
)
assert store.active_roles(tenant.identifier) == frozenset({CapabilityRole.CUS})
def test_revoke_role_resolves_by_identifier() -> None:
store, tenant = _store_with_tenant()
store.grant_role(
create_role_grant(
tenant=tenant,
grant_id="g-1",
role=CapabilityRole.CUS,
grant_reason="manual_grant",
plan_id=None,
granted_by="ops",
correlation_id="corr-1",
granted_at=datetime.now(UTC),
)
)
revoked = store.revoke_role(tenant_id=tenant.identifier, grant_id="g-1", at=datetime.now(UTC))
assert revoked.revoked_at is not None
assert store.active_roles(tenant.tenant_id) == frozenset()
def test_create_tenant_rejects_duplicate_identifier_with_different_internal_id() -> None:
store, tenant = _store_with_tenant()
other = Tenant.create(tenant_id="a-different-internal-id", identifier=tenant.identifier)
with pytest.raises(TenantAlreadyExistsError):
store.create_tenant(other)
def test_every_mutation_emits_an_event() -> None:
store, tenant = _store_with_tenant()
grant = create_role_grant(