User: fix last-admin validation and forbid non-admin role_id change

- Last-admin only when target role is non-admin (admins may switch admin roles).
- Use Ash.Changeset.get_attribute for new role_id. Tests: admin role switch, non-admin update_user role_id forbidden.
This commit is contained in:
Moritz 2026-02-04 09:19:47 +01:00
parent dbd0a57292
commit 67ce514ba0
2 changed files with 75 additions and 23 deletions

View file

@ -391,38 +391,52 @@ defmodule Mv.Accounts.User do
end
end
# Last-admin: prevent the only admin from changing their role (at least one admin required).
# Last-admin: prevent the only admin from leaving the admin role (at least one admin required).
# Only block when the user is leaving admin (target role is not admin). Switching between
# two admin roles (e.g. "Admin" and "Superadmin" both with permission_set_name "admin") is allowed.
validate fn changeset, _context ->
if Ash.Changeset.changing_attribute?(changeset, :role_id) do
current_role_id = changeset.data.role_id
new_role_id = Ash.Changeset.get_attribute(changeset, :role_id)
current_role =
Mv.Authorization.Role
|> Ash.get!(current_role_id, authorize?: false)
if current_role.permission_set_name != "admin" do
if is_nil(new_role_id) do
:ok
else
admin_role_ids =
current_role_id = changeset.data.role_id
current_role =
Mv.Authorization.Role
|> Ash.Query.for_read(:read)
|> Ash.Query.filter(expr(permission_set_name == "admin"))
|> Ash.read!(authorize?: false)
|> Enum.map(& &1.id)
|> Ash.get!(current_role_id, authorize?: false)
# Count only non-system users with admin role (system user is for internal ops)
system_email = Mv.Helpers.SystemActor.system_user_email()
new_role =
Mv.Authorization.Role
|> Ash.get!(new_role_id, authorize?: false)
count =
Mv.Accounts.User
|> Ash.Query.for_read(:read)
|> Ash.Query.filter(expr(role_id in ^admin_role_ids))
|> Ash.Query.filter(expr(email != ^system_email))
|> Ash.count!(authorize?: false)
# Only block when current user is admin and target role is not admin (leaving admin)
if current_role.permission_set_name == "admin" and
new_role.permission_set_name != "admin" do
admin_role_ids =
Mv.Authorization.Role
|> Ash.Query.for_read(:read)
|> Ash.Query.filter(expr(permission_set_name == "admin"))
|> Ash.read!(authorize?: false)
|> Enum.map(& &1.id)
if count <= 1 do
{:error,
field: :role_id, message: "At least one user must keep the Admin role."}
# Count only non-system users with admin role (system user is for internal ops)
system_email = Mv.Helpers.SystemActor.system_user_email()
count =
Mv.Accounts.User
|> Ash.Query.for_read(:read)
|> Ash.Query.filter(expr(role_id in ^admin_role_ids))
|> Ash.Query.filter(expr(email != ^system_email))
|> Ash.count!(authorize?: false)
if count <= 1 do
{:error,
field: :role_id, message: "At least one user must keep the Admin role."}
else
:ok
end
else
:ok
end