91 lines
3.7 KiB
Python
91 lines
3.7 KiB
Python
"""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")
|