diff --git a/alembic/versions/0015_rls_policies.py b/alembic/versions/0015_rls_policies.py new file mode 100644 index 0000000..38cd23c --- /dev/null +++ b/alembic/versions/0015_rls_policies.py @@ -0,0 +1,64 @@ +"""Enable Row Level Security with tenant isolation policies on core tables. + +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 +dends_on: Union[str, Sequence[str], None] = None + +# 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", +] + +# Tables without tenant_id — no RLS (plugins, plugin_migrations are global) +NO_RLS_TABLES = ["plugins", "plugin_migrations"] + + +def upgrade() -> None: + for table_name in RLS_TABLES: + # Enable RLS on the table + op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY") + + # 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")