91 lines
3.0 KiB
Python
91 lines
3.0 KiB
Python
"""Create PostgreSQL RLS policies for row-level security on contacts.
|
|
|
|
Revision ID: 0052
|
|
Revises: 0051
|
|
Create Date: 2026-07-29
|
|
|
|
This migration enables PostgreSQL Row-Level Security on the contacts table
|
|
and creates policies that enforce visibility based on:
|
|
1. System admin sees everything
|
|
2. Owner sees own rows
|
|
3. Tenant-owned (owner_id IS NULL) visible to all
|
|
4. Shared via entity_permissions
|
|
"""
|
|
|
|
from alembic import op
|
|
|
|
revision = "0052"
|
|
down_revision = "0051"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# Enable RLS on contacts table
|
|
op.execute("ALTER TABLE contacts ENABLE ROW LEVEL SECURITY")
|
|
|
|
# Policy: System admin sees everything
|
|
op.execute("""
|
|
CREATE POLICY contacts_admin_visible ON contacts
|
|
FOR ALL
|
|
USING (current_setting('app.is_system_admin', true) = 'true')
|
|
""")
|
|
|
|
# Policy: Owner sees own rows
|
|
op.execute("""
|
|
CREATE POLICY contacts_owner_visible ON contacts
|
|
FOR ALL
|
|
USING (
|
|
owner_id::text = current_setting('app.current_user_id', true)
|
|
)
|
|
""")
|
|
|
|
# Policy: Tenant-owned (owner_id IS NULL) visible to all in tenant
|
|
op.execute("""
|
|
CREATE POLICY contacts_tenant_owned_visible ON contacts
|
|
FOR ALL
|
|
USING (owner_id IS NULL)
|
|
""")
|
|
|
|
# Policy: Shared via entity_permissions
|
|
op.execute("""
|
|
CREATE POLICY contacts_shared_visible ON contacts
|
|
FOR ALL
|
|
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
|
|
))
|
|
)
|
|
)
|
|
)
|
|
""")
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP POLICY IF EXISTS contacts_shared_visible ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_tenant_owned_visible ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_owner_visible ON contacts")
|
|
op.execute("DROP POLICY IF EXISTS contacts_admin_visible ON contacts")
|
|
op.execute("ALTER TABLE contacts DISABLE ROW LEVEL SECURITY")
|