"""Multi-tenant support: org/team isolation for sessions and data.""" from __future__ import annotations import os import time from typing import Any from fastapi import APIRouter, Header, HTTPException from fusionagi._logger import logger router = APIRouter() DEFAULT_TENANT = os.environ.get("FUSIONAGI_DEFAULT_TENANT", "default") # In-memory tenant registry; for production, back with Postgres _tenant_store: dict[str, dict[str, Any]] = { DEFAULT_TENANT: { "id": DEFAULT_TENANT, "name": "Default Tenant", "status": "active", "created_at": time.time(), "config": {}, } } def resolve_tenant(x_tenant_id: str | None = Header(default=None)) -> str: """Resolve tenant from X-Tenant-ID header or default.""" return x_tenant_id or DEFAULT_TENANT @router.get("/tenants/current") def get_current_tenant(x_tenant_id: str | None = Header(default=None)) -> dict[str, Any]: """Return the resolved tenant context. The tenant is determined from the X-Tenant-ID header. Falls back to the default tenant if no header is provided. """ tid = resolve_tenant(x_tenant_id) return { "tenant_id": tid, "is_default": tid == DEFAULT_TENANT, "isolation_mode": "logical", "exists": tid in _tenant_store, } @router.get("/tenants") def list_tenants() -> dict[str, Any]: """List all registered tenants. Returns: JSON with tenants array and total count. """ tenants = list(_tenant_store.values()) return {"tenants": tenants, "total": len(tenants)} @router.get("/tenants/{tenant_id}") def get_tenant(tenant_id: str) -> dict[str, Any]: """Get a specific tenant by ID. Args: tenant_id: Tenant identifier. Returns: Tenant record. Raises: 404 if tenant not found. """ tenant = _tenant_store.get(tenant_id) if not tenant: raise HTTPException(status_code=404, detail=f"Tenant {tenant_id} not found") return tenant @router.post("/tenants") def create_tenant(body: dict[str, Any]) -> dict[str, Any]: """Register a new tenant. Args: body: JSON with 'id' and optional 'name', 'config' fields. Returns: Created tenant record. """ tenant_id = body.get("id", "") if not tenant_id: raise HTTPException(status_code=400, detail="Tenant ID required") if tenant_id in _tenant_store: raise HTTPException(status_code=409, detail=f"Tenant {tenant_id} already exists") name = body.get("name", tenant_id) config = body.get("config", {}) tenant = { "id": tenant_id, "name": name, "status": "active", "created_at": time.time(), "config": config, } _tenant_store[tenant_id] = tenant logger.info("Tenant created", extra={"tenant_id": tenant_id, "name": name}) return tenant @router.put("/tenants/{tenant_id}") def update_tenant(tenant_id: str, body: dict[str, Any]) -> dict[str, Any]: """Update tenant configuration. Args: tenant_id: Tenant identifier. body: JSON with fields to update (name, config, status). Returns: Updated tenant record. """ tenant = _tenant_store.get(tenant_id) if not tenant: raise HTTPException(status_code=404, detail=f"Tenant {tenant_id} not found") if "name" in body: tenant["name"] = body["name"] if "config" in body: tenant["config"] = body["config"] if "status" in body: tenant["status"] = body["status"] logger.info("Tenant updated", extra={"tenant_id": tenant_id}) return tenant @router.delete("/tenants/{tenant_id}") def deactivate_tenant(tenant_id: str) -> dict[str, Any]: """Deactivate a tenant (soft delete). Args: tenant_id: Tenant identifier. Returns: Confirmation with tenant status. """ if tenant_id == DEFAULT_TENANT: raise HTTPException(status_code=400, detail="Cannot deactivate default tenant") tenant = _tenant_store.get(tenant_id) if not tenant: raise HTTPException(status_code=404, detail=f"Tenant {tenant_id} not found") tenant["status"] = "inactive" logger.info("Tenant deactivated", extra={"tenant_id": tenant_id}) return {"id": tenant_id, "status": "inactive"}