"""Enable RLS on Phase 2 session and supporting tables. 10 tables use a standard tenant-only policy. step_library uses a visibility-aware policy — public steps visible to all tenants. NOTE: session_messages does not exist in this codebase (removed from plan). script_generations is the correct table name (not script_template_generations). sessions and ai_sessions are two separate tables, both in scope. Prerequisites: - Phase 1 migration must have run (resolutionflow_app role exists, Phase 1 tables have RLS) - NOT NULL write-path bugs fixed (P2-A commits b641ac6) - shares.py cross-tenant session fix deployed (P2-B commit ac2b193) Revision ID: 70a5dd746e83 Revises: c5f48b9890f9 Create Date: 2026-04-10 06:54:49.431817 """ from typing import Sequence, Union from alembic import op # revision identifiers, used by Alembic. revision: str = '70a5dd746e83' down_revision: Union[str, None] = 'c5f48b9890f9' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None _NULL_UUID = "00000000-0000-0000-0000-000000000000" _CURRENT_ACCOUNT = ( f"COALESCE(NULLIF(current_setting('app.current_account_id', TRUE), ''), " f"'{_NULL_UUID}')::uuid" ) # Standard tenant-only policy — account_id must match the current tenant. # When no tenant context is set, COALESCE returns the nil UUID so zero rows # are visible (fail-closed). _STANDARD_USING = f"account_id = {_CURRENT_ACCOUNT}" # Visibility-aware policy for step_library — public steps (visibility='public') # must be visible to ALL tenants regardless of account_id. This covers the # visibility='public' arm of build_step_visibility_filter() in app/core/filters.py. # The created_by arm (private steps visible to their author) is covered # transitively: private steps share account_id with their creator, so the # account_id match handles it. This relies on account_id NOT NULL on step_library. _STEP_LIBRARY_USING = f"account_id = {_CURRENT_ACCOUNT} OR visibility = 'public'" # Standard tables: strict tenant isolation, no cross-tenant visibility. _STANDARD_TABLES = [ "sessions", "ai_sessions", "session_branches", "session_supporting_data", "session_resolution_outputs", "session_handoffs", "script_templates", "script_generations", "maintenance_schedules", "psa_post_log", ] def upgrade() -> None: # ── Standard tenant-isolation tables ──────────────────────────────────── 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 ({_STANDARD_USING}) """) # ── step_library ──────────────────────────────────────────────────────── # Public steps (visibility='public') must be readable by all tenants so # the Solutions Library browsing experience works without tenant context. # Private/team steps remain tenant-scoped. op.execute("ALTER TABLE step_library ENABLE ROW LEVEL SECURITY") op.execute("ALTER TABLE step_library FORCE ROW LEVEL SECURITY") op.execute(f""" CREATE POLICY tenant_isolation ON step_library USING ({_STEP_LIBRARY_USING}) """) def downgrade() -> None: for table in _STANDARD_TABLES + ["step_library"]: op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table}") op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY") op.execute(f"ALTER TABLE {table} NO FORCE ROW LEVEL SECURITY")