"""Enable RLS on Phase 4 tables — all remaining tenant-scoped tables. All tables in this migration already have account_id NOT NULL (enforced by earlier migrations). This migration adds ENABLE ROW LEVEL SECURITY, FORCE ROW LEVEL SECURITY, and the appropriate tenant isolation policy to each. Policy variants used: - Standard: account_id = current_setting(app.current_account_id)::uuid - Platform: standard OR account_id = PLATFORM_ACCOUNT_ID (for global content tables readable by all tenants) Skipped intentionally: - accounts — IS the root table; no account_id column - plan_feature_defaults — platform config; no account_id column - script_categories — global lookup table; no account_id column (ScriptTemplate in the same file has account_id, ScriptCategory does not) Revision ID: b3c7e9f2a1d8 Revises: 172ad76d7d20 Create Date: 2026-04-12 """ from typing import Union from alembic import op revision: str = "b3c7e9f2a1d8" down_revision: Union[str, None] = "172ad76d7d20" branch_labels = None depends_on = None PLATFORM_ACCOUNT_ID = "00000000-0000-0000-0000-000000000001" # Standard policy — tenant sees only own rows. _STANDARD_TABLES = [ "users", "account_invites", "account_limit_overrides", "account_feature_overrides", "subscriptions", "ai_chat_sessions", "ai_conversations", "ai_session_steps", "ai_session_embeddings", "ai_suggestions", "ai_usage", "assistant_chats", "attachments", "copilot_conversations", "feedback", "file_uploads", "fork_points", "kb_imports", "notifications", "notification_configs", "notification_logs", "psa_activity_logs", "psa_member_mappings", "script_builder_sessions", "session_ratings", "tree_embeddings", "user_folders", "user_pinned_trees", ] # Platform-visibility policy — tenant sees own rows PLUS PLATFORM_ACCOUNT_ID rows. # These tables hold global content created by ResolutionFlow admins. _PLATFORM_TABLES = [ "platform_steps", "template_trees", ] _POLICY_EXPR = ( "account_id = COALESCE(" "NULLIF(current_setting('app.current_account_id', TRUE), ''), " "'00000000-0000-0000-0000-000000000000'" ")::uuid" ) def upgrade() -> None: # Standard tables — tenant isolation only for table in _STANDARD_TABLES: op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY") op.execute(f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY") op.execute(f""" CREATE POLICY tenant_isolation ON {table} USING ({_POLICY_EXPR}) """) # Platform-visible tables — own rows OR global platform rows for table in _PLATFORM_TABLES: op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY") op.execute(f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY") op.execute(f""" CREATE POLICY tenant_isolation ON {table} USING ( {_POLICY_EXPR} OR account_id = '{PLATFORM_ACCOUNT_ID}'::uuid ) """) def downgrade() -> None: for table in _STANDARD_TABLES + _PLATFORM_TABLES: op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table}") op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY")