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

@ -19,6 +19,7 @@ except ImportError: # pragma: no cover - exercised by the deployment guard
ConnectionPool = None
PoolTimeout = None
from tenant_engine.audit_core import envelope_for, new_event_id
from tenant_engine.domain import (
CapabilityRole,
PlanAssignment,
@ -35,9 +36,11 @@ from tenant_engine.guardrail import (
load_limit,
)
from tenant_engine.store import (
AuthorizationRecord,
DomainEvent,
GrantNotFoundError,
IdempotencyConflictError,
OutboxRow,
StoreUnavailableError,
TenantAlreadyExistsError,
TenantNotFoundError,
@ -118,7 +121,7 @@ class PostgresTenantStore:
with self._pool.connection() as conn:
conn.execute("SELECT 1").fetchone()
def create_tenant(self, tenant: Tenant) -> None:
def create_tenant(self, tenant: Tenant, *, authz: dict[str, Any] | None = None) -> None:
try:
with self._pool.connection() as conn, conn.transaction():
conn.execute(
@ -144,7 +147,7 @@ class PostgresTenantStore:
conn,
"tenant_created",
tenant.tenant_id,
{"identifier": tenant.identifier, "grouping": tenant.grouping},
{"identifier": tenant.identifier, "grouping": tenant.grouping, **(authz or {})},
)
except StoreUnavailableError as exc:
if isinstance(exc.__cause__, UniqueViolation):
@ -204,15 +207,13 @@ class PostgresTenantStore:
updated.tenant_id,
),
)
self._record_receipt(
conn, updated, idempotency_key, request_fingerprint
)
self._record_receipt(conn, updated, idempotency_key, request_fingerprint)
self._emit(
conn, event_type, updated.tenant_id, {**evidence, "version": updated.version}
)
return updated, False
def grant_role(self, grant: RoleGrant) -> None:
def grant_role(self, grant: RoleGrant, *, authz: dict[str, Any] | None = None) -> None:
tenant = self.get_tenant(grant.tenant_id)
self._require_active(tenant, "grant a role")
with self._pool.connection() as conn, conn.transaction():
@ -248,10 +249,13 @@ class PostgresTenantStore:
"role": grant.role.value,
"grant_reason": grant.grant_reason,
"correlation_id": grant.correlation_id,
**(authz or {}),
},
)
def revoke_role(self, *, tenant_id: str, grant_id: str, at: datetime) -> RoleGrant:
def revoke_role(
self, *, tenant_id: str, grant_id: str, at: datetime, authz: dict[str, Any] | None = None
) -> RoleGrant:
tenant = self.get_tenant(tenant_id)
with self._pool.connection() as conn, conn.transaction():
row = conn.execute(
@ -266,7 +270,7 @@ class PostgresTenantStore:
conn,
"role_revoked",
tenant.tenant_id,
{"grant_id": grant_id, "role": grant.role.value},
{"grant_id": grant_id, "role": grant.role.value, **(authz or {})},
)
return grant
@ -279,7 +283,9 @@ class PostgresTenantStore:
).fetchall()
return frozenset(CapabilityRole(row["role"]) for row in rows)
def assign_plan(self, assignment: PlanAssignment) -> None:
def assign_plan(
self, assignment: PlanAssignment, *, authz: dict[str, Any] | None = None
) -> None:
tenant = self.get_tenant(assignment.tenant_id)
self._require_active(tenant, "assign a plan")
with self._pool.connection() as conn, conn.transaction():
@ -290,17 +296,89 @@ class PostgresTenantStore:
(tenant.tenant_id, assignment.plan_id, assignment.assigned_at),
)
self._emit(
conn, "plan_assigned", tenant.tenant_id, {"plan_id": assignment.plan_id}
conn,
"plan_assigned",
tenant.tenant_id,
{"plan_id": assignment.plan_id, **(authz or {})},
)
def events(self) -> list[DomainEvent]:
def events_for(self, tenant_id: str) -> list[DomainEvent]:
tenant = self.get_tenant(tenant_id)
with self._pool.connection() as conn:
rows = conn.execute("SELECT * FROM events ORDER BY seq").fetchall()
rows = conn.execute(
"SELECT * FROM events WHERE tenant_id = %s ORDER BY seq",
(tenant.tenant_id,),
).fetchall()
return [
DomainEvent(row["event_type"], row["tenant_id"], row["at"], row["payload"])
for row in rows
]
def record_authorization(self, record: AuthorizationRecord) -> None:
with self._pool.connection() as conn, conn.transaction():
conn.execute(
"""INSERT INTO authz_records
(action, tenant_id, actor, allowed, source, reason, at,
decision_id, request_digest, effect, stance)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
(
record.action,
record.tenant_id,
record.actor,
record.allowed,
record.source,
record.reason,
record.at,
record.decision_id,
record.request_digest,
record.effect,
record.stance,
),
)
def authorization_records(self, tenant_id: str) -> list[AuthorizationRecord]:
keys = [tenant_id]
try:
keys.append(self.get_tenant(tenant_id).tenant_id)
except TenantNotFoundError:
pass
placeholders = ",".join(["%s"] * len(keys))
with self._pool.connection() as conn:
rows = conn.execute(
f"SELECT * FROM authz_records WHERE tenant_id IN ({placeholders}) ORDER BY seq",
keys,
).fetchall()
return [_authz_row(row) for row in rows]
def pending_outbox(self) -> list[OutboxRow]:
with self._pool.connection() as conn:
rows = conn.execute(
"""SELECT * FROM audit_outbox
WHERE delivered_at IS NULL AND dead_at IS NULL
ORDER BY created_at"""
).fetchall()
return [_outbox_row(row) for row in rows]
def mark_outbox(self, event_id: str, *, status: str, detail: str = "") -> None:
with self._pool.connection() as conn, conn.transaction():
if status in {"delivered", "duplicate"}:
conn.execute(
"""UPDATE audit_outbox SET delivered_at = %s, last_error = NULL
WHERE event_id = %s""",
(datetime.now(UTC), event_id),
)
elif status == "dead":
conn.execute(
"UPDATE audit_outbox SET dead_at = %s, last_error = %s WHERE event_id = %s",
(datetime.now(UTC), detail, event_id),
)
else:
conn.execute(
"""UPDATE audit_outbox SET attempts = attempts + 1, last_error = %s
WHERE event_id = %s""",
(detail, event_id),
)
def guardrail_overrides(self, tenant_id: str) -> dict[str, LimitValue]:
tenant = self.get_tenant(tenant_id)
with self._pool.connection() as conn:
@ -333,6 +411,7 @@ class PostgresTenantStore:
idempotency_key: str,
request_fingerprint: str,
at: datetime,
authz: dict[str, Any] | None = None,
) -> tuple[Tenant, GuardrailChange | None, bool]:
DEFAULT_REGISTRY.get(limit_key)
with self._pool.connection() as conn, conn.transaction():
@ -428,6 +507,7 @@ class PostgresTenantStore:
"cleared": value is None,
"correlation_id": correlation_id,
"version": updated.version,
**(authz or {}),
},
)
return updated, change, False
@ -467,9 +547,24 @@ class PostgresTenantStore:
@staticmethod
def _emit(conn: Any, event_type: str, tenant_id: str, payload: dict[str, Any]) -> None:
event_id = str(payload.get("event_id") or new_event_id())
payload = {**payload, "event_id": event_id}
at = datetime.now(UTC)
conn.execute(
"INSERT INTO events (event_type, tenant_id, at, payload) VALUES (%s, %s, %s, %s)",
(event_type, tenant_id, datetime.now(UTC), Jsonb(payload)),
(event_type, tenant_id, at, Jsonb(payload)),
)
envelope = envelope_for(
event_id=event_id,
event_type=event_type,
tenant_id=tenant_id,
observed_at=at.isoformat(),
payload=payload,
)
conn.execute(
"""INSERT INTO audit_outbox (event_id, tenant_id, envelope, created_at)
VALUES (%s, %s, %s, %s)""",
(event_id, tenant_id, Jsonb(envelope), at),
)
@ -501,9 +596,7 @@ def _row(tenant: Tenant) -> dict[str, Any]:
"created_at": tenant.created_at.isoformat() if tenant.created_at else None,
"updated_at": tenant.updated_at.isoformat() if tenant.updated_at else None,
"retired_at": tenant.retired_at.isoformat() if tenant.retired_at else None,
"reactivated_at": (
tenant.reactivated_at.isoformat() if tenant.reactivated_at else None
),
"reactivated_at": (tenant.reactivated_at.isoformat() if tenant.reactivated_at else None),
}
@ -539,3 +632,32 @@ def _change(row: Mapping[str, Any]) -> GuardrailChange:
correlation_id=row["correlation_id"],
changed_at=_dt(row["changed_at"]),
)
def _authz_row(row: Mapping[str, Any]) -> AuthorizationRecord:
return AuthorizationRecord(
action=row["action"],
tenant_id=row["tenant_id"],
actor=row["actor"],
allowed=bool(row["allowed"]),
source=row["source"],
reason=row["reason"],
at=_dt(row["at"]),
decision_id=row["decision_id"],
request_digest=row["request_digest"],
effect=row["effect"],
stance=row["stance"],
)
def _outbox_row(row: Mapping[str, Any]) -> OutboxRow:
return OutboxRow(
event_id=row["event_id"],
tenant_id=row["tenant_id"],
envelope=row["envelope"],
created_at=_dt(row["created_at"]),
attempts=row["attempts"],
last_error=row["last_error"],
delivered_at=_dt(row["delivered_at"]) if row["delivered_at"] else None,
dead_at=_dt(row["dead_at"]) if row["dead_at"] else None,
)