Use system actor for email uniqueness validation

Update email validation modules to use system actor for queries.
This ensures data integrity checks always run regardless of user permissions.
This commit is contained in:
Moritz 2026-01-20 22:09:19 +01:00 committed by Simon
parent 8f06442de9
commit 564e35f65e
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
2 changed files with 19 additions and 8 deletions

View file

@ -73,12 +73,18 @@ defmodule Mv.Accounts.User.Validations.EmailNotUsedByOtherMember do
end end
defp check_email_uniqueness(email, exclude_member_id) do defp check_email_uniqueness(email, exclude_member_id) do
alias Mv.Helpers
alias Mv.Helpers.SystemActor
query = query =
Mv.Membership.Member Mv.Membership.Member
|> Ash.Query.filter(email == ^to_string(email)) |> Ash.Query.filter(email == ^to_string(email))
|> maybe_exclude_id(exclude_member_id) |> maybe_exclude_id(exclude_member_id)
case Ash.read(query) do system_actor = SystemActor.get_system_actor()
opts = Helpers.ash_actor_opts(system_actor)
case Ash.read(query, opts) do
{:ok, []} -> {:ok, []} ->
:ok :ok

View file

@ -30,8 +30,7 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
def validate(changeset, _opts, _context) do def validate(changeset, _opts, _context) do
email_changing? = Ash.Changeset.changing_attribute?(changeset, :email) email_changing? = Ash.Changeset.changing_attribute?(changeset, :email)
actor = Map.get(changeset.context || %{}, :actor) linked_user_id = get_linked_user_id(changeset.data)
linked_user_id = get_linked_user_id(changeset.data, actor)
is_linked? = not is_nil(linked_user_id) is_linked? = not is_nil(linked_user_id)
# Only validate if member is already linked AND email is changing # Only validate if member is already linked AND email is changing
@ -40,19 +39,22 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
if should_validate? do if should_validate? do
new_email = Ash.Changeset.get_attribute(changeset, :email) new_email = Ash.Changeset.get_attribute(changeset, :email)
check_email_uniqueness(new_email, linked_user_id, actor) check_email_uniqueness(new_email, linked_user_id)
else else
:ok :ok
end end
end end
defp check_email_uniqueness(email, exclude_user_id, actor) do defp check_email_uniqueness(email, exclude_user_id) do
alias Mv.Helpers.SystemActor
query = query =
Mv.Accounts.User Mv.Accounts.User
|> Ash.Query.filter(email == ^email) |> Ash.Query.filter(email == ^email)
|> maybe_exclude_id(exclude_user_id) |> maybe_exclude_id(exclude_user_id)
opts = Helpers.ash_actor_opts(actor) system_actor = SystemActor.get_system_actor()
opts = Helpers.ash_actor_opts(system_actor)
case Ash.read(query, opts) do case Ash.read(query, opts) do
{:ok, []} -> {:ok, []} ->
@ -69,8 +71,11 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
defp maybe_exclude_id(query, nil), do: query defp maybe_exclude_id(query, nil), do: query
defp maybe_exclude_id(query, id), do: Ash.Query.filter(query, id != ^id) defp maybe_exclude_id(query, id), do: Ash.Query.filter(query, id != ^id)
defp get_linked_user_id(member_data, actor) do defp get_linked_user_id(member_data) do
opts = Helpers.ash_actor_opts(actor) alias Mv.Helpers.SystemActor
system_actor = SystemActor.get_system_actor()
opts = Helpers.ash_actor_opts(system_actor)
case Ash.load(member_data, :user, opts) do case Ash.load(member_data, :user, opts) do
{:ok, %{user: %{id: id}}} -> id {:ok, %{user: %{id: id}}} -> id