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
+27 -60
View File
@@ -14,8 +14,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import get_settings
from app.core.auth import get_redis, get_session_data, refresh_session_ttl
from app.core.db import get_db, set_tenant_context, set_user_context
from app.models.guest_user import GuestUser
logger = logging.getLogger(__name__)
# Known write-permission modules — used by require_write() to check
@@ -43,39 +41,6 @@ async def get_redis_dep() -> aioredis.Redis:
return get_redis()
async def get_current_guest(
request: Request,
redis: aioredis.Redis = Depends(get_redis_dep),
) -> dict[str, Any]:
"""Get the current guest user from guest session cookie.
Returns session data dict with guest_user_id, tenant_id, email, name.
Used for guest-specific endpoints (guest login, guest contacts).
"""
session_id = request.cookies.get("guest_session")
if not session_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"detail": "Not authenticated", "code": "not_authenticated"},
)
import json
raw = await redis.get(f"guest_session:{session_id}")
if raw is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"detail": "Session expired or invalid", "code": "session_invalid"},
)
session_data = json.loads(raw)
# Extend TTL on each request (sliding session)
await redis.expire(f"guest_session:{session_id}", 1800)
return session_data
async def get_current_user(
request: Request,
db: AsyncSession = Depends(get_db),
@@ -231,23 +196,15 @@ async def get_current_user_or_bearer(
async def require_admin(
current_user: dict[str, Any] = Depends(get_current_user),
) -> dict[str, Any]:
"""Require admin role (legacy + new permission system).
"""Require admin access via is_system_admin or *:* permission.
Legacy role string 'admin' is deprecated — log a warning when used.
New system uses is_system_admin or *:* permission.
⚠️ Legacy Role Bypass entfernt — alle Admins müssen echte role_id haben
Legacy role string 'admin' no longer grants access. Users must have
is_system_admin=True or *:* permission through the RBAC system.
"""
if current_user.get("is_system_admin"):
return current_user
# Legacy role string fallback — deprecated
if current_user.get("role") == "admin":
logger.warning(
"Legacy role string 'admin' used for user=%s — deprecated, "
"migrate to is_system_admin or *:* permission",
current_user.get("user_id"),
)
return current_user
# New permission system check
from app.core.permissions import check_permission
@@ -263,24 +220,15 @@ async def require_admin(
async def require_write(
current_user: dict[str, Any] = Depends(get_current_user),
) -> dict[str, Any]:
"""Require write permission (admin, editor, or custom role with write perms).
"""Require write permission via is_system_admin or specific module:write permissions.
Legacy role strings 'admin'/'editor' are deprecated — log a warning when used.
New system checks specific module:write permissions instead of broad wildcards.
⚠️ Legacy Role Bypass entfernt — alle Admins müssen echte role_id haben
Legacy role strings 'admin'/'editor' no longer grant write access. Users must
have is_system_admin=True or specific module:write permissions through RBAC.
"""
if current_user.get("is_system_admin"):
return current_user
# Legacy role string fallback — deprecated
role = current_user.get("role", "viewer")
if role in ("admin", "editor"):
logger.warning(
"Legacy role string '%s' used for user=%s in require_write — deprecated, "
"migrate to specific module:write permissions",
role, current_user.get("user_id"),
)
return current_user
# Check via permission system for specific write permissions
from app.core.permissions import check_permission
@@ -297,12 +245,31 @@ async def require_write(
def require_permission(permission: str):
"""FastAPI dependency factory: require a specific permission.
Enforces API token scopes (Problem 2 fix): when the request is authenticated
via a Bearer API token, ``_token_scopes`` is set on the user context. The
required permission must be present in the scopes (wildcard match supported).
Session-auth requests (no ``_token_scopes``) use the normal permission check.
Usage:
@router.get("/contacts", dependencies=[Depends(require_permission("contacts:read"))])
"""
async def _check(
current_user: dict[str, Any] = Depends(get_current_user),
) -> dict[str, Any]:
# API token scope enforcement (Problem 2 fix)
token_scopes = current_user.get("_token_scopes")
if token_scopes is not None:
from app.core.permissions import _permission_matches_any
if not _permission_matches_any(set(token_scopes), permission):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
"detail": f"Token scope '{permission}' required",
"code": "insufficient_scope",
},
)
return current_user
if current_user.get("is_system_admin"):
return current_user
from app.core.permissions import check_permission