fix(security): enable RLS for all tenant tables missing RLS (0108)
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
"""Enable RLS for all tenant tables that were added after migration 0085.
|
||||||
|
|
||||||
|
Migration 0085 activated Row Level Security only for tables that existed at the
|
||||||
|
time it ran. Plugin tables and other tables created by later migrations were
|
||||||
|
not covered, leaving ~84 tables with a ``tenant_id`` column but without RLS.
|
||||||
|
|
||||||
|
This migration dynamically discovers every table in the ``public`` schema that
|
||||||
|
has a ``tenant_id`` column but does **not** yet have RLS enabled, then:
|
||||||
|
|
||||||
|
1. Enables and forces RLS.
|
||||||
|
2. Drops any stale ``tenant_isolation`` / ``{table}_tenant_isolation`` policies.
|
||||||
|
3. Creates a fail-closed ``{table}_tenant_isolation`` policy scoped to
|
||||||
|
``crm_api`` and ``crm_worker``.
|
||||||
|
4. Grants CRUD to ``crm_api`` and ``crm_worker``.
|
||||||
|
5. Grants the appropriate permissions to ``crm_auth`` on login tables
|
||||||
|
(users, user_tenants, tenants, sessions, password_reset_tokens).
|
||||||
|
|
||||||
|
Global tables without tenant isolation requirements are skipped.
|
||||||
|
|
||||||
|
Revision ID: 0108
|
||||||
|
Revises: 0107
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "0108"
|
||||||
|
down_revision = "0107"
|
||||||
|
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 / identity tables that DO have tenant_id and need RLS, but also need
|
||||||
|
# crm_auth to keep the permissions defined in migration 0085.
|
||||||
|
AUTH_TABLES = {
|
||||||
|
"users": ["SELECT"],
|
||||||
|
"user_tenants": ["SELECT"],
|
||||||
|
"tenants": ["SELECT"],
|
||||||
|
"password_reset_tokens": ["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||||
|
"sessions": ["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _exec(sql: str) -> None:
|
||||||
|
op.execute(sql)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Dynamic discovery + RLS activation for every tenant table that #
|
||||||
|
# was created after migration 0085 and therefore lacks RLS. #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
_exec("""
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
policy_sql TEXT;
|
||||||
|
auth_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'
|
||||||
|
)
|
||||||
|
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);
|
||||||
|
|
||||||
|
-- Grant crm_auth permissions on login tables (matching 0085 AUTH_TABLES)
|
||||||
|
IF r.table_name = 'users' THEN
|
||||||
|
GRANT SELECT ON public.users TO crm_auth;
|
||||||
|
ELSIF r.table_name = 'user_tenants' THEN
|
||||||
|
GRANT SELECT ON public.user_tenants TO crm_auth;
|
||||||
|
ELSIF r.table_name = 'tenants' THEN
|
||||||
|
GRANT SELECT ON public.tenants TO crm_auth;
|
||||||
|
ELSIF r.table_name = 'password_reset_tokens' THEN
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.password_reset_tokens TO crm_auth;
|
||||||
|
ELSIF r.table_name = 'sessions' THEN
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON public.sessions TO crm_auth;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user