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

10
Containerfile Normal file
View file

@ -0,0 +1,10 @@
FROM python:3.12-slim
RUN useradd --system --uid 10001 --create-home tenant-engine
WORKDIR /app
COPY pyproject.toml README.md LICENSE ./
COPY src ./src
RUN pip install --no-cache-dir .
USER 10001
EXPOSE 8090
ENV TENANT_ENGINE_HOST=0.0.0.0 TENANT_ENGINE_HTTP_PORT=8090
CMD ["tenant-engine"]

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)

View file

@ -0,0 +1,11 @@
from tenant_engine.domain import Tenant
from tenant_engine.sqlite_store import SQLiteTenantStore
def test_tenant_survives_store_reopen(tmp_path):
path = str(tmp_path / "tenants.db")
SQLiteTenantStore(path).create_tenant(
Tenant.create(tenant_id="tenant:friendly:new", identifier="tenant:friendly:new")
)
tenant = SQLiteTenantStore(path).get_tenant("tenant:friendly:new")
assert tenant.grouping == "friendly"

View file

@ -0,0 +1,56 @@
---
id: TEN-WP-0004
type: workplan
title: "Production tenant authority runtime"
domain: infotech
repo: tenant-engine
status: finished
owner: codex
topic_slug: netkingdom
created: "2026-08-08"
updated: "2026-08-08"
---
# Production tenant authority runtime
Deploy the already-defined tenant authority and flex-auth policy behind the
user-engine platform portal with restart-safe persistence.
## Durable store and package
```task
id: TEN-WP-0004-T01
status: done
priority: high
```
Provide a durable runtime store, container image, and persistence tests.
Done 2026-08-09: SQLite-backed persistence on a 1 Gi RWO PVC, immutable image,
and restart-survival verification are complete.
## Production deployment
```task
id: TEN-WP-0004-T02
status: done
priority: high
```
Deploy flex-auth and tenant-engine with least-privilege networking and storage.
Done 2026-08-09: both services are Ready with namespace-scoped ingress/egress;
the live flex-auth-authorized create returned 201.
## Portal integration and live verification
```task
id: TEN-WP-0004-T03
status: done
priority: high
```
Align user-engine's adapter with the authority contract and prove tenant creation.
Done 2026-08-09: the deployed portal adapter created
`tenant:trial:portalcheck`; tenant-engine resolved it after a pod restart.