feat: granular RBAC system with user groups, deny-list, permission registry, system-admin, self-mod prevention

This commit is contained in:
Agent Zero
2026-07-15 21:59:45 +02:00
parent 905bc9b744
commit b490a62322
17 changed files with 1408 additions and 13 deletions
+17
View File
@@ -9,8 +9,10 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.audit import log_audit
from app.core.auth import get_redis
from app.core.db import get_db
from app.core.notifications import create_notification
from app.core.permissions import invalidate_permission_cache
from app.deps import get_current_user, require_admin
from app.schemas.user import UserCreate, UserUpdate
from app.services.user_service import user_service, _UNSET
@@ -143,6 +145,9 @@ async def update_user(
Uses ``model_fields_set`` to detect whether ``role_id`` was explicitly
present in the request body (even if sent as ``null``). This allows
the caller to clear the FK by sending ``role_id: null``.
Self-modification prevention: a user cannot change their own role,
is_active status, or system_admin flag.
"""
tenant_id = uuid.UUID(current_user["tenant_id"])
acting_user_id = uuid.UUID(current_user["user_id"])
@@ -153,6 +158,14 @@ async def update_user(
400, detail={"detail": "Invalid user_id", "code": "invalid_id"}
) from None
# Self-modification prevention: cannot change own role or active status
if uid == acting_user_id:
if body.role is not None or body.is_active is not None or "role_id" in body.model_fields_set:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"detail": "Cannot modify your own role or active status", "code": "self_modification_forbidden"},
)
# Determine if role_id was explicitly sent (Pydantic v2)
role_id_sent = "role_id" in body.model_fields_set
@@ -188,6 +201,10 @@ async def update_user(
await log_audit(db, tenant_id, acting_user_id, "update", "user", uid, changes=changes)
# Invalidate permission cache for the updated user
redis = get_redis()
await invalidate_permission_cache(redis, uid, tenant_id)
return {
"id": str(user.id),
"email": user.email,