Deploy durable tenant authority runtime

This commit is contained in:
tegwick 2026-08-09 01:35:00 +02:00
parent 8bd0741104
commit 1e44cace1f
6 changed files with 210 additions and 2 deletions

View file

@ -10,6 +10,7 @@ class Settings:
flex_auth_timeout_seconds: float
host: str
port: int
database_path: str | None = None
@classmethod
def from_env(cls) -> "Settings":
@ -17,5 +18,6 @@ class Settings:
flex_auth_base_url=os.getenv("TENANT_ENGINE_FLEX_AUTH_URL") or None,
flex_auth_timeout_seconds=float(os.getenv("TENANT_ENGINE_FLEX_AUTH_TIMEOUT_SECONDS", "3")),
host=os.getenv("TENANT_ENGINE_HOST", "127.0.0.1"),
port=int(os.getenv("TENANT_ENGINE_PORT", "8090")),
port=int(os.getenv("TENANT_ENGINE_HTTP_PORT", "8090")),
database_path=os.getenv("TENANT_ENGINE_DATABASE_PATH") or None,
)

View file

@ -4,11 +4,13 @@ import uvicorn
from tenant_engine.app import create_app
from tenant_engine.config import Settings
from tenant_engine.sqlite_store import SQLiteTenantStore
def main() -> None:
settings = Settings.from_env()
uvicorn.run(create_app(settings=settings), host=settings.host, port=settings.port)
store = SQLiteTenantStore(settings.database_path) if settings.database_path else None
uvicorn.run(create_app(settings=settings, store=store), host=settings.host, port=settings.port)
if __name__ == "__main__":

View file

@ -0,0 +1,127 @@
from __future__ import annotations
import json
import sqlite3
from datetime import datetime
from threading import RLock
from tenant_engine.domain import CapabilityRole, PlanAssignment, RoleGrant, Tenant
from tenant_engine.store import (
DomainEvent,
GrantNotFoundError,
TenantAlreadyExistsError,
TenantNotFoundError,
)
class SQLiteTenantStore:
"""Durable single-node tenant store used by the Kubernetes runtime."""
def __init__(self, path: str) -> None:
self._db = sqlite3.connect(path, check_same_thread=False)
self._db.row_factory = sqlite3.Row
self._lock = RLock()
with self._db:
self._db.executescript("""
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS tenants (
tenant_id TEXT PRIMARY KEY, identifier TEXT UNIQUE NOT NULL,
grouping_name TEXT
);
CREATE TABLE IF NOT EXISTS grants (
grant_id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, role TEXT NOT NULL,
grant_reason TEXT NOT NULL, plan_id TEXT, granted_by TEXT NOT NULL,
granted_at TEXT NOT NULL, correlation_id TEXT NOT NULL, revoked_at TEXT
);
CREATE TABLE IF NOT EXISTS plans (
tenant_id TEXT PRIMARY KEY, plan_id TEXT NOT NULL, assigned_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS events (
seq INTEGER PRIMARY KEY AUTOINCREMENT, event_type TEXT NOT NULL,
tenant_id TEXT NOT NULL, at TEXT NOT NULL, payload TEXT NOT NULL
);
""")
def create_tenant(self, tenant: Tenant) -> None:
with self._lock, self._db:
try:
self._db.execute(
"INSERT INTO tenants VALUES (?, ?, ?)",
(tenant.tenant_id, tenant.identifier, tenant.grouping),
)
except sqlite3.IntegrityError as exc:
raise TenantAlreadyExistsError(tenant.identifier) from exc
self._emit("tenant_created", tenant.tenant_id, {
"identifier": tenant.identifier, "grouping": tenant.grouping,
})
def get_tenant(self, tenant_id: str) -> Tenant:
row = self._db.execute(
"SELECT * FROM tenants WHERE tenant_id = ? OR identifier = ?", (tenant_id, tenant_id)
).fetchone()
if row is None:
raise TenantNotFoundError(tenant_id)
return Tenant(row["tenant_id"], row["identifier"], row["grouping_name"])
def grant_role(self, grant: RoleGrant) -> None:
tenant = self.get_tenant(grant.tenant_id)
with self._lock, self._db:
self._db.execute(
"INSERT OR REPLACE INTO grants VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(grant.grant_id, tenant.tenant_id, grant.role.value, grant.grant_reason,
grant.plan_id, grant.granted_by, grant.granted_at.isoformat(),
grant.correlation_id, grant.revoked_at.isoformat() if grant.revoked_at else None),
)
self._emit("role_granted", tenant.tenant_id, {"grant_id": grant.grant_id,
"role": grant.role.value, "grant_reason": grant.grant_reason,
"correlation_id": grant.correlation_id})
def revoke_role(self, *, tenant_id: str, grant_id: str, at: datetime) -> RoleGrant:
tenant = self.get_tenant(tenant_id)
row = self._db.execute(
"SELECT * FROM grants WHERE tenant_id = ? AND grant_id = ?",
(tenant.tenant_id, grant_id),
).fetchone()
if row is None:
raise GrantNotFoundError(grant_id)
grant = self._grant(row).revoke(at=at)
with self._lock, self._db:
self._db.execute("UPDATE grants SET revoked_at = ? WHERE grant_id = ?",
(at.isoformat(), grant_id))
self._emit("role_revoked", tenant.tenant_id,
{"grant_id": grant_id, "role": grant.role.value})
return grant
def active_roles(self, tenant_id: str) -> frozenset[CapabilityRole]:
tenant = self.get_tenant(tenant_id)
rows = self._db.execute(
"SELECT role FROM grants WHERE tenant_id = ? AND revoked_at IS NULL",
(tenant.tenant_id,),
).fetchall()
return frozenset(CapabilityRole(row["role"]) for row in rows)
def assign_plan(self, assignment: PlanAssignment) -> None:
tenant = self.get_tenant(assignment.tenant_id)
with self._lock, self._db:
self._db.execute(
"INSERT OR REPLACE INTO plans VALUES (?, ?, ?)",
(tenant.tenant_id, assignment.plan_id, assignment.assigned_at.isoformat()),
)
self._emit("plan_assigned", tenant.tenant_id, {"plan_id": assignment.plan_id})
def events(self) -> list[DomainEvent]:
return [DomainEvent(row["event_type"], row["tenant_id"],
datetime.fromisoformat(row["at"]), json.loads(row["payload"]))
for row in self._db.execute("SELECT * FROM events ORDER BY seq")]
def _emit(self, event_type: str, tenant_id: str, payload: dict) -> None:
now = datetime.now().astimezone()
self._db.execute("INSERT INTO events(event_type,tenant_id,at,payload) VALUES(?,?,?,?)",
(event_type, tenant_id, now.isoformat(), json.dumps(payload)))
@staticmethod
def _grant(row: sqlite3.Row) -> RoleGrant:
return RoleGrant(row["grant_id"], row["tenant_id"], CapabilityRole(row["role"]),
row["grant_reason"], row["plan_id"], row["granted_by"],
datetime.fromisoformat(row["granted_at"]), row["correlation_id"],
datetime.fromisoformat(row["revoked_at"]) if row["revoked_at"] else None)