fix(users): admin can never strip their own admin permission
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

Both backend (PATCH /users/{id}) and frontend (Edit User modal) now block
an admin from changing their own primary role away from admin or removing
admin from their own extra_roles, closing off a self-lockout scenario.
This commit is contained in:
kitos
2026-07-22 10:52:46 +02:00
parent 66951086fb
commit 3cd0f6fd99
3 changed files with 77 additions and 16 deletions
+11
View File
@@ -17,6 +17,7 @@ from app.dependencies.auth import require_role, require_any_role_strict
# Import UnitOfWork from app.domain.unit_of_work
from app.domain.unit_of_work import UnitOfWork
from app.domain.errors import BusinessRuleViolation
# Import User from app.models.user
from app.models.user import User
@@ -269,6 +270,16 @@ def update_user_route(
"""Update one or more fields of an existing user. **Requires admin role.**."""
# Assign update_data = payload.model_dump(exclude_unset=True)
update_data = payload.model_dump(exclude_unset=True)
# An admin can never strip their own admin permission — otherwise a
# careless self-edit (or a compromised session) could lock every admin
# out of the platform with no way back in.
if user_id == current_user.id and current_user.role == "admin":
final_role = update_data.get("role", current_user.role)
final_extra_roles = update_data.get("extra_roles", current_user.extra_roles or [])
if "admin" not in {final_role, *(final_extra_roles or [])}:
raise BusinessRuleViolation("You cannot remove your own admin permission.")
# Open context manager
with UnitOfWork(db) as uow:
# Assign user = update_user(db, user_id, **update_data)
+36
View File
@@ -4,6 +4,42 @@ instead of ever mixing permissions from more than one.
"""
def test_admin_cannot_remove_own_admin_role(api, auth_headers, admin_user):
resp = api(
"patch", f"/api/v1/users/{admin_user.id}", auth_headers,
json={"role": "viewer"},
)
assert resp.status_code == 400
assert "admin" in resp.text.lower()
def test_admin_cannot_drop_admin_via_extra_roles_either(api, auth_headers, admin_user):
# Give themselves a second role first, then try to swap the primary
# role away from admin without admin anywhere in extra_roles.
resp = api(
"patch", f"/api/v1/users/{admin_user.id}", auth_headers,
json={"role": "red_lead", "extra_roles": ["viewer"]},
)
assert resp.status_code == 400
def test_admin_can_switch_primary_role_if_admin_kept_in_extra_roles(api, auth_headers, admin_user):
resp = api(
"patch", f"/api/v1/users/{admin_user.id}", auth_headers,
json={"role": "red_lead", "extra_roles": ["admin"]},
)
assert resp.status_code == 200, resp.text
def test_admin_can_edit_own_other_fields_without_touching_role(api, auth_headers, admin_user):
resp = api(
"patch", f"/api/v1/users/{admin_user.id}", auth_headers,
json={"full_name": "Renamed Admin"},
)
assert resp.status_code == 200, resp.text
assert resp.json()["role"] == "admin"
def test_admin_can_grant_extra_roles(api, auth_headers):
created = api(
"post", "/api/v1/users", auth_headers,