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
This commit is contained in:
Agent Zero
2026-08-06 11:32:14 +02:00
parent 67015ef82b
commit 04d6562f5b
13 changed files with 635 additions and 632 deletions
@@ -0,0 +1,139 @@
"""Migrate guest_users to regular users with role='guest' and drop guest tables.
⚠️ Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest
This migration:
1. Creates User records for each guest (or links to existing users by email)
2. Creates UserTenant records with role='guest' and appropriate status
3. Drops guest_invitations and guest_users tables
After this migration, guests authenticate via the normal login flow and are
managed through the regular user system with role='guest' in user_tenants.
Revision ID: 0113
Revises: 0112
"""
from __future__ import annotations
from alembic import op
revision = "0113"
down_revision = "0112"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Migrate guest_users into users + user_tenants with role='guest'
op.execute("""
DO $$
DECLARE
guest_rec RECORD;
existing_user_id UUID;
new_user_id UUID;
mapped_status TEXT;
BEGIN
FOR guest_rec IN SELECT * FROM guest_users WHERE deleted_at IS NULL OR deleted_at IS NULL
LOOP
-- Map guest status to user_tenants status
mapped_status := CASE
WHEN guest_rec.status = 'active' THEN 'active'
WHEN guest_rec.status = 'invited' THEN 'invited'
WHEN guest_rec.status = 'expired' THEN 'disabled'
WHEN guest_rec.status = 'revoked' THEN 'disabled'
ELSE 'disabled'
END;
-- Check if a user with this email already exists
SELECT id INTO existing_user_id
FROM users
WHERE email = guest_rec.email
LIMIT 1;
IF existing_user_id IS NOT NULL THEN
-- User already exists — just create the tenant membership if missing
new_user_id := existing_user_id;
-- Check if user_tenants entry already exists for this user+tenant
IF NOT EXISTS (
SELECT 1 FROM user_tenants
WHERE user_id = new_user_id
AND tenant_id = guest_rec.tenant_id
) THEN
INSERT INTO user_tenants (user_id, tenant_id, is_default, role, status, created_at, updated_at)
VALUES (
new_user_id,
guest_rec.tenant_id,
false,
'guest',
mapped_status,
guest_rec.created_at,
guest_rec.updated_at
);
END IF;
ELSE
-- Create new user from guest record
INSERT INTO users (id, email, name, password_hash, is_active, preferences, is_system_admin, created_at, updated_at)
VALUES (
gen_random_uuid(),
guest_rec.email,
guest_rec.name,
COALESCE(guest_rec.password_hash, ''),
true,
'{}'::jsonb,
false,
guest_rec.created_at,
guest_rec.updated_at
)
RETURNING id INTO new_user_id;
-- Create user_tenants membership with guest role
INSERT INTO user_tenants (user_id, tenant_id, is_default, role, status, created_at, updated_at)
VALUES (
new_user_id,
guest_rec.tenant_id,
false,
'guest',
mapped_status,
guest_rec.created_at,
guest_rec.updated_at
);
END IF;
END LOOP;
END $$;
""")
# Drop guest tables
op.execute("DROP TABLE IF EXISTS guest_invitations CASCADE")
op.execute("DROP TABLE IF EXISTS guest_users CASCADE")
def downgrade() -> None:
# Recreate guest_users table (data is lost — this is a one-way migration)
op.execute("""
CREATE TABLE IF NOT EXISTS guest_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
password_hash VARCHAR(255),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
invited_by UUID REFERENCES users(id) ON DELETE SET NULL,
status VARCHAR(20) NOT NULL DEFAULT 'invited',
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
""")
op.execute("""
CREATE TABLE IF NOT EXISTS guest_invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
guest_user_id UUID NOT NULL REFERENCES guest_users(id) ON DELETE CASCADE,
token_hash VARCHAR(64) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
""")