RBAC + Per-Route Audit Events

This commit is contained in:
boris committed 2026-01-20 10:08:32 +00:00
1 parent 47b90fee87
commit 6901f6fcc4
18 files changed
+381 -122

No files matched your search

+22 -30
View File
@@ -9,11 +9,13 @@ from ninja import Query, Router, Schema
from ninja.errors import HttpError
from pydantic import EmailStr, Field
from apps.core.rbac import ROLE_ADMIN, ROLE_OPERATOR, ROLE_USER, get_user_role, require_roles, set_user_role
class UserCreateIn(Schema):
email: EmailStr
password: str = Field(min_length=8)
role: Literal["admin", "user"]
role: Literal["administrator", "operator", "auditor", "user", "admin"]
class UserListOut(Schema):
@@ -33,7 +35,7 @@ class UserDetailOut(Schema):
class UserUpdateIn(Schema):
email: EmailStr | None = None
password: str | None = Field(default=None, min_length=8)
role: Literal["admin", "user"] | None = None
role: Literal["administrator", "operator", "auditor", "user", "admin"] | None = None
is_active: bool | None = None
@@ -42,25 +44,8 @@ class UsersQuery(Schema):
offset: int = Field(default=0, ge=0)
def _require_admin(request: HttpRequest) -> None:
user = request.user
if not getattr(user, "is_authenticated", False):
raise HttpError(403, "Forbidden")
if not (user.is_staff or user.is_superuser):
raise HttpError(403, "Forbidden")
def _role_from_user(user) -> str:
return "admin" if (user.is_staff or user.is_superuser) else "user"
def _apply_role(user, role: str) -> None:
if role == "admin":
user.is_staff = True
user.is_superuser = True
else:
user.is_staff = False
user.is_superuser = False
return get_user_role(user) or ROLE_USER
def build_router() -> Router:
@@ -68,19 +53,23 @@ def build_router() -> Router:
@router.post("/", response=UserDetailOut)
def create_user(request: HttpRequest, payload: UserCreateIn):
"""Create a user with role and password (admin only)."""
_require_admin(request)
"""Create a user with role and password (admin or operator)."""
require_roles(request, ROLE_ADMIN, ROLE_OPERATOR)
User = get_user_model()
email = payload.email.strip().lower()
if User.objects.filter(email__iexact=email).exists():
raise HttpError(422, {"email": ["Email already exists."]})
user = User(username=email, email=email, is_active=True)
_apply_role(user, payload.role)
user.set_password(payload.password)
try:
user.save()
except IntegrityError:
raise HttpError(422, {"email": ["Email already exists."]})
try:
set_user_role(user, payload.role)
except ValueError:
raise HttpError(422, {"role": ["Invalid role."]})
user.save()
return {
"id": user.id,
"email": user.email,
@@ -90,8 +79,8 @@ def build_router() -> Router:
@router.get("/", response=List[UserListOut])
def list_users(request: HttpRequest, pagination: UsersQuery = Query(...)):
"""List users with pagination (admin only)."""
_require_admin(request)
"""List users with pagination (admin or operator)."""
require_roles(request, ROLE_ADMIN, ROLE_OPERATOR)
User = get_user_model()
qs = User.objects.order_by("id")[pagination.offset : pagination.offset + pagination.limit]
return [
@@ -106,8 +95,8 @@ def build_router() -> Router:
@router.get("/{user_id}", response=UserDetailOut)
def get_user(request: HttpRequest, user_id: int):
"""Get user details by id (admin only)."""
_require_admin(request)
"""Get user details by id (admin or operator)."""
require_roles(request, ROLE_ADMIN, ROLE_OPERATOR)
User = get_user_model()
try:
user = User.objects.get(id=user_id)
@@ -123,7 +112,7 @@ def build_router() -> Router:
@router.patch("/{user_id}", response=UserDetailOut)
def update_user(request: HttpRequest, user_id: int, payload: UserUpdateIn):
"""Update user fields such as role, email, or status (admin only)."""
_require_admin(request)
require_roles(request, ROLE_ADMIN)
if payload.email is None and payload.password is None and payload.role is None and payload.is_active is None:
raise HttpError(422, {"detail": "No fields provided."})
User = get_user_model()
@@ -140,7 +129,10 @@ def build_router() -> Router:
if payload.password is not None:
user.set_password(payload.password)
if payload.role is not None:
_apply_role(user, payload.role)
try:
set_user_role(user, payload.role)
except ValueError:
raise HttpError(422, {"role": ["Invalid role."]})
if payload.is_active is not None:
user.is_active = payload.is_active
user.save()
@@ -154,7 +146,7 @@ def build_router() -> Router:
@router.delete("/{user_id}", response={204: None})
def delete_user(request: HttpRequest, user_id: int):
"""Delete a user by id (admin only)."""
_require_admin(request)
require_roles(request, ROLE_ADMIN)
User = get_user_model()
try:
user = User.objects.get(id=user_id)