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
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue