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
222
tests/test_pep_write_path.py
Normal file
222
tests/test_pep_write_path.py
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
"""TEN-WP-0011-T02: decision records, stance, no verdict cache."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from tenant_engine.app import create_app
|
||||
from tenant_engine.authz import FlexAuthWriteAuthorizer
|
||||
from tenant_engine.config import Settings
|
||||
from tenant_engine.domain import Tenant
|
||||
from tenant_engine.flex_auth import FlexAuthCheckClient
|
||||
from tenant_engine.stance import FAIL_CLOSED
|
||||
from tenant_engine.store import InMemoryTenantStore
|
||||
|
||||
|
||||
def _allowing_client(handler) -> FlexAuthCheckClient:
|
||||
return FlexAuthCheckClient(
|
||||
base_url="https://flex-auth.example.test", transport=httpx.MockTransport(handler)
|
||||
)
|
||||
|
||||
|
||||
def test_granted_role_persists_decision_id_on_the_event():
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "decision:grant-1",
|
||||
"effect": "allow",
|
||||
"resource": {},
|
||||
"subject": {},
|
||||
"provenance": {},
|
||||
},
|
||||
)
|
||||
|
||||
store = InMemoryTenantStore()
|
||||
app = create_app(
|
||||
store=store,
|
||||
authorizer=FlexAuthWriteAuthorizer(client=_allowing_client(handler)),
|
||||
)
|
||||
client = TestClient(app)
|
||||
assert (
|
||||
client.post(
|
||||
"/tenants",
|
||||
json={
|
||||
"tenant_id": "t-1",
|
||||
"identifier": "tenant:friendly:binky",
|
||||
"actor": "tenant-engine",
|
||||
},
|
||||
).status_code
|
||||
== 201
|
||||
)
|
||||
granted = client.post(
|
||||
"/tenants/t-1/roles/grant",
|
||||
json={
|
||||
"grant_id": "g-1",
|
||||
"role": "CUS",
|
||||
"grant_reason": "manual_grant",
|
||||
"granted_by": "ops",
|
||||
"correlation_id": "c-1",
|
||||
"actor": "tenant-engine",
|
||||
},
|
||||
)
|
||||
assert granted.status_code == 201
|
||||
event = [e for e in store.events_for("t-1") if e.event_type == "role_granted"][-1]
|
||||
assert event.payload["authorization_decision_id"] == "decision:grant-1"
|
||||
assert event.payload["authorization_source"] == "decision"
|
||||
records = store.authorization_records("t-1")
|
||||
assert any(r.decision_id == "decision:grant-1" and r.allowed for r in records)
|
||||
|
||||
|
||||
def test_denied_grant_leaves_a_reconstructable_record():
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "decision:deny-1",
|
||||
"effect": "deny",
|
||||
"resource": {},
|
||||
"subject": {},
|
||||
"provenance": {},
|
||||
},
|
||||
)
|
||||
|
||||
store = InMemoryTenantStore()
|
||||
store.create_tenant(Tenant.create(tenant_id="t-1", identifier="tenant:friendly:binky"))
|
||||
app = create_app(
|
||||
store=store,
|
||||
authorizer=FlexAuthWriteAuthorizer(client=_allowing_client(handler)),
|
||||
)
|
||||
response = TestClient(app).post(
|
||||
"/tenants/t-1/roles/grant",
|
||||
json={
|
||||
"grant_id": "g-1",
|
||||
"role": "CUS",
|
||||
"grant_reason": "manual_grant",
|
||||
"granted_by": "ops",
|
||||
"correlation_id": "c-1",
|
||||
"actor": "tenant-engine",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
records = store.authorization_records("t-1")
|
||||
assert records
|
||||
assert records[-1].allowed is False
|
||||
assert records[-1].decision_id == "decision:deny-1"
|
||||
assert records[-1].source == "decision"
|
||||
assert store.events_for("t-1") # tenant_created only; no role_granted
|
||||
assert not any(e.event_type == "role_granted" for e in store.events_for("t-1"))
|
||||
|
||||
|
||||
def test_unreachable_engine_records_fail_closed_stance():
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
raise httpx.ConnectError("connection refused", request=request)
|
||||
|
||||
store = InMemoryTenantStore()
|
||||
app = create_app(
|
||||
store=store,
|
||||
authorizer=FlexAuthWriteAuthorizer(client=_allowing_client(handler)),
|
||||
settings=Settings(
|
||||
flex_auth_base_url="https://flex-auth.example.test",
|
||||
flex_auth_timeout_seconds=1,
|
||||
host="127.0.0.1",
|
||||
port=8090,
|
||||
),
|
||||
)
|
||||
response = TestClient(app).post(
|
||||
"/tenants",
|
||||
json={"tenant_id": "t-1", "identifier": "tenant:friendly:binky", "actor": "tenant-engine"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
records = store.authorization_records("t-1")
|
||||
assert records[-1].allowed is False
|
||||
assert records[-1].source == "stance"
|
||||
assert records[-1].stance == FAIL_CLOSED
|
||||
|
||||
|
||||
def test_unset_authorizer_records_fail_closed_stance():
|
||||
store = InMemoryTenantStore()
|
||||
app = create_app(store=store) # DefaultDeny
|
||||
response = TestClient(app).post(
|
||||
"/tenants",
|
||||
json={"tenant_id": "t-1", "identifier": "tenant:friendly:binky", "actor": "ops"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
records = store.authorization_records("t-1")
|
||||
assert records[-1].source == "stance"
|
||||
assert records[-1].stance == FAIL_CLOSED
|
||||
|
||||
|
||||
def test_verdict_is_not_cached_across_requests():
|
||||
calls = {"n": 0}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
calls["n"] += 1
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": f"d-{calls['n']}",
|
||||
"effect": "allow",
|
||||
"resource": {},
|
||||
"subject": {},
|
||||
"provenance": {},
|
||||
},
|
||||
)
|
||||
|
||||
authorizer = FlexAuthWriteAuthorizer(client=_allowing_client(handler))
|
||||
authorizer.authorize(action="tenant.create", tenant_id="t-1", actor="tenant-engine")
|
||||
authorizer.authorize(action="tenant.create", tenant_id="t-1", actor="tenant-engine")
|
||||
assert calls["n"] == 2
|
||||
|
||||
|
||||
def test_a_previous_allow_cannot_authorize_a_different_request():
|
||||
seen: list[str] = []
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
import json
|
||||
|
||||
body = json.loads(request.content)
|
||||
seen.append(body["action"])
|
||||
effect = "allow" if body["action"] == "tenant.create" else "deny"
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": f"d-{body['action']}",
|
||||
"effect": effect,
|
||||
"resource": {},
|
||||
"subject": {},
|
||||
"provenance": {},
|
||||
},
|
||||
)
|
||||
|
||||
store = InMemoryTenantStore()
|
||||
app = create_app(
|
||||
store=store,
|
||||
authorizer=FlexAuthWriteAuthorizer(client=_allowing_client(handler)),
|
||||
)
|
||||
client = TestClient(app)
|
||||
assert (
|
||||
client.post(
|
||||
"/tenants",
|
||||
json={
|
||||
"tenant_id": "t-1",
|
||||
"identifier": "tenant:friendly:binky",
|
||||
"actor": "tenant-engine",
|
||||
},
|
||||
).status_code
|
||||
== 201
|
||||
)
|
||||
denied = client.post(
|
||||
"/tenants/t-1/roles/grant",
|
||||
json={
|
||||
"grant_id": "g-1",
|
||||
"role": "CUS",
|
||||
"grant_reason": "manual_grant",
|
||||
"granted_by": "ops",
|
||||
"correlation_id": "c-1",
|
||||
"actor": "tenant-engine",
|
||||
},
|
||||
)
|
||||
assert denied.status_code == 403
|
||||
assert seen == ["tenant.create", "tenant.role.grant"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue