"""Enable RLS for 8 tables that need tenant isolation. Tables excluded (no tenant_id column): - outbox_deliveries: linked via event_outbox which has tenant_id - marketplace_listings: global plugin marketplace, not tenant-specific Revision ID: 0129 Revises: 0128 """ from alembic import op import sqlalchemy as sa revision = "0129" down_revision = "0128" branch_labels = None depends_on = None TABLES_NEEDING_RLS = [ "ai_decision_records", "approval_requests", "automation_agent_run_steps", "roles", "sequences", "wiki_articles", "wiki_article_versions", "wiki_categories", ] def _table_exists(conn, table_name: str) -> bool: """True when the table exists (dual-path convergence, Gate B). Plugin-owned tables may not exist yet on a fresh install when Alembic reaches this revision — skip them instead of failing. The plugin-side convergence migrations apply the same RLS policies. """ row = conn.execute( sa.text("SELECT to_regclass(:tname) IS NOT NULL"), {"tname": f"public.{table_name}"}, ).scalar() return bool(row) def upgrade() -> None: conn = op.get_bind() for table in TABLES_NEEDING_RLS: if not _table_exists(conn, table): continue op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY;") op.execute( f"CREATE POLICY tenant_isolation ON {table} " f"FOR ALL USING (tenant_id = current_setting('app.tenant_id')::uuid);" ) def downgrade() -> None: conn = op.get_bind() for table in TABLES_NEEDING_RLS: if not _table_exists(conn, table): continue op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table};") op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY;")