TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
|
|
|
|
|
from fastapi.testclient import TestClient
|
2026-08-29 13:02:51 +02:00
|
|
|
from helpers import AllowAllAuthorizer
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
|
|
|
|
|
from tenant_engine.app import create_app
|
|
|
|
|
from tenant_engine.domain import CapabilityRole, Tenant, create_role_grant
|
|
|
|
|
from tenant_engine.store import InMemoryTenantStore, TenantStore
|
|
|
|
|
|
|
|
|
|
|
2026-08-19 14:42:01 +02:00
|
|
|
def _client(store: TenantStore) -> TestClient:
|
2026-08-29 13:02:51 +02:00
|
|
|
return TestClient(create_app(store=store, authorizer=AllowAllAuthorizer()))
|
2026-08-19 14:42:01 +02:00
|
|
|
|
|
|
|
|
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
class _BrokenStore:
|
|
|
|
|
"""Test double: every active_roles() call raises, simulating an outage."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, delegate: TenantStore) -> None:
|
|
|
|
|
self._delegate = delegate
|
|
|
|
|
|
|
|
|
|
def create_tenant(self, tenant):
|
|
|
|
|
return self._delegate.create_tenant(tenant)
|
|
|
|
|
|
|
|
|
|
def get_tenant(self, tenant_id):
|
|
|
|
|
return self._delegate.get_tenant(tenant_id)
|
|
|
|
|
|
|
|
|
|
def grant_role(self, grant):
|
|
|
|
|
return self._delegate.grant_role(grant)
|
|
|
|
|
|
|
|
|
|
def revoke_role(self, **kwargs):
|
|
|
|
|
return self._delegate.revoke_role(**kwargs)
|
|
|
|
|
|
|
|
|
|
def active_roles(self, tenant_id):
|
|
|
|
|
from tenant_engine.store import StoreUnavailableError
|
|
|
|
|
|
|
|
|
|
raise StoreUnavailableError("simulated outage")
|
|
|
|
|
|
|
|
|
|
def assign_plan(self, assignment):
|
|
|
|
|
return self._delegate.assign_plan(assignment)
|
|
|
|
|
|
2026-08-29 13:02:51 +02:00
|
|
|
def events_for(self, tenant_id):
|
|
|
|
|
return self._delegate.events_for(tenant_id)
|
|
|
|
|
|
|
|
|
|
def record_authorization(self, record):
|
|
|
|
|
return None
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seeded_store() -> InMemoryTenantStore:
|
|
|
|
|
store = InMemoryTenantStore()
|
|
|
|
|
tenant = Tenant.create(tenant_id="t-binky", identifier="tenant:friendly:binky")
|
|
|
|
|
store.create_tenant(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),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cache_read_roles_returns_active_roles() -> None:
|
2026-08-19 14:42:01 +02:00
|
|
|
client = _client(_seeded_store())
|
|
|
|
|
response = client.get("/tenants/t-binky/roles", params={"actor": "key-cape"})
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json() == {"tenant_id": "t-binky", "roles": ["CUS"]}
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 00:15:26 +02:00
|
|
|
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.
|
|
|
|
|
"""
|
2026-08-19 14:42:01 +02:00
|
|
|
client = _client(_seeded_store())
|
2026-08-29 13:02:51 +02:00
|
|
|
response = client.get("/tenants/tenant:friendly:binky/roles", params={"actor": "key-cape"})
|
2026-07-24 00:15:26 +02:00
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json() == {"tenant_id": "tenant:friendly:binky", "roles": ["CUS"]}
|
|
|
|
|
|
|
|
|
|
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
def test_cache_read_roles_unknown_tenant_is_404() -> None:
|
2026-08-19 14:42:01 +02:00
|
|
|
client = _client(_seeded_store())
|
|
|
|
|
response = client.get("/tenants/does-not-exist/roles", params={"actor": "key-cape"})
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_live_lookup_roles_returns_active_roles() -> None:
|
2026-08-19 14:42:01 +02:00
|
|
|
client = _client(_seeded_store())
|
|
|
|
|
response = client.get("/tenants/t-binky/roles/live", params={"actor": "flex-auth"})
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json()["roles"] == ["CUS"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_live_lookup_fails_closed_on_store_outage() -> None:
|
|
|
|
|
broken = _BrokenStore(_seeded_store())
|
2026-08-19 14:42:01 +02:00
|
|
|
client = _client(broken)
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
|
2026-08-19 14:42:01 +02:00
|
|
|
response = client.get("/tenants/t-binky/roles/live", params={"actor": "flex-auth"})
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
|
|
|
|
|
assert response.status_code == 503
|
|
|
|
|
assert response.json() != {"tenant_id": "t-binky", "roles": []}, (
|
|
|
|
|
"outage must not be indistinguishable from a legitimate empty role list"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cache_read_also_fails_closed_on_store_outage() -> None:
|
|
|
|
|
broken = _BrokenStore(_seeded_store())
|
2026-08-19 14:42:01 +02:00
|
|
|
client = _client(broken)
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
|
2026-08-19 14:42:01 +02:00
|
|
|
response = client.get("/tenants/t-binky/roles", params={"actor": "key-cape"})
|
TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:24:09 +02:00
|
|
|
|
|
|
|
|
assert response.status_code == 503
|
2026-08-19 14:42:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unauthorized_reads_are_denied_before_tenant_existence_is_observed() -> None:
|
|
|
|
|
class _MustNotRead:
|
|
|
|
|
def get_tenant(self, tenant_id):
|
|
|
|
|
raise AssertionError(f"store read leaked for {tenant_id}")
|
|
|
|
|
|
|
|
|
|
def active_roles(self, tenant_id):
|
|
|
|
|
raise AssertionError(f"role read leaked for {tenant_id}")
|
|
|
|
|
|
|
|
|
|
client = TestClient(create_app(store=_MustNotRead()))
|
|
|
|
|
for path in (
|
|
|
|
|
"/tenants/known?actor=unbound",
|
|
|
|
|
"/tenants/unknown?actor=unbound",
|
|
|
|
|
"/tenants/known/roles?actor=unbound",
|
|
|
|
|
"/tenants/unknown/roles/live?actor=unbound",
|
|
|
|
|
):
|
|
|
|
|
response = client.get(path)
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
assert response.json()["error_code"] == "write_denied"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_read_requires_an_explicit_actor() -> None:
|
|
|
|
|
client = _client(_seeded_store())
|
|
|
|
|
assert client.get("/tenants/t-binky").status_code == 422
|
|
|
|
|
assert client.get("/tenants/t-binky/roles").status_code == 422
|
|
|
|
|
assert client.get("/tenants/t-binky/roles/live").status_code == 422
|