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
663 lines
25 KiB
Python
663 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Iterator, Mapping
|
|
from contextlib import contextmanager
|
|
from dataclasses import replace
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from threading import RLock
|
|
from typing import Any
|
|
|
|
try: # Optional runtime dependency; SQLite/development installs stay small.
|
|
import psycopg
|
|
from psycopg.errors import UniqueViolation
|
|
from psycopg.rows import dict_row
|
|
from psycopg.types.json import Jsonb
|
|
from psycopg_pool import ConnectionPool, PoolTimeout
|
|
except ImportError: # pragma: no cover - exercised by the deployment guard
|
|
psycopg = None
|
|
ConnectionPool = None
|
|
PoolTimeout = None
|
|
|
|
from tenant_engine.audit_core import envelope_for, new_event_id
|
|
from tenant_engine.domain import (
|
|
CapabilityRole,
|
|
PlanAssignment,
|
|
RoleGrant,
|
|
Tenant,
|
|
TenantLifecycle,
|
|
TenantRetiredError,
|
|
)
|
|
from tenant_engine.guardrail import (
|
|
DEFAULT_REGISTRY,
|
|
GuardrailChange,
|
|
LimitValue,
|
|
dump_limit,
|
|
load_limit,
|
|
)
|
|
from tenant_engine.store import (
|
|
AuthorizationRecord,
|
|
DomainEvent,
|
|
GrantNotFoundError,
|
|
IdempotencyConflictError,
|
|
OutboxRow,
|
|
StoreUnavailableError,
|
|
TenantAlreadyExistsError,
|
|
TenantNotFoundError,
|
|
VersionConflictError,
|
|
guard_guardrail_write,
|
|
)
|
|
|
|
|
|
class RotatingConnectionPool:
|
|
"""A bounded pool whose DSN is read from a projected file per checkout.
|
|
|
|
OpenBao can replace the file atomically when a database lease rotates.
|
|
The next checkout swaps pools, while max_lifetime also bounds how long a
|
|
still-valid connection can retain an old credential.
|
|
"""
|
|
|
|
def __init__(self, dsn_file: str, *, min_size: int = 1, max_size: int = 4) -> None:
|
|
if ConnectionPool is None:
|
|
raise RuntimeError("install tenant-engine[postgres] for PostgreSQL support")
|
|
self._dsn_file = Path(dsn_file)
|
|
self._min_size = min_size
|
|
self._max_size = max_size
|
|
self._lock = RLock()
|
|
self._dsn = ""
|
|
self._pool = None
|
|
|
|
@contextmanager
|
|
def connection(self) -> Iterator[Any]:
|
|
try:
|
|
dsn = self._dsn_file.read_text(encoding="utf-8").strip()
|
|
if not dsn:
|
|
raise OSError("database URL file is empty")
|
|
old_pool = None
|
|
with self._lock:
|
|
if self._pool is None or dsn != self._dsn:
|
|
old_pool = self._pool
|
|
self._pool = ConnectionPool(
|
|
conninfo=dsn,
|
|
min_size=self._min_size,
|
|
max_size=self._max_size,
|
|
timeout=5,
|
|
max_lifetime=300,
|
|
kwargs={"row_factory": dict_row},
|
|
open=True,
|
|
)
|
|
self._dsn = dsn
|
|
pool = self._pool
|
|
if old_pool is not None:
|
|
old_pool.close()
|
|
with pool.connection() as connection:
|
|
yield connection
|
|
except OSError as exc:
|
|
raise StoreUnavailableError("database credential unavailable") from exc
|
|
except (psycopg.Error, PoolTimeout) as exc:
|
|
raise StoreUnavailableError("PostgreSQL unavailable") from exc
|
|
|
|
def close(self) -> None:
|
|
with self._lock:
|
|
if self._pool is not None:
|
|
self._pool.close()
|
|
self._pool = None
|
|
|
|
|
|
class PostgresTenantStore:
|
|
"""PostgreSQL implementation of the complete TenantStore protocol."""
|
|
|
|
backend_name = "postgresql"
|
|
|
|
def __init__(self, dsn_file: str, *, min_pool_size: int = 1, max_pool_size: int = 4) -> None:
|
|
self._pool = RotatingConnectionPool(
|
|
dsn_file, min_size=min_pool_size, max_size=max_pool_size
|
|
)
|
|
|
|
def close(self) -> None:
|
|
self._pool.close()
|
|
|
|
def ping(self) -> None:
|
|
with self._pool.connection() as conn:
|
|
conn.execute("SELECT 1").fetchone()
|
|
|
|
def create_tenant(self, tenant: Tenant, *, authz: dict[str, Any] | None = None) -> None:
|
|
try:
|
|
with self._pool.connection() as conn, conn.transaction():
|
|
conn.execute(
|
|
"""INSERT INTO tenants
|
|
(tenant_id, identifier, grouping_name, display_name, contact_email,
|
|
lifecycle, version, created_at, updated_at, retired_at, reactivated_at)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
|
|
(
|
|
tenant.tenant_id,
|
|
tenant.identifier,
|
|
tenant.grouping,
|
|
tenant.display_name,
|
|
tenant.contact_email,
|
|
tenant.lifecycle.value,
|
|
tenant.version,
|
|
tenant.created_at,
|
|
tenant.updated_at,
|
|
tenant.retired_at,
|
|
tenant.reactivated_at,
|
|
),
|
|
)
|
|
self._emit(
|
|
conn,
|
|
"tenant_created",
|
|
tenant.tenant_id,
|
|
{"identifier": tenant.identifier, "grouping": tenant.grouping, **(authz or {})},
|
|
)
|
|
except StoreUnavailableError as exc:
|
|
if isinstance(exc.__cause__, UniqueViolation):
|
|
raise TenantAlreadyExistsError(tenant.identifier) from exc
|
|
raise
|
|
|
|
def get_tenant(self, tenant_id: str) -> Tenant:
|
|
with self._pool.connection() as conn:
|
|
row = conn.execute(
|
|
"SELECT * FROM tenants WHERE tenant_id = %s OR identifier = %s",
|
|
(tenant_id, tenant_id),
|
|
).fetchone()
|
|
if row is None:
|
|
raise TenantNotFoundError(tenant_id)
|
|
return _tenant(row)
|
|
|
|
def mutate_tenant(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
expected_version: int,
|
|
mutate: Callable[[Tenant], Tenant],
|
|
event_type: str,
|
|
evidence: dict[str, Any],
|
|
idempotency_key: str,
|
|
request_fingerprint: str,
|
|
) -> tuple[Tenant, bool]:
|
|
with self._pool.connection() as conn, conn.transaction():
|
|
row = self._locked_tenant(conn, tenant_id)
|
|
receipt = conn.execute(
|
|
"""SELECT request_fingerprint, result FROM idempotency_receipts
|
|
WHERE tenant_id = %s AND idempotency_key = %s""",
|
|
(row["tenant_id"], idempotency_key),
|
|
).fetchone()
|
|
if receipt is not None:
|
|
if receipt["request_fingerprint"] != request_fingerprint:
|
|
raise IdempotencyConflictError(idempotency_key)
|
|
return _tenant(receipt["result"]), True
|
|
|
|
current = _tenant(row)
|
|
if current.version != expected_version:
|
|
raise VersionConflictError(expected=expected_version, actual=current.version)
|
|
updated = mutate(current)
|
|
conn.execute(
|
|
"""UPDATE tenants SET grouping_name = %s, display_name = %s,
|
|
contact_email = %s, lifecycle = %s, version = %s, updated_at = %s,
|
|
retired_at = %s, reactivated_at = %s WHERE tenant_id = %s""",
|
|
(
|
|
updated.grouping,
|
|
updated.display_name,
|
|
updated.contact_email,
|
|
updated.lifecycle.value,
|
|
updated.version,
|
|
updated.updated_at,
|
|
updated.retired_at,
|
|
updated.reactivated_at,
|
|
updated.tenant_id,
|
|
),
|
|
)
|
|
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, *, 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():
|
|
conn.execute(
|
|
"""INSERT INTO grants
|
|
(grant_id, tenant_id, role, grant_reason, plan_id, granted_by,
|
|
granted_at, correlation_id, revoked_at)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT (grant_id) DO UPDATE SET
|
|
tenant_id = EXCLUDED.tenant_id, role = EXCLUDED.role,
|
|
grant_reason = EXCLUDED.grant_reason, plan_id = EXCLUDED.plan_id,
|
|
granted_by = EXCLUDED.granted_by, granted_at = EXCLUDED.granted_at,
|
|
correlation_id = EXCLUDED.correlation_id,
|
|
revoked_at = EXCLUDED.revoked_at""",
|
|
(
|
|
grant.grant_id,
|
|
tenant.tenant_id,
|
|
grant.role.value,
|
|
grant.grant_reason,
|
|
grant.plan_id,
|
|
grant.granted_by,
|
|
grant.granted_at,
|
|
grant.correlation_id,
|
|
grant.revoked_at,
|
|
),
|
|
)
|
|
self._emit(
|
|
conn,
|
|
"role_granted",
|
|
tenant.tenant_id,
|
|
{
|
|
"grant_id": grant.grant_id,
|
|
"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, authz: dict[str, Any] | None = None
|
|
) -> RoleGrant:
|
|
tenant = self.get_tenant(tenant_id)
|
|
with self._pool.connection() as conn, conn.transaction():
|
|
row = conn.execute(
|
|
"SELECT * FROM grants WHERE tenant_id = %s AND grant_id = %s FOR UPDATE",
|
|
(tenant.tenant_id, grant_id),
|
|
).fetchone()
|
|
if row is None:
|
|
raise GrantNotFoundError(grant_id)
|
|
grant = _grant(row).revoke(at=at)
|
|
conn.execute("UPDATE grants SET revoked_at = %s WHERE grant_id = %s", (at, grant_id))
|
|
self._emit(
|
|
conn,
|
|
"role_revoked",
|
|
tenant.tenant_id,
|
|
{"grant_id": grant_id, "role": grant.role.value, **(authz or {})},
|
|
)
|
|
return grant
|
|
|
|
def active_roles(self, tenant_id: str) -> frozenset[CapabilityRole]:
|
|
tenant = self.get_tenant(tenant_id)
|
|
with self._pool.connection() as conn:
|
|
rows = conn.execute(
|
|
"SELECT role FROM grants WHERE tenant_id = %s AND revoked_at IS NULL",
|
|
(tenant.tenant_id,),
|
|
).fetchall()
|
|
return frozenset(CapabilityRole(row["role"]) for row in rows)
|
|
|
|
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():
|
|
conn.execute(
|
|
"""INSERT INTO plans (tenant_id, plan_id, assigned_at) VALUES (%s, %s, %s)
|
|
ON CONFLICT (tenant_id) DO UPDATE SET
|
|
plan_id = EXCLUDED.plan_id, assigned_at = EXCLUDED.assigned_at""",
|
|
(tenant.tenant_id, assignment.plan_id, assignment.assigned_at),
|
|
)
|
|
self._emit(
|
|
conn,
|
|
"plan_assigned",
|
|
tenant.tenant_id,
|
|
{"plan_id": assignment.plan_id, **(authz or {})},
|
|
)
|
|
|
|
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 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:
|
|
rows = conn.execute(
|
|
"SELECT * FROM guardrail_overrides WHERE tenant_id = %s", (tenant.tenant_id,)
|
|
).fetchall()
|
|
return {row["limit_key"]: load_limit(row) for row in rows}
|
|
|
|
def guardrail_changes(self, tenant_id: str) -> list[GuardrailChange]:
|
|
tenant = self.get_tenant(tenant_id)
|
|
with self._pool.connection() as conn:
|
|
rows = conn.execute(
|
|
"""SELECT * FROM guardrail_changes WHERE tenant_id = %s
|
|
ORDER BY changed_at, change_id""",
|
|
(tenant.tenant_id,),
|
|
).fetchall()
|
|
return [_change(row) for row in rows]
|
|
|
|
def set_guardrail_override(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
expected_version: int,
|
|
limit_key: str,
|
|
value: LimitValue | None,
|
|
change_id: str,
|
|
changed_by: str,
|
|
reason: str,
|
|
correlation_id: str,
|
|
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():
|
|
row = self._locked_tenant(conn, tenant_id)
|
|
receipt = conn.execute(
|
|
"""SELECT request_fingerprint, result FROM idempotency_receipts
|
|
WHERE tenant_id = %s AND idempotency_key = %s""",
|
|
(row["tenant_id"], idempotency_key),
|
|
).fetchone()
|
|
if receipt is not None:
|
|
if receipt["request_fingerprint"] != request_fingerprint:
|
|
raise IdempotencyConflictError(idempotency_key)
|
|
prior = conn.execute(
|
|
"SELECT * FROM guardrail_changes WHERE change_id = %s", (change_id,)
|
|
).fetchone()
|
|
return _tenant(receipt["result"]), _change(prior) if prior else None, True
|
|
|
|
current = _tenant(row)
|
|
if current.version != expected_version:
|
|
raise VersionConflictError(expected=expected_version, actual=current.version)
|
|
override_rows = conn.execute(
|
|
"SELECT * FROM guardrail_overrides WHERE tenant_id = %s",
|
|
(current.tenant_id,),
|
|
).fetchall()
|
|
overrides = {item["limit_key"]: load_limit(item) for item in override_rows}
|
|
guard_guardrail_write(
|
|
tenant=current, overrides=overrides, limit_key=limit_key, value=value
|
|
)
|
|
change = GuardrailChange(
|
|
change_id=change_id,
|
|
tenant_id=current.tenant_id,
|
|
limit_key=limit_key,
|
|
previous=overrides.get(limit_key),
|
|
current=value,
|
|
changed_by=changed_by,
|
|
reason=reason,
|
|
correlation_id=correlation_id,
|
|
changed_at=at,
|
|
)
|
|
if value is None:
|
|
conn.execute(
|
|
"DELETE FROM guardrail_overrides WHERE tenant_id = %s AND limit_key = %s",
|
|
(current.tenant_id, limit_key),
|
|
)
|
|
else:
|
|
dumped = dump_limit(value)
|
|
conn.execute(
|
|
"""INSERT INTO guardrail_overrides
|
|
(tenant_id, limit_key, kind, amount, currency, period)
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT (tenant_id, limit_key) DO UPDATE SET
|
|
kind = EXCLUDED.kind, amount = EXCLUDED.amount,
|
|
currency = EXCLUDED.currency, period = EXCLUDED.period""",
|
|
(
|
|
current.tenant_id,
|
|
limit_key,
|
|
dumped["kind"],
|
|
dumped["amount"],
|
|
dumped["currency"],
|
|
dumped["period"],
|
|
),
|
|
)
|
|
conn.execute(
|
|
"""INSERT INTO guardrail_changes
|
|
(change_id, tenant_id, limit_key, previous, current, changed_by,
|
|
reason, correlation_id, changed_at)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)""",
|
|
(
|
|
change.change_id,
|
|
change.tenant_id,
|
|
change.limit_key,
|
|
Jsonb(dump_limit(change.previous)) if change.previous else None,
|
|
Jsonb(dump_limit(change.current)) if change.current else None,
|
|
change.changed_by,
|
|
change.reason,
|
|
change.correlation_id,
|
|
change.changed_at,
|
|
),
|
|
)
|
|
updated = replace(current, version=current.version + 1, updated_at=at)
|
|
conn.execute(
|
|
"UPDATE tenants SET version = %s, updated_at = %s WHERE tenant_id = %s",
|
|
(updated.version, updated.updated_at, updated.tenant_id),
|
|
)
|
|
self._record_receipt(conn, updated, idempotency_key, request_fingerprint)
|
|
self._emit(
|
|
conn,
|
|
"guardrail_changed",
|
|
updated.tenant_id,
|
|
{
|
|
"limit_key": limit_key,
|
|
"change_id": change_id,
|
|
"cleared": value is None,
|
|
"correlation_id": correlation_id,
|
|
"version": updated.version,
|
|
**(authz or {}),
|
|
},
|
|
)
|
|
return updated, change, False
|
|
|
|
@staticmethod
|
|
def _require_active(tenant: Tenant, what: str) -> None:
|
|
if tenant.lifecycle is not TenantLifecycle.ACTIVE:
|
|
raise TenantRetiredError(f"cannot {what} on a retired tenant")
|
|
|
|
@staticmethod
|
|
def _locked_tenant(conn: Any, tenant_id: str) -> Mapping[str, Any]:
|
|
row = conn.execute(
|
|
"""SELECT * FROM tenants WHERE tenant_id = %s OR identifier = %s
|
|
FOR UPDATE""",
|
|
(tenant_id, tenant_id),
|
|
).fetchone()
|
|
if row is None:
|
|
raise TenantNotFoundError(tenant_id)
|
|
return row
|
|
|
|
@staticmethod
|
|
def _record_receipt(
|
|
conn: Any, tenant: Tenant, idempotency_key: str, request_fingerprint: str
|
|
) -> None:
|
|
conn.execute(
|
|
"""INSERT INTO idempotency_receipts
|
|
(tenant_id, idempotency_key, request_fingerprint, result, recorded_at)
|
|
VALUES (%s, %s, %s, %s, %s)""",
|
|
(
|
|
tenant.tenant_id,
|
|
idempotency_key,
|
|
request_fingerprint,
|
|
Jsonb(_row(tenant)),
|
|
datetime.now(UTC),
|
|
),
|
|
)
|
|
|
|
@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, 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),
|
|
)
|
|
|
|
|
|
def _tenant(row: Mapping[str, Any]) -> Tenant:
|
|
return Tenant(
|
|
tenant_id=row["tenant_id"],
|
|
identifier=row["identifier"],
|
|
grouping=row["grouping_name"],
|
|
display_name=row["display_name"],
|
|
contact_email=row["contact_email"],
|
|
lifecycle=TenantLifecycle(row["lifecycle"]),
|
|
version=int(row["version"]),
|
|
created_at=_dt(row["created_at"]),
|
|
updated_at=_dt(row["updated_at"]),
|
|
retired_at=_dt(row["retired_at"]),
|
|
reactivated_at=_dt(row["reactivated_at"]),
|
|
)
|
|
|
|
|
|
def _row(tenant: Tenant) -> dict[str, Any]:
|
|
return {
|
|
"tenant_id": tenant.tenant_id,
|
|
"identifier": tenant.identifier,
|
|
"grouping_name": tenant.grouping,
|
|
"display_name": tenant.display_name,
|
|
"contact_email": tenant.contact_email,
|
|
"lifecycle": tenant.lifecycle.value,
|
|
"version": tenant.version,
|
|
"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),
|
|
}
|
|
|
|
|
|
def _dt(value: datetime | str | None) -> datetime | None:
|
|
if isinstance(value, datetime) or value is None:
|
|
return value
|
|
return datetime.fromisoformat(value)
|
|
|
|
|
|
def _grant(row: Mapping[str, Any]) -> RoleGrant:
|
|
return RoleGrant(
|
|
row["grant_id"],
|
|
row["tenant_id"],
|
|
CapabilityRole(row["role"]),
|
|
row["grant_reason"],
|
|
row["plan_id"],
|
|
row["granted_by"],
|
|
_dt(row["granted_at"]),
|
|
row["correlation_id"],
|
|
_dt(row["revoked_at"]),
|
|
)
|
|
|
|
|
|
def _change(row: Mapping[str, Any]) -> GuardrailChange:
|
|
return GuardrailChange(
|
|
change_id=row["change_id"],
|
|
tenant_id=row["tenant_id"],
|
|
limit_key=row["limit_key"],
|
|
previous=load_limit(row["previous"]) if row["previous"] else None,
|
|
current=load_limit(row["current"]) if row["current"] else None,
|
|
changed_by=row["changed_by"],
|
|
reason=row["reason"],
|
|
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,
|
|
)
|