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:
+27
-60
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user