2026-07-04 01:32:14 +00:00
|
|
|
"""Enable Row Level Security with tenant isolation policies on core tables.
|
|
|
|
|
|
2026-07-04 08:12:39 +00:00
|
|
|
Idempotent: drops existing policies before creating them.
|
|
|
|
|
|
2026-07-04 01:32:14 +00:00
|
|
|
Revision ID: 0015_rls_policies
|
|
|
|
|
Revises: 0014_currency_unique_fix
|
|
|
|
|
Create Date: 2026-07-04
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Sequence, Union
|
|
|
|
|
|
|
|
|
|
from alembic import op
|
|
|
|
|
|
|
|
|
|
revision: str = "0015_rls_policies"
|
|
|
|
|
down_revision: Union[str, None] = "0014_currency_unique_fix"
|
|
|
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
2026-07-04 01:37:04 +00:00
|
|
|
depends_on: Union[str, Sequence[str], None] = None
|
2026-07-04 01:32:14 +00:00
|
|
|
|
|
|
|
|
# Tables with tenant_id column that get RLS
|
|
|
|
|
RLS_TABLES = [
|
|
|
|
|
"companies",
|
|
|
|
|
"contacts",
|
|
|
|
|
"company_contacts",
|
|
|
|
|
"currencies",
|
|
|
|
|
"tax_rates",
|
|
|
|
|
"sequences",
|
|
|
|
|
"system_settings",
|
|
|
|
|
"attachments",
|
|
|
|
|
"addresses",
|
|
|
|
|
"users",
|
|
|
|
|
"roles",
|
|
|
|
|
"sessions",
|
|
|
|
|
"audit_log",
|
|
|
|
|
"deletion_log",
|
|
|
|
|
"notifications",
|
|
|
|
|
"ai_conversations",
|
|
|
|
|
"ai_messages",
|
|
|
|
|
"workflows",
|
|
|
|
|
"workflow_instances",
|
|
|
|
|
"workflow_step_history",
|
|
|
|
|
"password_reset_tokens",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def upgrade() -> None:
|
|
|
|
|
for table_name in RLS_TABLES:
|
2026-07-04 08:12:39 +00:00
|
|
|
# Enable RLS on the table (idempotent — ENABLE is safe to repeat)
|
2026-07-04 01:32:14 +00:00
|
|
|
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
|
|
|
|
|
2026-07-04 08:12:39 +00:00
|
|
|
# Drop existing policy if it exists (idempotent — prevents DuplicateObjectError on restart)
|
|
|
|
|
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table_name}")
|
|
|
|
|
|
2026-07-04 01:32:14 +00:00
|
|
|
# Create tenant isolation policy
|
|
|
|
|
# USING clause: tenant_id must match the session variable set by the app
|
|
|
|
|
op.execute(
|
|
|
|
|
f"CREATE POLICY tenant_isolation ON {table_name} "
|
|
|
|
|
f"USING (tenant_id = current_setting('app.current_tenant_id')::uuid)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def downgrade() -> None:
|
|
|
|
|
for table_name in reversed(RLS_TABLES):
|
|
|
|
|
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table_name}")
|
|
|
|
|
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|