From 14fa87364072475d313758660753d9d31ed26a1e Mon Sep 17 00:00:00 2001 From: Moritz Date: Fri, 30 Jan 2026 11:13:23 +0100 Subject: [PATCH] Restrict User.update_user to admin; allow :update for email only - Add ActorIsAdmin policy check (admin permission set only) - User: policy action(:update_user) forbid_unless + authorize_if ActorIsAdmin - User: primary :update action accept [:email] for non-admin profile edit --- lib/accounts/user.ex | 9 ++++++++ lib/mv/authorization/checks/actor_is_admin.ex | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 lib/mv/authorization/checks/actor_is_admin.ex diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index 4015aaa..f792973 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -103,6 +103,7 @@ defmodule Mv.Accounts.User do # the specialized :update_user action below. update :update do primary? true + accept [:email] # Required because custom validation functions (email validation, member relationship validation) # cannot be executed atomically. These validations need to query the database and perform @@ -310,6 +311,14 @@ defmodule Mv.Accounts.User do authorize_if expr(id == ^actor(:id)) end + # update_user allows :member argument (link/unlink). Only admins may use it to prevent + # privilege escalation (own_data could otherwise link to any member and get :linked scope). + policy action(:update_user) do + description "Only admins can update user with member link/unlink" + forbid_unless Mv.Authorization.Checks.ActorIsAdmin + authorize_if Mv.Authorization.Checks.ActorIsAdmin + end + # UPDATE/DESTROY via HasPermission (evaluates PermissionSets scope) policy action_type([:read, :create, :update, :destroy]) do description "Check permissions from user's role and permission set" diff --git a/lib/mv/authorization/checks/actor_is_admin.ex b/lib/mv/authorization/checks/actor_is_admin.ex new file mode 100644 index 0000000..2328876 --- /dev/null +++ b/lib/mv/authorization/checks/actor_is_admin.ex @@ -0,0 +1,22 @@ +defmodule Mv.Authorization.Checks.ActorIsAdmin do + @moduledoc """ + Policy check: true when the actor's role has permission_set_name "admin". + + Used to restrict actions (e.g. User.update_user for member link/unlink) to admins only. + """ + use Ash.Policy.SimpleCheck + + @impl true + def describe(_opts), do: "actor has admin permission set" + + @impl true + def match?(nil, _context, _opts), do: false + + def match?(actor, _context, _opts) do + ps_name = + get_in(actor, [Access.key(:role), Access.key(:permission_set_name)]) || + get_in(actor, [Access.key("role"), Access.key("permission_set_name")]) + + ps_name == "admin" + end +end