"""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")