feat: migration — enable RLS on trees, tags, categories, psa_connections, flow_proposals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
108
backend/alembic/versions/c5f48b9890f9_enable_rls_phase1.py
Normal file
108
backend/alembic/versions/c5f48b9890f9_enable_rls_phase1.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"""enable_rls_phase1
|
||||||
|
|
||||||
|
Revision ID: c5f48b9890f9
|
||||||
|
Revises: 0b470d9e6cf1
|
||||||
|
Create Date: 2026-04-10 04:01:13.043321
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'c5f48b9890f9'
|
||||||
|
down_revision: Union[str, None] = '0b470d9e6cf1'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
_NULL_UUID = "00000000-0000-0000-0000-000000000000"
|
||||||
|
_PLATFORM_UUID = "00000000-0000-0000-0000-000000000001"
|
||||||
|
_CURRENT_ACCOUNT = (
|
||||||
|
f"COALESCE(NULLIF(current_setting('app.current_account_id', TRUE), ''), "
|
||||||
|
f"'{_NULL_UUID}')::uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ── trees ───────────────────────────────────────────────────────────────
|
||||||
|
# Extended policy mirrors can_access_tree() in app/core/permissions.py.
|
||||||
|
# Tenant sees: own rows, platform rows, any default tree, any public tree,
|
||||||
|
# any gallery-featured tree.
|
||||||
|
# is_gallery_featured = TRUE is included because /public/templates is a
|
||||||
|
# no-auth endpoint — no tenant context is set, so gallery trees must pass
|
||||||
|
# RLS on their own flag rather than relying on account_id or is_public.
|
||||||
|
# Private/team trees from other accounts are hidden.
|
||||||
|
op.execute("ALTER TABLE trees ENABLE ROW LEVEL SECURITY")
|
||||||
|
op.execute("ALTER TABLE trees FORCE ROW LEVEL SECURITY")
|
||||||
|
op.execute(f"""
|
||||||
|
CREATE POLICY tenant_isolation ON trees
|
||||||
|
USING (
|
||||||
|
account_id = {_CURRENT_ACCOUNT}
|
||||||
|
OR account_id = '{_PLATFORM_UUID}'::uuid
|
||||||
|
OR is_default = TRUE
|
||||||
|
OR is_public = TRUE
|
||||||
|
OR is_gallery_featured = TRUE
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ── tree_tags ────────────────────────────────────────────────────────────
|
||||||
|
# Own account + platform tags (global tags visible to all tenants).
|
||||||
|
op.execute("ALTER TABLE tree_tags ENABLE ROW LEVEL SECURITY")
|
||||||
|
op.execute("ALTER TABLE tree_tags FORCE ROW LEVEL SECURITY")
|
||||||
|
op.execute(f"""
|
||||||
|
CREATE POLICY tenant_isolation ON tree_tags
|
||||||
|
USING (
|
||||||
|
account_id = {_CURRENT_ACCOUNT}
|
||||||
|
OR account_id = '{_PLATFORM_UUID}'::uuid
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ── tree_categories ──────────────────────────────────────────────────────
|
||||||
|
op.execute("ALTER TABLE tree_categories ENABLE ROW LEVEL SECURITY")
|
||||||
|
op.execute("ALTER TABLE tree_categories FORCE ROW LEVEL SECURITY")
|
||||||
|
op.execute(f"""
|
||||||
|
CREATE POLICY tenant_isolation ON tree_categories
|
||||||
|
USING (
|
||||||
|
account_id = {_CURRENT_ACCOUNT}
|
||||||
|
OR account_id = '{_PLATFORM_UUID}'::uuid
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ── step_categories ──────────────────────────────────────────────────────
|
||||||
|
op.execute("ALTER TABLE step_categories ENABLE ROW LEVEL SECURITY")
|
||||||
|
op.execute("ALTER TABLE step_categories FORCE ROW LEVEL SECURITY")
|
||||||
|
op.execute(f"""
|
||||||
|
CREATE POLICY tenant_isolation ON step_categories
|
||||||
|
USING (
|
||||||
|
account_id = {_CURRENT_ACCOUNT}
|
||||||
|
OR account_id = '{_PLATFORM_UUID}'::uuid
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ── psa_connections ──────────────────────────────────────────────────────
|
||||||
|
# Tenant-only — PSA credentials must never cross tenant boundaries.
|
||||||
|
op.execute("ALTER TABLE psa_connections ENABLE ROW LEVEL SECURITY")
|
||||||
|
op.execute("ALTER TABLE psa_connections FORCE ROW LEVEL SECURITY")
|
||||||
|
op.execute(f"""
|
||||||
|
CREATE POLICY tenant_isolation ON psa_connections
|
||||||
|
USING (account_id = {_CURRENT_ACCOUNT})
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ── flow_proposals ────────────────────────────────────────────────────────
|
||||||
|
# Tenant-only.
|
||||||
|
op.execute("ALTER TABLE flow_proposals ENABLE ROW LEVEL SECURITY")
|
||||||
|
op.execute("ALTER TABLE flow_proposals FORCE ROW LEVEL SECURITY")
|
||||||
|
op.execute(f"""
|
||||||
|
CREATE POLICY tenant_isolation ON flow_proposals
|
||||||
|
USING (account_id = {_CURRENT_ACCOUNT})
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
for table in ["trees", "tree_tags", "tree_categories", "step_categories",
|
||||||
|
"psa_connections", "flow_proposals"]:
|
||||||
|
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")
|
||||||
Reference in New Issue
Block a user