Add email-change permission validation for linked members
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Only admins or the linked user may change a linked member's email. - New validation EmailChangePermission (uses Actor.admin?, Loader.get_linked_user). - Register on Member update_member; docs and gettext.
This commit is contained in:
parent
ad02f8914f
commit
4ea31f0f37
7 changed files with 324 additions and 28 deletions
|
|
@ -0,0 +1,69 @@
|
|||
defmodule Mv.Membership.Member.Validations.EmailChangePermission do
|
||||
@moduledoc """
|
||||
Validates that only admins or the linked user may change a linked member's email.
|
||||
|
||||
This validation runs on member update when the email attribute is changing.
|
||||
It allows the change only if:
|
||||
- The member is not linked to a user, or
|
||||
- The actor has the admin permission set (via `Mv.Authorization.Actor.admin?/1`), or
|
||||
- The actor is the user linked to this member (actor.member_id == member.id).
|
||||
|
||||
This prevents non-admins from changing another user's linked member email,
|
||||
which would sync to that user's account and break email synchronization.
|
||||
|
||||
No system-actor fallback: missing actor is treated as not allowed.
|
||||
"""
|
||||
use Ash.Resource.Validation
|
||||
use Gettext, backend: MvWeb.Gettext, otp_app: :mv
|
||||
|
||||
alias Mv.Authorization.Actor
|
||||
alias Mv.EmailSync.Loader
|
||||
|
||||
@doc """
|
||||
Validates that the actor may change the member's email when the member is linked.
|
||||
|
||||
Only runs when the email attribute is changing (checked inside). Skips when
|
||||
member is not linked. Allows when actor is admin or owns the linked member.
|
||||
"""
|
||||
@impl true
|
||||
def validate(changeset, _opts, context) do
|
||||
if Ash.Changeset.changing_attribute?(changeset, :email) do
|
||||
validate_linked_member_email_change(changeset, context)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_linked_member_email_change(changeset, context) do
|
||||
linked_user = Loader.get_linked_user(changeset.data)
|
||||
|
||||
if is_nil(linked_user) do
|
||||
:ok
|
||||
else
|
||||
actor = resolve_actor(changeset, context)
|
||||
member_id = changeset.data.id
|
||||
|
||||
if Actor.admin?(actor) or actor_owns_member?(actor, member_id) do
|
||||
:ok
|
||||
else
|
||||
msg =
|
||||
dgettext("default", "Only administrators can change email for members linked to users")
|
||||
|
||||
{:error, field: :email, message: msg}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Ash stores actor in changeset.context.private.actor; validation context also has .actor
|
||||
defp resolve_actor(changeset, context) do
|
||||
get_in(changeset.context || %{}, [:private, :actor]) ||
|
||||
(context && Map.get(context, :actor))
|
||||
end
|
||||
|
||||
defp actor_owns_member?(nil, _member_id), do: false
|
||||
|
||||
defp actor_owns_member?(actor, member_id) do
|
||||
actor_member_id = Map.get(actor, :member_id) || Map.get(actor, "member_id")
|
||||
actor_member_id == member_id
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue