# Policy Engine Tenant/Project RLS Design (Prep for POLICY-TEN-48-001) ## Goals - Add tenant + project scoping to Policy Engine data and APIs with Row Level Security (RLS) to enforce isolation. - Provide deterministic migration order and guardrails so downstream consumers (Registry, Risk Engine, VEX Lens) can align without drift. ## Scope - Applies to `PolicyEngine` Postgres tables: `risk_profiles`, `risk_profile_versions`, `risk_profile_overrides`, `simulations`, `simulation_jobs`, `policy_events`, `policy_packs` (registry), and `policy_audit`. - API surface: all `/api/risk/*`, `/api/policy/*`, registry endpoints, and CLI operations. ## Schema Changes - Add columns (nullable=false): - `tenant_id text` - `project_id text NULL` (optional for tenant-wide assets) - `created_by text`, `updated_by text` - Composite keys: - Primary/business keys extend with `tenant_id` (and `project_id` where present). - Unique constraints include `tenant_id` (+ `project_id`) to prevent cross-tenant collisions. - Indexes: - `(tenant_id)` and `(tenant_id, project_id)` for all hot tables. - Deterministic ordering indexes `(tenant_id, project_id, created_at, id)` for paging. ## RLS Policies - Enable RLS on all scoped tables. - Policy examples: - `USING (tenant_id = current_setting('app.tenant_id')::text AND (project_id IS NULL OR project_id = current_setting('app.project_id', true)))` - Write policy also checks `app.can_write` custom GUC when needed. - Set GUCs in connection middleware: - `SET LOCAL app.tenant_id = @TenantHeader` - `SET LOCAL app.project_id = @ProjectHeader` (optional) - `SET LOCAL app.can_write = true|false` based on auth scope. ## Migrations (order) 1) Add columns (nullable with default) + backfill tenants/projects from existing data or default `public`. 2) Backfill audit columns (`created_by`, `updated_by`) from existing provenance if present. 3) Add indexes. 4) Tighten constraints (drop defaults, set NOT NULL where required). 5) Enable RLS and create policies. 6) Update views/functions to include tenant/project predicates. ## API/DTO Changes - Require headers: `X-Stella-Tenant` (mandatory), `X-Stella-Project` (optional). - Extend DTOs to include `tenantId`, `projectId` where relevant. - Validate header presence early; return 400 with deterministic error code `POLICY_TENANT_HEADER_REQUIRED` when missing. ## CLI Contracts - CLI commands accept `--tenant` and optional `--project` flags; persist in profile config. - Example (captured output): ``` $ stella policy profiles list --tenant tenant-123 --project proj-a --page-size 10 tenant: tenant-123 project: proj-a page: 1 size: 10 profiles: - risk-profile-core@3.2.0 (status=active) - risk-profile-payments@1.4.1 (status=active) ``` ## Testing Strategy - Unit: policy predicates covering tenant/project matches, NULL project handling, and deny-by-default. - Integration: end-to-end API calls with different tenants/projects; ensure cross-tenant leakage is rejected with 403 and deterministic error codes. - Migration safety: run in `SAFE` mode first (RLS disabled, predicates logged) then enable RLS after verification. ## Rollout Notes - Default tenant for legacy data: `public` (configurable). - Air-gap/offline bundles must embed `tenant_id`/`project_id` in metadata; validation rejects mismatched headers. - Observability: add metrics `policy.rls.denied_total` and structured logs tagging `tenant_id`, `project_id`. ## Ownership - Policy Guild owns schema and API updates; Platform/DB Guild reviews RLS policies; Security Guild signs off on deny-by-default posture.