"""Add PostgreSQL Row Level Security (RLS) for tenant isolation Revision ID: 0024 Revises: 0023 Create Date: 2026-05-23 """ from alembic import op from sqlalchemy import text revision = "0024" down_revision = "0023" branch_labels = None depends_on = None # --------------------------------------------------------------------------- # RLS expression helpers # --------------------------------------------------------------------------- _BYPASS = "COALESCE(current_setting('app.bypass_rls', true), 'off') = 'on'" _CID = "company_id = NULLIF(current_setting('app.company_id', true), '')::uuid" _IID = "id = NULLIF(current_setting('app.company_id', true), '')::uuid" def _using_cid(): return f"({_BYPASS} OR {_CID})" def _using_iid(): return f"({_BYPASS} OR {_IID})" def _using_join(): return ( f"({_BYPASS} OR user_id IN (" f"SELECT id FROM users WHERE {_CID}))" ) # --------------------------------------------------------------------------- # Tables # --------------------------------------------------------------------------- # Direct company_id column COMPANY_COL_TABLES = [ "absence_types", "audit_logs", "caldav_company_configs", "departments", "kiosk_devices", "ldap_configs", "overtime_balances", "smtp_configs", "users", "work_schedules", ] # Linked via user_id → users.company_id USER_JOIN_TABLES = [ "absences", "caldav_user_configs", "password_resets", "sessions", "time_entries", "vacation_balances", ] # public_holidays is global – no RLS # --------------------------------------------------------------------------- # upgrade / downgrade # --------------------------------------------------------------------------- def _exec(sql: str) -> None: op.execute(text(sql)) def _enable(table: str, using: str) -> None: _exec(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY") _exec(f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY") for cmd in ("SELECT", "INSERT", "UPDATE", "DELETE"): _exec(f"DROP POLICY IF EXISTS rls_{table}_{cmd.lower()} ON {table}") _exec(f"CREATE POLICY rls_{table}_select ON {table} FOR SELECT USING {using}") _exec(f"CREATE POLICY rls_{table}_insert ON {table} FOR INSERT WITH CHECK {using}") _exec(f"CREATE POLICY rls_{table}_update ON {table} FOR UPDATE USING {using} WITH CHECK {using}") _exec(f"CREATE POLICY rls_{table}_delete ON {table} FOR DELETE USING {using}") def _disable(table: str) -> None: for cmd in ("select", "insert", "update", "delete"): _exec(f"DROP POLICY IF EXISTS rls_{table}_{cmd} ON {table}") _exec(f"ALTER TABLE {table} NO FORCE ROW LEVEL SECURITY") _exec(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY") def upgrade() -> None: # companies: restrict by id (companies IS the tenant root) _enable("companies", _using_iid()) for table in COMPANY_COL_TABLES: _enable(table, _using_cid()) for table in USER_JOIN_TABLES: _enable(table, _using_join()) def downgrade() -> None: _disable("companies") for table in COMPANY_COL_TABLES: _disable(table) for table in USER_JOIN_TABLES: _disable(table)