Files
leocrm/app/routes/guests.py
T
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

205 lines
6.3 KiB
Python

"""Guest management routes — invite, list, delete guests (admin only).
⚠️ Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest
Guests are now regular users with role='guest' in user_tenants. They authenticate
via the normal login flow and are managed through the standard user system.
This router provides admin endpoints for inviting and managing guest users.
"""
from __future__ import annotations
import hashlib
import secrets
import uuid
from datetime import UTC, datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import get_settings
from app.core.auth import get_redis, hash_password
from app.core.db import get_db
from app.deps import get_current_user, require_admin
from app.models.user import User, UserTenant
from app.models.tenant import Tenant
router = APIRouter(prefix="/api/v1/guests", tags=["guests"])
settings = get_settings()
def _hash_token(token: str) -> str:
"""Hash a token using SHA-256."""
return hashlib.sha256(token.encode()).hexdigest()
@router.post("/invite")
async def invite_guest(
request: Request,
body: dict,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin),
):
"""Invite a guest user. Admin only. Creates a regular user with role='guest'.
⚠️ Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest
"""
email = body.get("email", "")
name = body.get("name", "")
expires_in_hours = body.get("expires_in_hours", 72)
if not email or not name:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"detail": "Email and name required", "code": "missing_fields"},
)
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
# Check if user already exists by email
user_q = await db.execute(
select(User).where(User.email == email)
)
existing_user = user_q.scalar_one_or_none()
if existing_user:
# Check if already a member of this tenant
ut_q = await db.execute(
select(UserTenant).where(
UserTenant.user_id == existing_user.id,
UserTenant.tenant_id == tenant_id,
)
)
existing_ut = ut_q.scalar_one_or_none()
if existing_ut:
if existing_ut.status == "active" and existing_ut.role == "guest":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={"detail": "Guest already active", "code": "guest_exists"},
)
# Re-invite: update existing membership
existing_ut.role = "guest"
existing_ut.status = "invited"
await db.flush()
else:
# Create new tenant membership with guest role
ut = UserTenant(
user_id=existing_user.id,
tenant_id=tenant_id,
is_default=False,
role="guest",
status="invited",
)
db.add(ut)
await db.flush()
else:
# Create new user with a random password (will be set on acceptance)
raw_token = secrets.token_urlsafe(32)
new_user = User(
email=email,
name=name,
password_hash=hash_password(raw_token), # Temporary password
is_active=True,
)
db.add(new_user)
await db.flush()
# Create tenant membership with guest role
ut = UserTenant(
user_id=new_user.id,
tenant_id=tenant_id,
is_default=False,
role="guest",
status="invited",
)
db.add(ut)
await db.commit()
await db.refresh(new_user)
return {
"email": email,
"name": name,
"status": "invited",
"role": "guest",
"message": "Guest invited — they can now log in via the normal login flow",
}
@router.get("")
async def list_guests(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin),
):
"""List all guest users for the current tenant."""
tenant_id = uuid.UUID(current_user["tenant_id"])
# Query user_tenants with role='guest' and join users
result = await db.execute(
select(UserTenant, User)
.join(User, UserTenant.user_id == User.id)
.where(UserTenant.tenant_id == tenant_id)
.where(UserTenant.role == "guest")
.order_by(UserTenant.created_at.desc())
)
rows = result.all()
return [
{
"id": str(ut.user_id),
"email": user.email,
"name": user.name,
"status": ut.status,
"role": "guest",
"created_at": ut.created_at.isoformat() if ut.created_at else None,
}
for ut, user in rows
]
@router.delete("/{guest_id}")
async def delete_guest(
guest_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin),
):
"""Revoke a guest user's tenant membership and invalidate sessions."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
gid = uuid.UUID(guest_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"detail": "Invalid guest ID", "code": "invalid_id"},
)
# Find the user_tenants entry for this guest
ut_q = await db.execute(
select(UserTenant)
.where(UserTenant.user_id == gid)
.where(UserTenant.tenant_id == tenant_id)
.where(UserTenant.role == "guest")
)
ut = ut_q.scalar_one_or_none()
if not ut:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"detail": "Guest not found", "code": "not_found"},
)
# Revoke: set status to disabled
ut.status = "disabled"
await db.commit()
# Invalidate all active sessions for this user
redis = get_redis()
if redis:
try:
from app.core.auth import invalidate_all_user_sessions
await invalidate_all_user_sessions(redis, gid)
except Exception:
pass
return {"message": "Guest revoked, all sessions invalidated", "status": "disabled"}