Implement TEN-WP-0011 security layer conformance
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:
tegwick 2026-08-29 13:02:51 +02:00
parent 80961af91e
commit 672cf4da6e
40 changed files with 2285 additions and 361 deletions

View file

@ -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"