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
|
|
@ -9,9 +9,9 @@ from datetime import UTC, datetime
|
|||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from helpers import AllowAllAuthorizer, ScopedAuthorizer
|
||||
|
||||
from tenant_engine.app import create_app
|
||||
from tenant_engine.authz import WriteAuthorizationDeniedError, WriteAuthorizer
|
||||
from tenant_engine.domain import (
|
||||
CapabilityRole,
|
||||
EmptyUpdateError,
|
||||
|
|
@ -148,22 +148,8 @@ def test_a_new_platform_default_grant_is_refused_after_moving_off_trial():
|
|||
# --- API ----------------------------------------------------------------
|
||||
|
||||
|
||||
class _AllowAll(WriteAuthorizer):
|
||||
def authorize(self, *, action: str, tenant_id: str, actor: str) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class _Scoped(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")
|
||||
|
||||
|
||||
def make_client(authorizer=None, identifier="tenant:small:acme") -> TestClient:
|
||||
app = create_app(store=InMemoryTenantStore(), authorizer=authorizer or _AllowAll())
|
||||
app = create_app(store=InMemoryTenantStore(), authorizer=authorizer or AllowAllAuthorizer())
|
||||
client = TestClient(app)
|
||||
client.post(
|
||||
"/tenants",
|
||||
|
|
@ -191,35 +177,47 @@ def test_the_route_reclassifies_and_bumps_the_version():
|
|||
|
||||
def test_the_new_ceiling_is_visible_through_the_guardrail_read():
|
||||
client = make_client(identifier="tenant:trial:acme")
|
||||
assert client.get("/tenants/t-1/guardrails", params={"actor": "flex-auth"}).json()[
|
||||
"limits"
|
||||
][KEY]["amount"] == 0
|
||||
assert (
|
||||
client.get("/tenants/t-1/guardrails", params={"actor": "flex-auth"}).json()["limits"][KEY][
|
||||
"amount"
|
||||
]
|
||||
== 0
|
||||
)
|
||||
post_grouping(client, "medium")
|
||||
assert client.get("/tenants/t-1/guardrails", params={"actor": "flex-auth"}).json()[
|
||||
"limits"
|
||||
][KEY]["amount"] == 100_000
|
||||
assert (
|
||||
client.get("/tenants/t-1/guardrails", params={"actor": "flex-auth"}).json()["limits"][KEY][
|
||||
"amount"
|
||||
]
|
||||
== 100_000
|
||||
)
|
||||
|
||||
|
||||
def test_reclassification_is_authorized_separately_from_a_rename():
|
||||
# policy can permit a display-name edit without permitting a move that
|
||||
# changes the spend ceiling
|
||||
client = make_client(_Scoped("tenant.create", "tenant.update"))
|
||||
client = make_client(ScopedAuthorizer("tenant.create", "tenant.update"))
|
||||
assert post_grouping(client).status_code == 403
|
||||
assert client.patch(
|
||||
"/tenants/t-1",
|
||||
json={"metadata": {"display_name": "Acme"}, **BODY},
|
||||
headers=HEADERS,
|
||||
).status_code == 200
|
||||
assert (
|
||||
client.patch(
|
||||
"/tenants/t-1",
|
||||
json={"metadata": {"display_name": "Acme"}, **BODY},
|
||||
headers=HEADERS,
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
|
||||
|
||||
def test_renaming_permission_is_not_conferred_by_reclassification_permission():
|
||||
client = make_client(_Scoped("tenant.create", "tenant.grouping.set"))
|
||||
client = make_client(ScopedAuthorizer("tenant.create", "tenant.grouping.set"))
|
||||
assert post_grouping(client).status_code == 200
|
||||
assert client.patch(
|
||||
"/tenants/t-1",
|
||||
json={"metadata": {"display_name": "Acme"}, **BODY},
|
||||
headers={"Idempotency-Key": "idem-2", "If-Match": '"2"'},
|
||||
).status_code == 403
|
||||
assert (
|
||||
client.patch(
|
||||
"/tenants/t-1",
|
||||
json={"metadata": {"display_name": "Acme"}, **BODY},
|
||||
headers={"Idempotency-Key": "idem-2", "If-Match": '"2"'},
|
||||
).status_code
|
||||
== 403
|
||||
)
|
||||
|
||||
|
||||
def test_an_unknown_grouping_is_a_distinct_error_code():
|
||||
|
|
@ -273,7 +271,7 @@ def test_the_change_is_auditable_as_its_own_event():
|
|||
client = make_client()
|
||||
post_grouping(client)
|
||||
store = client.app.state.store
|
||||
events = [e for e in store.events() if e.event_type == "tenant_grouping_changed"]
|
||||
events = [e for e in store.events_for("t-1") if e.event_type == "tenant_grouping_changed"]
|
||||
assert len(events) == 1
|
||||
assert events[0].payload["actor"] == "ops"
|
||||
assert events[0].payload["reason"] == "grew past the band"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue