From c4f919f3a59485be4897a6dac737862080a73775 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 10 Apr 2026 04:02:10 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20migration=20=E2=80=94=20enable=20RLS=20?= =?UTF-8?q?on=20trees,=20tags,=20categories,=20psa=5Fconnections,=20flow?= =?UTF-8?q?=5Fproposals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../c5f48b9890f9_enable_rls_phase1.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 backend/alembic/versions/c5f48b9890f9_enable_rls_phase1.py diff --git a/backend/alembic/versions/c5f48b9890f9_enable_rls_phase1.py b/backend/alembic/versions/c5f48b9890f9_enable_rls_phase1.py new file mode 100644 index 00000000..333c5ca2 --- /dev/null +++ b/backend/alembic/versions/c5f48b9890f9_enable_rls_phase1.py @@ -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")