Files
leocrm/alembic/versions/0111_enable_rls_remaining_tenant_tables.py
Agent Zero 04d6562f5b fix(security): Fix critical permission system issues
Problem 1: Remove legacy role bypass
- Remove role="admin" string bypass in permissions.py resolve_permissions()
- Remove role="admin"/"editor" bypass in auth.py check_permission()
- Remove legacy role string fallback in deps.py require_admin/require_write
- Add migration 0112: Create Role records for built-in roles and link role_id
- KI-Kommentar: Legacy Role Bypass entfernt — alle Admins müssen echte role_id haben

Problem 2: Enforce API token scopes
- Add _token_scopes check in require_permission() in deps.py
- When _token_scopes is set (API token auth), required permission must be in scopes
- When _token_scopes not set (session auth), normal permission check applies

Problem 3: Migration chain verification
- Chain is already linear: 0027→0028_rls_force→0028_user_preferences→0029
- user_preferences table confirmed exists in DB
- No duplicate revision IDs found

Problem 4: RLS for remaining tenant tables
- Add migration 0111: Dynamic RLS activation for any remaining tables with tenant_id
- Login tables and global tables explicitly excluded
- DB check shows 0 tables currently missing RLS (safety net migration)

Problem 5: Permission cache invalidation on tenant switch
- Add invalidate_permission_cache() call in switch_tenant() for old tenant
- Stale cached permissions from old tenant no longer leak

Problem 6+7: Guest system removal
- Remove get_current_guest() from deps.py
- Remove guest_auth.py router from main.py and routes/__init__.py
- Rewrite guests.py to use regular User/UserTenant with role=guest
- Remove GuestUser/GuestInvitation from models/__init__.py
- Add migration 0113: Migrate guest_users to regular users, drop guest tables
- Update frontend GuestLogin/GuestContacts to redirect to normal pages
- KI-Kommentar: Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest
2026-08-06 11:32:14 +02:00

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