75 lines
2.4 KiB
Elixir
75 lines
2.4 KiB
Elixir
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.
|
|
|
|
Missing actor is not allowed; the system actor counts as admin (via `Actor.admin?/1`).
|
|
"""
|
|
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 or the linked user can change the email for members linked to users"
|
|
)
|
|
|
|
{:error, field: :email, message: msg}
|
|
end
|
|
end
|
|
end
|
|
|
|
# Ash stores actor in changeset.context.private.actor; validation context has .actor; some callsites use context.actor
|
|
defp resolve_actor(changeset, context) do
|
|
ctx = changeset.context || %{}
|
|
|
|
get_in(ctx, [:private, :actor]) ||
|
|
Map.get(ctx, :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
|