fix: remove platform_steps and template_trees from Phase 4 RLS
Both tables have no account_id column — they are globally readable by all authenticated users and must not have RLS policies. Also removes the corresponding test cases that assumed these tables had account_id-based policies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,8 @@ Skipped intentionally:
|
|||||||
- accounts — IS the root table; no account_id column
|
- accounts — IS the root table; no account_id column
|
||||||
- plan_feature_defaults — platform config; no account_id column
|
- plan_feature_defaults — platform config; no account_id column
|
||||||
- script_categories — global lookup table; no account_id column
|
- script_categories — global lookup table; no account_id column
|
||||||
(ScriptTemplate in the same file has account_id,
|
- platform_steps — global content; no account_id column (readable by all)
|
||||||
ScriptCategory does not)
|
- template_trees — global content; no account_id column (readable by all)
|
||||||
|
|
||||||
Revision ID: b3c7e9f2a1d8
|
Revision ID: b3c7e9f2a1d8
|
||||||
Revises: 172ad76d7d20
|
Revises: 172ad76d7d20
|
||||||
@@ -29,8 +29,6 @@ down_revision: Union[str, None] = "172ad76d7d20"
|
|||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
PLATFORM_ACCOUNT_ID = "00000000-0000-0000-0000-000000000001"
|
|
||||||
|
|
||||||
# Standard policy — tenant sees only own rows.
|
# Standard policy — tenant sees only own rows.
|
||||||
_STANDARD_TABLES = [
|
_STANDARD_TABLES = [
|
||||||
"users",
|
"users",
|
||||||
@@ -63,13 +61,6 @@ _STANDARD_TABLES = [
|
|||||||
"user_pinned_trees",
|
"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 = (
|
_POLICY_EXPR = (
|
||||||
"account_id = COALESCE("
|
"account_id = COALESCE("
|
||||||
"NULLIF(current_setting('app.current_account_id', TRUE), ''), "
|
"NULLIF(current_setting('app.current_account_id', TRUE), ''), "
|
||||||
@@ -79,7 +70,6 @@ _POLICY_EXPR = (
|
|||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Standard tables — tenant isolation only
|
|
||||||
for table in _STANDARD_TABLES:
|
for table in _STANDARD_TABLES:
|
||||||
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY")
|
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY")
|
||||||
op.execute(f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY")
|
op.execute(f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY")
|
||||||
@@ -88,20 +78,8 @@ def upgrade() -> None:
|
|||||||
USING ({_POLICY_EXPR})
|
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:
|
def downgrade() -> None:
|
||||||
for table in _STANDARD_TABLES + _PLATFORM_TABLES:
|
for table in _STANDARD_TABLES:
|
||||||
op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table}")
|
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} DISABLE ROW LEVEL SECURITY")
|
||||||
|
|||||||
@@ -958,8 +958,10 @@ async def test_tree_shares_account_a_cannot_see_account_b(admin_conn, conn_a):
|
|||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Phase 4 RLS isolation tests
|
# Phase 4 RLS isolation tests
|
||||||
# Tables: users, script_builder_sessions, ai_session_steps,
|
# Tables: users, script_builder_sessions, ai_session_steps, notifications
|
||||||
# notifications, platform_steps, template_trees
|
#
|
||||||
|
# Note: platform_steps and template_trees have no account_id column and no RLS —
|
||||||
|
# they are globally readable by all authenticated users.
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1083,58 +1085,3 @@ async def test_notifications_account_a_cannot_see_account_b(admin_conn, conn_a):
|
|||||||
finally:
|
finally:
|
||||||
await admin_conn.execute(f"DELETE FROM notifications WHERE id = '{notif_id}'")
|
await admin_conn.execute(f"DELETE FROM notifications WHERE id = '{notif_id}'")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# platform_steps — platform content visible to all tenants
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_platform_steps_visible_to_all_tenants(admin_conn, conn_a):
|
|
||||||
"""Platform steps (PLATFORM_ACCOUNT_ID) must be visible to any tenant."""
|
|
||||||
step_id = str(uuid.uuid4())
|
|
||||||
await admin_conn.execute(f"""
|
|
||||||
INSERT INTO platform_steps (
|
|
||||||
id, account_id, title, step_type, content,
|
|
||||||
is_active, created_at, updated_at
|
|
||||||
) VALUES (
|
|
||||||
'{step_id}', '{PLATFORM_ACCOUNT_ID}', 'Phase4 RLS Platform Step',
|
|
||||||
'action', '{{}}'::jsonb, TRUE, NOW(), NOW()
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
try:
|
|
||||||
rows = await conn_a.fetch(
|
|
||||||
f"SELECT id FROM platform_steps WHERE id = '{step_id}'"
|
|
||||||
)
|
|
||||||
assert len(rows) == 1, (
|
|
||||||
"Platform steps (PLATFORM_ACCOUNT_ID) must be visible to all tenants"
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
await admin_conn.execute(f"DELETE FROM platform_steps WHERE id = '{step_id}'")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# template_trees — platform content visible to all tenants
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_template_trees_visible_to_all_tenants(admin_conn, conn_a):
|
|
||||||
"""Template trees (PLATFORM_ACCOUNT_ID) must be visible to any tenant."""
|
|
||||||
tmpl_id = str(uuid.uuid4())
|
|
||||||
await admin_conn.execute(f"""
|
|
||||||
INSERT INTO template_trees (
|
|
||||||
id, account_id, name, tree_structure, is_active,
|
|
||||||
created_at, updated_at
|
|
||||||
) VALUES (
|
|
||||||
'{tmpl_id}', '{PLATFORM_ACCOUNT_ID}', 'Phase4 RLS Template',
|
|
||||||
'{{}}'::jsonb, TRUE, NOW(), NOW()
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
try:
|
|
||||||
rows = await conn_a.fetch(
|
|
||||||
f"SELECT id FROM template_trees WHERE id = '{tmpl_id}'"
|
|
||||||
)
|
|
||||||
assert len(rows) == 1, (
|
|
||||||
"Template trees (PLATFORM_ACCOUNT_ID) must be visible to all tenants"
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
await admin_conn.execute(f"DELETE FROM template_trees WHERE id = '{tmpl_id}'")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user