04d6562f5b
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
201 lines
6.7 KiB
Python
201 lines
6.7 KiB
Python
"""Migrate legacy role strings (admin/editor/viewer) to real Role records with role_id.
|
|
|
|
⚠️ Legacy Role Bypass entfernt — alle Admins müssen echte role_id haben
|
|
|
|
This migration creates Role records for each tenant's built-in roles (admin, editor,
|
|
viewer) and links UserTenant.role_id to the corresponding Role. After this migration,
|
|
the legacy role string on UserTenant.role is no longer used for permission checks —
|
|
all permissions come through the Role-based RBAC system.
|
|
|
|
Revision ID: 0112
|
|
Revises: 0111
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
|
|
|
|
revision = "0112"
|
|
down_revision = "0111"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
# Permission sets for built-in roles
|
|
ADMIN_PERMISSIONS = ["*:*"]
|
|
|
|
EDITOR_PERMISSIONS = [
|
|
"contacts:read", "contacts:write",
|
|
"users:read", "roles:read", "audit:read",
|
|
"attachments:read", "attachments:write",
|
|
"workflows:read", "workflows:write",
|
|
"sequences:read", "sequences:write",
|
|
"addresses:read", "addresses:write",
|
|
"taxes:read", "taxes:write",
|
|
"currencies:read", "currencies:write",
|
|
"notifications:read", "notifications:write",
|
|
"import_export:read", "import_export:write",
|
|
"user_preferences:read", "user_preferences:write",
|
|
]
|
|
|
|
VIEWER_PERMISSIONS = [
|
|
"contacts:read", "users:read", "roles:read",
|
|
"audit:read", "attachments:read", "workflows:read",
|
|
"sequences:read", "addresses:read", "taxes:read",
|
|
"currencies:read", "notifications:read",
|
|
"import_export:read",
|
|
"user_preferences:read", "user_preferences:write",
|
|
]
|
|
|
|
GUEST_PERMISSIONS = [
|
|
"contacts:read",
|
|
"attachments:read",
|
|
"user_preferences:read",
|
|
]
|
|
|
|
|
|
def upgrade() -> None:
|
|
# For each tenant, create Role records for built-in roles and link UserTenant.role_id
|
|
op.execute("""
|
|
DO $$
|
|
DECLARE
|
|
tenant_rec RECORD;
|
|
admin_role_id UUID;
|
|
editor_role_id UUID;
|
|
viewer_role_id UUID;
|
|
guest_role_id UUID;
|
|
BEGIN
|
|
FOR tenant_rec IN SELECT id FROM tenants WHERE deleted_at IS NULL
|
|
LOOP
|
|
-- Create or find admin role for this tenant
|
|
SELECT id INTO admin_role_id
|
|
FROM roles
|
|
WHERE tenant_id = tenant_rec.id
|
|
AND name = 'admin'
|
|
AND deleted_at IS NULL
|
|
LIMIT 1;
|
|
|
|
IF admin_role_id IS NULL THEN
|
|
INSERT INTO roles (id, tenant_id, name, permissions, denied_permissions, field_permissions, permission_version, created_at)
|
|
VALUES (
|
|
gen_random_uuid(),
|
|
tenant_rec.id,
|
|
'admin',
|
|
'["*:*"]'::jsonb,
|
|
'[]'::jsonb,
|
|
'{}'::jsonb,
|
|
1,
|
|
now()
|
|
)
|
|
RETURNING id INTO admin_role_id;
|
|
END IF;
|
|
|
|
-- Create or find editor role for this tenant
|
|
SELECT id INTO editor_role_id
|
|
FROM roles
|
|
WHERE tenant_id = tenant_rec.id
|
|
AND name = 'editor'
|
|
AND deleted_at IS NULL
|
|
LIMIT 1;
|
|
|
|
IF editor_role_id IS NULL THEN
|
|
INSERT INTO roles (id, tenant_id, name, permissions, denied_permissions, field_permissions, permission_version, created_at)
|
|
VALUES (
|
|
gen_random_uuid(),
|
|
tenant_rec.id,
|
|
'editor',
|
|
'["contacts:read","contacts:write","users:read","roles:read","audit:read","attachments:read","attachments:write","workflows:read","workflows:write","sequences:read","sequences:write","addresses:read","addresses:write","taxes:read","taxes:write","currencies:read","currencies:write","notifications:read","notifications:write","import_export:read","import_export:write","user_preferences:read","user_preferences:write"]'::jsonb,
|
|
'[]'::jsonb,
|
|
'{}'::jsonb,
|
|
1,
|
|
now()
|
|
)
|
|
RETURNING id INTO editor_role_id;
|
|
END IF;
|
|
|
|
-- Create or find viewer role for this tenant
|
|
SELECT id INTO viewer_role_id
|
|
FROM roles
|
|
WHERE tenant_id = tenant_rec.id
|
|
AND name = 'viewer'
|
|
AND deleted_at IS NULL
|
|
LIMIT 1;
|
|
|
|
IF viewer_role_id IS NULL THEN
|
|
INSERT INTO roles (id, tenant_id, name, permissions, denied_permissions, field_permissions, permission_version, created_at)
|
|
VALUES (
|
|
gen_random_uuid(),
|
|
tenant_rec.id,
|
|
'viewer',
|
|
'["contacts:read","users:read","roles:read","audit:read","attachments:read","workflows:read","sequences:read","addresses:read","taxes:read","currencies:read","notifications:read","import_export:read","user_preferences:read","user_preferences:write"]'::jsonb,
|
|
'[]'::jsonb,
|
|
'{}'::jsonb,
|
|
1,
|
|
now()
|
|
)
|
|
RETURNING id INTO viewer_role_id;
|
|
END IF;
|
|
|
|
-- Create or find guest role for this tenant
|
|
SELECT id INTO guest_role_id
|
|
FROM roles
|
|
WHERE tenant_id = tenant_rec.id
|
|
AND name = 'guest'
|
|
AND deleted_at IS NULL
|
|
LIMIT 1;
|
|
|
|
IF guest_role_id IS NULL THEN
|
|
INSERT INTO roles (id, tenant_id, name, permissions, denied_permissions, field_permissions, permission_version, created_at)
|
|
VALUES (
|
|
gen_random_uuid(),
|
|
tenant_rec.id,
|
|
'guest',
|
|
'["contacts:read","attachments:read","user_preferences:read"]'::jsonb,
|
|
'[]'::jsonb,
|
|
'{}'::jsonb,
|
|
1,
|
|
now()
|
|
)
|
|
RETURNING id INTO guest_role_id;
|
|
END IF;
|
|
|
|
-- Link UserTenant records to the appropriate Role based on legacy role string
|
|
UPDATE user_tenants SET role_id = admin_role_id
|
|
WHERE tenant_id = tenant_rec.id
|
|
AND role = 'admin'
|
|
AND role_id IS NULL;
|
|
|
|
UPDATE user_tenants SET role_id = editor_role_id
|
|
WHERE tenant_id = tenant_rec.id
|
|
AND role = 'editor'
|
|
AND role_id IS NULL;
|
|
|
|
UPDATE user_tenants SET role_id = viewer_role_id
|
|
WHERE tenant_id = tenant_rec.id
|
|
AND role = 'viewer'
|
|
AND role_id IS NULL;
|
|
|
|
UPDATE user_tenants SET role_id = guest_role_id
|
|
WHERE tenant_id = tenant_rec.id
|
|
AND role = 'guest'
|
|
AND role_id IS NULL;
|
|
END LOOP;
|
|
END $$;
|
|
""")
|
|
|
|
|
|
def downgrade() -> None:
|
|
# Unlink role_id for built-in role mappings (keep the Role records)
|
|
op.execute("""
|
|
UPDATE user_tenants SET role_id = NULL
|
|
WHERE role IN ('admin', 'editor', 'viewer', 'guest')
|
|
AND role_id IS NOT NULL
|
|
AND EXISTS (
|
|
SELECT 1 FROM roles r
|
|
WHERE r.id = user_tenants.role_id
|
|
AND r.name IN ('admin', 'editor', 'viewer', 'guest')
|
|
);
|
|
""")
|