Tenants Module
Overview
Multi-tenancy is the architectural foundation of ThreatOps SOCaaS. Every alert, incident, detection rule, SLA record, and user is scoped to a tenant. Without a robust tenant management layer, a platform operator cannot onboard new customers, control their tier and SIEM configuration, or enforce isolation between organizations.
The Tenants module provides the data layer and API for creating, listing, reading, and updating tenant records. It enforces role-based access control — super admins see all tenants, while standard users see only their own tenant. A graceful fallback to realistic demo data ensures the frontend remains functional even when the database is unavailable. Tenant creation is integrated with the onboarding flow, allowing new tenants to be added directly from the UI.
What Was Proposed
- Full CRUD API for tenant records (create, list, get, update)
- Role-based access control: super admins see all tenants, non-super-admins see only their own
- Tenant attributes: name, slug, tier (standard/premium/enterprise), primary SIEM, status, endpoint count, settings, and subscription dates
- Graceful fallback to demo data when the database is unavailable
- Frontend management table with tier and status color-coding, onboarding progress bars, and navigation to the onboarding flow
- Integration with tenant middleware for request-scoped tenant ID injection
What's Built
| List tenants endpoint (RBAC: super_admin sees all, others see own) | ✓ Complete |
| Get tenant by ID endpoint (403 for cross-tenant access) | ✓ Complete |
| Create tenant endpoint (POST, 201) | ✓ Complete |
| Update tenant endpoint (PATCH, partial update) | ✓ Complete |
| Demo data fallback (3 realistic tenants when DB unavailable) | ✓ Complete |
| Tenant model (PostgreSQL table via SQLAlchemy async) | ✓ Complete |
| TenantTier enum (standard/premium/enterprise) | ✓ Complete |
| TenantStatus enum (onboarding/active/suspended/churned) | ✓ Complete |
| SIEMType enum (sentinel/splunk/elastic/chronicle/qradar) | ✓ Complete |
| JSON settings field (auto_triage, SLA targets, MFA enforcement) | ✓ Complete |
| Frontend table with tier badges, status badges, endpoint count, onboarding progress bar | ✓ Complete |
| Frontend row click navigates to /onboarding?tenant={slug} | ✓ Complete |
| Frontend "Add Tenant" button routes to /onboarding | ✓ Complete |
| Tenant middleware sets request.state.tenant_id for all routes | ✓ Complete |
Architecture
Backend Router
File: app/routers/tenants.py — Prefix: /api/v1/tenants
The router uses async SQLAlchemy with AsyncSession injected via Depends(get_db). All queries use select(Tenant) with await db.execute(). A _demo_tenants() helper returns three pre-configured realistic tenants (Acme Financial Corp, GlobalHealth Systems, TechStart Inc) as a JSONResponse when any database exception is raised, allowing the frontend to function during development or database outages.
Role-Based Access Control
The list endpoint checks request.state.user_role (injected by the tenant middleware) against the value "super_admin". Super admins receive an unfiltered query over all tenant rows. All other roles receive a query filtered to Tenant.id == request.state.tenant_id, ensuring strict tenant isolation. The get-by-ID endpoint enforces the same check with an explicit 403 response rather than filtering.
Data Flow
Browser (Next.js) --> GET /api/v1/tenants/ --> TenantMiddleware sets request.state.tenant_id + user_role --> tenants router: if user_role != "super_admin": filter by tenant_id --> SQLAlchemy async query against PostgreSQL `tenants` table --> On DB error: return _demo_tenants() as JSONResponse --> Frontend renders table with tier/status badges + progress bars
Tenant Middleware
File: app/middleware/tenant.py — Inspects the JWT bearer token on every request and sets request.state.tenant_id and request.state.user_role. These values are consumed by the tenants router, alerts router, incidents router, and all other tenant-scoped modules.
Frontend Page
File: src/app/tenants/page.tsx — Client component that calls getTenants() from src/lib/api.ts on mount. Falls back silently to five hardcoded demo tenants (California Dept of Technology, Acme Corp, City of Merced, Fresno COE, San Mateo County) if the API call throws. The page uses Next.js useRouter for programmatic navigation to /onboarding?tenant={slug} on row click and to /onboarding on the Add Tenant button.
API Endpoints
GET /api/v1/tenants/
# List tenants. super_admin sees all; others see only their own tenant.
# Query params: skip=0 (int), limit=50 (int)
# Returns: list[TenantResponse]
# Fallback: JSONResponse with 3 demo tenants on DB error
GET /api/v1/tenants/{tenant_id}
# Get a single tenant by ID.
# 403 if non-super-admin requests a different tenant's record.
# 404 if tenant does not exist.
# Returns: TenantResponse
POST /api/v1/tenants/
# Create a new tenant.
# Body: TenantCreate { name, slug, tier?, primary_siem, status?, settings?, ... }
# Returns: TenantResponse (HTTP 201)
PATCH /api/v1/tenants/{tenant_id}
# Partial update of a tenant record.
# Body: TenantUpdate { any subset of Tenant fields }
# 404 if tenant does not exist.
# Returns: TenantResponse
Routing
| Layer | Path | Description |
|---|---|---|
| /tenants | Frontend route (Next.js App Router) | Tenant management table page |
| /api/v1/tenants | API prefix (FastAPI router) | All tenant CRUD endpoints |
| /onboarding | Frontend route (Next.js App Router) | Add Tenant / tenant detail — linked from tenants page |
Data Model
Model: app/models/tenant.py — Table: tenants (SQLAlchemy declarative base)
| Field | Type | Description |
|---|---|---|
id | String(36) PK | UUID primary key, auto-generated |
name | String(255) NOT NULL | Full organization display name (e.g. "Acme Financial Corp") |
slug | String(100) UNIQUE NOT NULL | URL-safe identifier used in onboarding URLs (e.g. "acme-financial") |
tier | Enum(TenantTier) | standard / premium / enterprise. Default: standard |
primary_siem | Enum(SIEMType) | sentinel / splunk / elastic / chronicle / qradar |
status | Enum(TenantStatus) | onboarding / active / suspended / churned. Default: onboarding |
settings | JSON | Freeform tenant settings: auto_triage (bool), sla_critical_minutes (int), mfa_enforced (bool), etc. |
subscription_start | DateTime (TZ) | Contract start date (nullable) |
subscription_end | DateTime (TZ) | Contract end date (nullable) |
endpoint_count | int | Number of monitored endpoints for this tenant. Default: 0 |
created_at | DateTime (TZ) | Row creation timestamp (server default: now()) |
updated_at | DateTime (TZ) | Last modification timestamp (auto-updated on write) |
Relationships
users— One-to-many relationship withUsermodel. Loaded withlazy="selectin"(automatic join on load).- All alert, incident, detection rule, and SLA records have a
tenant_idforeign key pointing to this table for multi-tenant isolation.
Demo Data (returned on DB failure)
| Tenant | Tier | SIEM | Status | Endpoints |
|---|---|---|---|---|
| Acme Financial Corp | enterprise | sentinel | active | 12,500 |
| GlobalHealth Systems | standard | splunk | active | 3,200 |
| TechStart Inc | startup | crowdstrike | active | 450 |
Frontend Fallback Data
The frontend also has its own hardcoded fallback array displayed when the API call fails. This is a separate list from the backend demo data:
| Tenant | Tier | SIEM | Status | Onboarding % |
|---|---|---|---|---|
| California Dept of Technology | enterprise | sentinel | active | 100% |
| Acme Corp | premium | sentinel | active | 100% |
| City of Merced | standard | sentinel | active | 100% |
| Fresno COE | standard | splunk | active | 85% |
| San Mateo County | premium | sentinel | onboarding | 60% |
Prerequisites
- Database — PostgreSQL with async SQLAlchemy (
app/core/database.py). Thetenantstable must exist. Alembic migrations create it. On failure, the router falls back to demo data. - Tenant Middleware —
app/middleware/tenant.pymust be registered inapp/main.pyto injectrequest.state.tenant_idandrequest.state.user_role. Without it, the RBAC check in the list endpoint will raise anAttributeError. - Auth Schemas —
app/schemas/tenant.pydefinesTenantCreate,TenantUpdate, andTenantResponsePydantic models used for request validation and response serialization. - Frontend API helper —
src/lib/api.tsexports thegetTenants()function called by the page component. Uses the base API client with bearer token auth.
UI Layout
Page Structure
- Header Row — Orange Building2 icon + "Tenant Management" h1 + tenant count badge (gray pill showing number of tenants). Orange "Add Tenant" button (Plus icon) on the right — navigates to
/onboarding. - Loading State — Centered orange spinning Loader2 icon with "Loading tenants..." label while the API call is in flight.
- Tenant Table — White rounded card with
shadow-sm. Columns:
| Column | Content | Styling |
|---|---|---|
| Name | Organization name (medium weight) + slug below (small, gray) | Two-line cell |
| SIEM | Primary SIEM type (capitalized) | slate-700 |
| Tier | Tier name as a pill badge | standard=gray, premium=orange, enterprise=amber |
| Status | Status as a pill badge | active=green, onboarding=amber, suspended=red |
| Endpoints | Endpoint count (locale-formatted, right-aligned) | slate-700 |
| Onboarding | Progress bar + percentage label | Orange fill bar on slate-200 track, 8px height |
Each row is clickable (cursor-pointer, hover:bg-slate-50) and navigates to /onboarding?tenant={slug ?? id} using Next.js router.