Implement TEN-WP-0011 security layer conformance
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
Engine/PIP declaration is now checkable (layer.yaml plus a Tooling-client scan). Writes persist a decision record or the published fail-closed stance, live-lookup freshness is published, events_for is tenant-scoped, and mutation evidence drains to audit-core from a local outbox without blocking the mutation. Sender registration is requested as AUDIT-IN-0002. Boundary-contract amendment is requested as NET-IN-0002. Assistant: grok Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
This commit is contained in:
parent
80961af91e
commit
672cf4da6e
40 changed files with 2285 additions and 361 deletions
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from helpers import AllowAllAuthorizer, ScopedAuthorizer, deny
|
||||
|
||||
from tenant_engine.app import create_app
|
||||
from tenant_engine.authz import WriteAuthorizationDeniedError, WriteAuthorizer
|
||||
from tenant_engine.authz import AuthorizationOutcome
|
||||
from tenant_engine.store import InMemoryTenantStore, StoreUnavailableError
|
||||
|
||||
KEY = "spend.monthly"
|
||||
|
|
@ -13,36 +14,18 @@ LIMIT = {"kind": "spend", "amount": "9000", "currency": "EUR", "period": "P1M"}
|
|||
BODY = {"actor": "ops", "reason": "raised for pilot", "correlation_id": "corr-1"}
|
||||
|
||||
|
||||
class _AllowAllAuthorizer(WriteAuthorizer):
|
||||
def authorize(self, *, action: str, tenant_id: str, actor: str) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class _ScopedAuthorizer(WriteAuthorizer):
|
||||
def __init__(self, *allowed: str) -> None:
|
||||
self._allowed = set(allowed)
|
||||
|
||||
def authorize(self, *, action: str, tenant_id: str, actor: str) -> None:
|
||||
if action not in self._allowed:
|
||||
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.
|
||||
"""
|
||||
class _TenantScopedAuthorizer:
|
||||
"""Permits guardrail work on exactly one tenant."""
|
||||
|
||||
def __init__(self, permitted_tenant: str) -> None:
|
||||
self._permitted = permitted_tenant
|
||||
|
||||
def authorize(self, *, action: str, tenant_id: str, actor: str) -> None:
|
||||
def authorize(self, *, action: str, tenant_id: str, actor: str) -> AuthorizationOutcome:
|
||||
if action == "tenant.create":
|
||||
return
|
||||
return AllowAllAuthorizer().authorize(action=action, tenant_id=tenant_id, actor=actor)
|
||||
if tenant_id != self._permitted:
|
||||
raise WriteAuthorizationDeniedError(action, "not permitted for this tenant")
|
||||
raise deny(action, tenant_id, actor, "not permitted for this tenant")
|
||||
return AllowAllAuthorizer().authorize(action=action, tenant_id=tenant_id, actor=actor)
|
||||
|
||||
|
||||
class _BrokenStore(InMemoryTenantStore):
|
||||
|
|
@ -57,7 +40,7 @@ class _BrokenWriteStore(InMemoryTenantStore):
|
|||
|
||||
def make_client(authorizer=None, store=None) -> TestClient:
|
||||
app = create_app(
|
||||
store=store or InMemoryTenantStore(), authorizer=authorizer or _AllowAllAuthorizer()
|
||||
store=store or InMemoryTenantStore(), authorizer=authorizer or AllowAllAuthorizer()
|
||||
)
|
||||
client = TestClient(app)
|
||||
client.post(
|
||||
|
|
@ -107,19 +90,19 @@ def test_a_trial_tenant_reads_a_zero_spend_ceiling(client):
|
|||
|
||||
def test_read_is_authorized_separately_from_write():
|
||||
# a PDP gets the read and nothing else
|
||||
client = make_client(_ScopedAuthorizer("tenant.create", "tenant.guardrail.read"))
|
||||
client = make_client(ScopedAuthorizer("tenant.create", "tenant.guardrail.read"))
|
||||
assert read(client).status_code == 200
|
||||
assert put(client).status_code == 403
|
||||
|
||||
|
||||
def test_write_permission_does_not_confer_read_permission():
|
||||
client = make_client(_ScopedAuthorizer("tenant.create", "tenant.guardrail.set"))
|
||||
client = make_client(ScopedAuthorizer("tenant.create", "tenant.guardrail.set"))
|
||||
assert read(client).status_code == 403
|
||||
assert put(client).status_code == 200
|
||||
|
||||
|
||||
def test_an_unauthorized_read_cannot_probe_tenant_existence():
|
||||
client = make_client(_ScopedAuthorizer("tenant.create"))
|
||||
client = make_client(ScopedAuthorizer("tenant.create"))
|
||||
known = client.get("/tenants/t-1/guardrails", params={"actor": "nobody"})
|
||||
unknown = client.get("/tenants/t-404/guardrails", params={"actor": "nobody"})
|
||||
assert known.status_code == unknown.status_code == 403
|
||||
|
|
@ -330,7 +313,7 @@ def test_a_store_outage_fails_closed_on_write():
|
|||
|
||||
|
||||
def test_errors_never_reflect_policy_internals():
|
||||
client = make_client(_ScopedAuthorizer("tenant.create"))
|
||||
client = make_client(ScopedAuthorizer("tenant.create"))
|
||||
body = put(client).json()
|
||||
assert "tenant.db" not in str(body)
|
||||
assert body["error_code"] == "write_denied"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue