ADR 003: Classification and Row-Level Security
Status: Accepted
Date: 2026-05-08
Context
The Rule Repository stores data with varying sensitivity levels across multiple tenants. Rules, evaluations, and audit entries need access control that goes beyond simple role-based checks -- classification-based Row-Level Security (RLS) must ensure that users only see data matching their clearance level and department membership.
Decision
We use PostgreSQL Row-Level Security with four classification levels (PUBLIC, INTERNAL, CONFIDENTIAL, RESTRICTED). Every query session sets three variables via with_user_context():
app.user_id-- the authenticated userapp.user_clearance-- the user's maximum classification clearanceapp.user_departments-- comma-separated department IDs
RLS policies on rules, evaluations, and audit_log tables enforce that:
PUBLICrows are visible to all authenticated users within the tenantINTERNALrows are visible to any tenant memberCONFIDENTIALrows are visible to department members and approved subscribersRESTRICTEDrows are visible only to named individuals or users withAUDITORcapacity
Classification-based RLS coexists with tenant-based RLS. Both layers must pass for a row to be visible.
Consequences
- Every database query must call
with_user_context()first -- direct queries bypass classification - Elasticsearch queries must include a matching classification filter via
services/classification/es_filter.py - The audit log itself is subject to classification -- auditors access it through a separate connection pool
- Performance impact is minimal since RLS is index-backed on
tenant_idandclassification