TEN-WP-0006-T05: cross-tenant denial and write-path outage conformance
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b6d016869f
commit
d1d9c9a735
2 changed files with 61 additions and 1 deletions
|
|
@ -27,11 +27,34 @@ class _ScopedAuthorizer(WriteAuthorizer):
|
||||||
raise WriteAuthorizationDeniedError(action, "not permitted")
|
raise WriteAuthorizationDeniedError(action, "not permitted")
|
||||||
|
|
||||||
|
|
||||||
|
class _TenantScopedAuthorizer(WriteAuthorizer):
|
||||||
|
"""Permits guardrail work on exactly one tenant.
|
||||||
|
|
||||||
|
Stands in for a flex-auth policy that scopes an operator to their own
|
||||||
|
tenant -- the case where a caller is authenticated and permitted in
|
||||||
|
general, but not for *this* tenant.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, permitted_tenant: str) -> None:
|
||||||
|
self._permitted = permitted_tenant
|
||||||
|
|
||||||
|
def authorize(self, *, action: str, tenant_id: str, actor: str) -> None:
|
||||||
|
if action == "tenant.create":
|
||||||
|
return
|
||||||
|
if tenant_id != self._permitted:
|
||||||
|
raise WriteAuthorizationDeniedError(action, "not permitted for this tenant")
|
||||||
|
|
||||||
|
|
||||||
class _BrokenStore(InMemoryTenantStore):
|
class _BrokenStore(InMemoryTenantStore):
|
||||||
def get_tenant(self, tenant_id: str):
|
def get_tenant(self, tenant_id: str):
|
||||||
raise StoreUnavailableError("connection to /var/lib/tenant-engine/tenant.db refused")
|
raise StoreUnavailableError("connection to /var/lib/tenant-engine/tenant.db refused")
|
||||||
|
|
||||||
|
|
||||||
|
class _BrokenWriteStore(InMemoryTenantStore):
|
||||||
|
def set_guardrail_override(self, **kwargs):
|
||||||
|
raise StoreUnavailableError("connection to /var/lib/tenant-engine/tenant.db refused")
|
||||||
|
|
||||||
|
|
||||||
def make_client(authorizer=None, store=None) -> TestClient:
|
def make_client(authorizer=None, store=None) -> TestClient:
|
||||||
app = create_app(
|
app = create_app(
|
||||||
store=store or InMemoryTenantStore(), authorizer=authorizer or _AllowAllAuthorizer()
|
store=store or InMemoryTenantStore(), authorizer=authorizer or _AllowAllAuthorizer()
|
||||||
|
|
@ -276,6 +299,43 @@ def test_tightening_a_retired_tenants_guardrail_is_allowed(client):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
# --- Cross-tenant denial and write-path outage ----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_caller_cannot_reach_across_tenants():
|
||||||
|
client = make_client(_TenantScopedAuthorizer("t-own"))
|
||||||
|
client.post(
|
||||||
|
"/tenants",
|
||||||
|
json={"tenant_id": "t-own", "identifier": "tenant:small:own", "actor": "ops"},
|
||||||
|
)
|
||||||
|
assert client.get("/tenants/t-own/guardrails", params={"actor": "ops"}).status_code == 200
|
||||||
|
assert read(client, actor="ops").status_code == 403
|
||||||
|
assert put(client).status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_cross_tenant_write_changes_nothing():
|
||||||
|
client = make_client(_TenantScopedAuthorizer("t-own"))
|
||||||
|
assert put(client).status_code == 403
|
||||||
|
# the store was never touched: the version is untouched
|
||||||
|
assert client.get("/tenants/t-1").json()["version"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_store_outage_fails_closed_on_write():
|
||||||
|
client = make_client(store=_BrokenWriteStore())
|
||||||
|
response = put(client)
|
||||||
|
assert response.status_code == 503
|
||||||
|
assert response.json()["error_code"] == "tenant_authority_unavailable"
|
||||||
|
assert "tenant.db" not in response.text
|
||||||
|
assert response.json()["correlation_id"] == "corr-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_errors_never_reflect_policy_internals():
|
||||||
|
client = make_client(_ScopedAuthorizer("tenant.create"))
|
||||||
|
body = put(client).json()
|
||||||
|
assert "tenant.db" not in str(body)
|
||||||
|
assert body["error_code"] == "write_denied"
|
||||||
|
|
||||||
|
|
||||||
# --- Compatibility --------------------------------------------------------
|
# --- Compatibility --------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ Decisions:
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: TEN-WP-0006-T05
|
id: TEN-WP-0006-T05
|
||||||
status: todo
|
status: progress
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "92036fa3-9031-4b42-a67e-93196e236e08"
|
state_hub_task_id: "92036fa3-9031-4b42-a67e-93196e236e08"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue