203 lines
7.8 KiB
Python
203 lines
7.8 KiB
Python
"""Fix RLS policies on contacts — add tenant_id isolation.
|
|
|
|
Revision ID: 0060
|
|
Revises: 0059
|
|
Create Date: 2026-07-29
|
|
|
|
This migration drops the insecure contact RLS policies (created in 0052)
|
|
and recreates them with proper tenant_id isolation.
|
|
|
|
Problems fixed:
|
|
1. contacts_tenant_owned_visible had USING (owner_id IS NULL) without tenant_id check
|
|
2. contacts_admin_visible had no tenant_id check
|
|
3. contacts_owner_visible had no tenant_id check
|
|
4. All policies used FOR ALL instead of separate SELECT/INSERT/UPDATE/DELETE
|
|
5. No WITH CHECK on write operations
|
|
"""
|
|
|
|
from alembic import op
|
|
|
|
revision = "0060"
|
|
down_revision = "0059"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# Drop all existing contact policies
|
|
op.execute("DROP POLICY IF EXISTS contacts_admin_visible ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_owner_visible ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_tenant_owned_visible ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_shared_visible ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS tenant_isolation ON contacts")
|
|
|
|
# ── Restrive policy: Tenant isolation (always enforced) ──
|
|
# This is the base policy that ALL other permissive policies are ANDed with
|
|
op.execute("""
|
|
CREATE POLICY contacts_tenant_isolation ON contacts
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid)
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid)
|
|
""")
|
|
|
|
# ── Permissive policies for SELECT (visibility) ──
|
|
|
|
# System admin sees everything (within tenant)
|
|
op.execute("""
|
|
CREATE POLICY contacts_admin_select ON contacts
|
|
FOR SELECT
|
|
USING (
|
|
current_setting('app.is_system_admin', true) = 'true'
|
|
AND tenant_id = current_setting('app.current_tenant_id', true)::uuid
|
|
)
|
|
""")
|
|
|
|
# Owner sees own rows (within tenant)
|
|
op.execute("""
|
|
CREATE POLICY contacts_owner_select ON contacts
|
|
FOR SELECT
|
|
USING (
|
|
owner_id::text = current_setting('app.current_user_id', true)
|
|
AND tenant_id = current_setting('app.current_tenant_id', true)::uuid
|
|
)
|
|
""")
|
|
|
|
# Tenant-owned (owner_id IS NULL) visible to all in tenant
|
|
op.execute("""
|
|
CREATE POLICY contacts_tenant_owned_select ON contacts
|
|
FOR SELECT
|
|
USING (
|
|
owner_id IS NULL
|
|
AND tenant_id = current_setting('app.current_tenant_id', true)::uuid
|
|
)
|
|
""")
|
|
|
|
# Shared via entity_permissions (within tenant)
|
|
op.execute("""
|
|
CREATE POLICY contacts_shared_select ON contacts
|
|
FOR SELECT
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM entity_permissions ep
|
|
WHERE ep.entity_type = 'contact'
|
|
AND ep.entity_id = contacts.id
|
|
AND ep.tenant_id = contacts.tenant_id
|
|
AND ep.permission_level != 'none'
|
|
AND (
|
|
ep.expires_at IS NULL OR ep.expires_at > NOW()
|
|
)
|
|
AND (
|
|
(ep.principal_type = 'user'
|
|
AND ep.principal_id::text = current_setting('app.current_user_id', true))
|
|
OR
|
|
(ep.principal_type = 'group'
|
|
AND ep.principal_id::text = ANY(
|
|
string_to_array(current_setting('app.current_user_groups', true), ',')
|
|
))
|
|
OR
|
|
(ep.principal_type = 'role'
|
|
AND ep.principal_id IN (
|
|
SELECT ut.role_id FROM user_tenants ut
|
|
WHERE ut.user_id::text = current_setting('app.current_user_id', true)
|
|
AND ut.tenant_id = contacts.tenant_id
|
|
))
|
|
)
|
|
)
|
|
AND tenant_id = current_setting('app.current_tenant_id', true)::uuid
|
|
)
|
|
""")
|
|
|
|
# ── Permissive policies for INSERT ──
|
|
op.execute("""
|
|
CREATE POLICY contacts_insert_policy ON contacts
|
|
FOR INSERT
|
|
WITH CHECK (
|
|
tenant_id = current_setting('app.current_tenant_id', true)::uuid
|
|
AND (
|
|
current_setting('app.is_system_admin', true) = 'true'
|
|
OR owner_id::text = current_setting('app.current_user_id', true)
|
|
OR owner_id IS NULL
|
|
)
|
|
)
|
|
""")
|
|
|
|
# ── Permissive policies for UPDATE ──
|
|
op.execute("""
|
|
CREATE POLICY contacts_update_policy ON contacts
|
|
FOR UPDATE
|
|
USING (
|
|
tenant_id = current_setting('app.current_tenant_id', true)::uuid
|
|
AND (
|
|
current_setting('app.is_system_admin', true) = 'true'
|
|
OR owner_id::text = current_setting('app.current_user_id', true)
|
|
OR owner_id IS NULL
|
|
OR EXISTS (
|
|
SELECT 1 FROM entity_permissions ep
|
|
WHERE ep.entity_type = 'contact'
|
|
AND ep.entity_id = contacts.id
|
|
AND ep.tenant_id = contacts.tenant_id
|
|
AND ep.permission_level IN ('write', 'admin', 'delete')
|
|
AND (
|
|
ep.expires_at IS NULL OR ep.expires_at > NOW()
|
|
)
|
|
AND (
|
|
(ep.principal_type = 'user'
|
|
AND ep.principal_id::text = current_setting('app.current_user_id', true))
|
|
OR
|
|
(ep.principal_type = 'group'
|
|
AND ep.principal_id::text = ANY(
|
|
string_to_array(current_setting('app.current_user_groups', true), ',')
|
|
))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
WITH CHECK (
|
|
tenant_id = current_setting('app.current_tenant_id', true)::uuid
|
|
)
|
|
""")
|
|
|
|
# ── Permissive policies for DELETE ──
|
|
op.execute("""
|
|
CREATE POLICY contacts_delete_policy ON contacts
|
|
FOR DELETE
|
|
USING (
|
|
tenant_id = current_setting('app.current_tenant_id', true)::uuid
|
|
AND (
|
|
current_setting('app.is_system_admin', true) = 'true'
|
|
OR owner_id::text = current_setting('app.current_user_id', true)
|
|
OR EXISTS (
|
|
SELECT 1 FROM entity_permissions ep
|
|
WHERE ep.entity_type = 'contact'
|
|
AND ep.entity_id = contacts.id
|
|
AND ep.tenant_id = contacts.tenant_id
|
|
AND ep.permission_level IN ('admin', 'delete')
|
|
AND (
|
|
ep.expires_at IS NULL OR ep.expires_at > NOW()
|
|
)
|
|
AND (
|
|
(ep.principal_type = 'user'
|
|
AND ep.principal_id::text = current_setting('app.current_user_id', true))
|
|
OR
|
|
(ep.principal_type = 'group'
|
|
AND ep.principal_id::text = ANY(
|
|
string_to_array(current_setting('app.current_user_groups', true), ',')
|
|
))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
""")
|
|
|
|
|
|
def downgrade() -> None:
|
|
# Drop the new secure policies
|
|
op.execute("DROP POLICY IF EXISTS contacts_tenant_isolation ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_admin_select ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_owner_select ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_tenant_owned_select ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_shared_select ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_insert_policy ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_update_policy ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_delete_policy ON contacts")
|