125 lines
3.7 KiB
Python
125 lines
3.7 KiB
Python
|
|
"""Enable RLS for all remaining tenant tables that still lack RLS after 0108/0109.
|
||
|
|
|
||
|
|
Migration 0108 dynamically discovered tables with tenant_id and enabled RLS.
|
||
|
|
However, new tables may have been added since, or some were missed.
|
||
|
|
|
||
|
|
This migration re-runs the same dynamic discovery to catch any stragglers.
|
||
|
|
|
||
|
|
Login tables (users, user_tenants, tenants, sessions, password_reset_tokens,
|
||
|
|
roles, permissions) are explicitly excluded — they must NOT have RLS.
|
||
|
|
|
||
|
|
Global tables (alembic_version, plugin_migrations, marketplace_listings,
|
||
|
|
sequences, notification_types) are also excluded.
|
||
|
|
|
||
|
|
Revision ID: 0111
|
||
|
|
Revises: 0110
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from alembic import op
|
||
|
|
|
||
|
|
revision = "0111"
|
||
|
|
down_revision = "0110"
|
||
|
|
branch_labels = None
|
||
|
|
depends_on = None
|
||
|
|
|
||
|
|
|
||
|
|
# Tables that must never get RLS (global / cross-tenant infrastructure)
|
||
|
|
GLOBAL_TABLES = [
|
||
|
|
"alembic_version",
|
||
|
|
"plugin_migrations",
|
||
|
|
"marketplace_listings",
|
||
|
|
"sequences",
|
||
|
|
"notification_types",
|
||
|
|
]
|
||
|
|
|
||
|
|
# Login-related tables — RLS blocks crm_auth during login flow
|
||
|
|
# See 0108 and 0109 for detailed explanation
|
||
|
|
LOGIN_TABLES = [
|
||
|
|
"users",
|
||
|
|
"user_tenants",
|
||
|
|
"tenants",
|
||
|
|
"sessions",
|
||
|
|
"password_reset_tokens",
|
||
|
|
"roles",
|
||
|
|
"permissions",
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def _exec(sql: str) -> None:
|
||
|
|
op.execute(sql)
|
||
|
|
|
||
|
|
|
||
|
|
def upgrade() -> None:
|
||
|
|
# Dynamic discovery + RLS activation for any tenant table still missing RLS
|
||
|
|
_exec("""
|
||
|
|
DO $$
|
||
|
|
DECLARE
|
||
|
|
r RECORD;
|
||
|
|
policy_sql TEXT;
|
||
|
|
BEGIN
|
||
|
|
FOR r IN
|
||
|
|
SELECT t.table_name
|
||
|
|
FROM information_schema.tables t
|
||
|
|
JOIN information_schema.columns c
|
||
|
|
ON c.table_schema = t.table_schema
|
||
|
|
AND c.table_name = t.table_name
|
||
|
|
AND c.column_name = 'tenant_id'
|
||
|
|
WHERE t.table_schema = 'public'
|
||
|
|
AND t.table_type = 'BASE TABLE'
|
||
|
|
AND t.table_name NOT IN (
|
||
|
|
'alembic_version',
|
||
|
|
'plugin_migrations',
|
||
|
|
'marketplace_listings',
|
||
|
|
'sequences',
|
||
|
|
'notification_types',
|
||
|
|
-- Login tables must NOT have RLS
|
||
|
|
'users',
|
||
|
|
'user_tenants',
|
||
|
|
'tenants',
|
||
|
|
'sessions',
|
||
|
|
'password_reset_tokens',
|
||
|
|
'roles',
|
||
|
|
'permissions'
|
||
|
|
)
|
||
|
|
AND NOT EXISTS (
|
||
|
|
SELECT 1
|
||
|
|
FROM pg_class pc
|
||
|
|
JOIN pg_namespace pn ON pn.oid = pc.relnamespace
|
||
|
|
WHERE pn.nspname = 'public'
|
||
|
|
AND pc.relname = t.table_name
|
||
|
|
AND pc.relrowsecurity = true
|
||
|
|
)
|
||
|
|
LOOP
|
||
|
|
-- Enable + force RLS
|
||
|
|
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', r.table_name);
|
||
|
|
EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', r.table_name);
|
||
|
|
|
||
|
|
-- Drop stale policies (idempotent)
|
||
|
|
EXECUTE format('DROP POLICY IF EXISTS tenant_isolation ON public.%I', r.table_name);
|
||
|
|
EXECUTE format('DROP POLICY IF EXISTS %s_tenant_isolation ON public.%I', r.table_name, r.table_name);
|
||
|
|
|
||
|
|
-- Create fail-closed policy
|
||
|
|
policy_sql := format(
|
||
|
|
'CREATE POLICY %s_tenant_isolation '
|
||
|
|
'ON public.%I '
|
||
|
|
'FOR ALL '
|
||
|
|
'TO crm_api, crm_worker '
|
||
|
|
'USING (tenant_id = NULLIF(current_setting(''app.current_tenant_id'', true), '''')::uuid) '
|
||
|
|
'WITH CHECK (tenant_id = NULLIF(current_setting(''app.current_tenant_id'', true), '''')::uuid)',
|
||
|
|
r.table_name, r.table_name
|
||
|
|
);
|
||
|
|
EXECUTE policy_sql;
|
||
|
|
|
||
|
|
-- Grant CRUD to crm_api and crm_worker
|
||
|
|
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO crm_api', r.table_name);
|
||
|
|
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO crm_worker', r.table_name);
|
||
|
|
END LOOP;
|
||
|
|
END $$;
|
||
|
|
""")
|
||
|
|
|
||
|
|
|
||
|
|
def downgrade() -> None:
|
||
|
|
pass
|