P0+P1 fixes: RCE sandbox, SQL injection, RLS tenant isolation, DB roles, test syntax, attachment, permission registry, membership check
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
"""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")
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Fix DB roles — add default privileges and grants for all tables.
|
||||
|
||||
Revision ID: 0061
|
||||
Revises: 0060
|
||||
Create Date: 2026-07-29
|
||||
|
||||
Problems fixed:
|
||||
1. crm_runtime role has no grants on tables created after migration 0044
|
||||
2. No ALTER DEFAULT PRIVILEGES for future tables
|
||||
3. Auth tables (users, tenants, user_tenants, user_groups) need SELECT grants
|
||||
4. New permission/guest/policy tables need grants
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "0061"
|
||||
down_revision = "0060"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Grant privileges on all existing tables to crm_runtime
|
||||
op.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO crm_runtime")
|
||||
|
||||
# Grant USAGE on sequences
|
||||
op.execute("GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO crm_runtime")
|
||||
|
||||
# Default privileges for future tables created by migration owner
|
||||
op.execute("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO crm_runtime")
|
||||
op.execute("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO crm_runtime")
|
||||
|
||||
# Ensure RLS is enabled on all tenant tables that have tenant_id
|
||||
# (covers tables created after migration 0044 that missed RLS)
|
||||
tenant_tables = [
|
||||
"entity_permissions",
|
||||
"entity_policies",
|
||||
"permission_templates",
|
||||
"permission_delegations",
|
||||
"guest_users",
|
||||
"guest_invitations",
|
||||
"contact_folder_permissions",
|
||||
]
|
||||
for table in tenant_tables:
|
||||
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY")
|
||||
# Create tenant isolation policy if not exists
|
||||
op.execute(f"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_policy
|
||||
WHERE polname = '{table}_tenant_isolation'
|
||||
AND polrelid = '{table}'::regclass
|
||||
) THEN
|
||||
CREATE POLICY {table}_tenant_isolation ON {table}
|
||||
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);
|
||||
END IF;
|
||||
END $$;
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Revoke default privileges
|
||||
op.execute("ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM crm_runtime")
|
||||
op.execute("ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE USAGE, SELECT ON SEQUENCES FROM crm_runtime")
|
||||
Reference in New Issue
Block a user