Use system actor for email synchronization

Update email sync loader and changes to use system actor instead of user actor.
This ensures email sync always works regardless of user permissions.
This commit is contained in:
Moritz 2026-01-20 22:09:18 +01:00 committed by Simon
parent ffe5faa999
commit 8f06442de9
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
3 changed files with 27 additions and 39 deletions

View file

@ -41,10 +41,8 @@ defmodule Mv.EmailSync.Changes.SyncMemberEmailToUser do
Ash.Changeset.around_transaction(changeset, fn cs, callback ->
result = callback.(cs)
actor = Map.get(changeset.context, :actor)
with {:ok, member} <- Helpers.extract_record(result),
linked_user <- Loader.get_linked_user(member, actor) do
linked_user <- Loader.get_linked_user(member) do
Helpers.sync_email_to_linked_record(result, linked_user, new_email)
else
_ -> result

View file

@ -33,17 +33,7 @@ defmodule Mv.EmailSync.Changes.SyncUserEmailToMember do
if Map.get(context, :syncing_email, false) do
changeset
else
# Ensure actor is in changeset context - get it from context if available
actor = Map.get(changeset.context, :actor) || Map.get(context, :actor)
changeset_with_actor =
if actor && !Map.has_key?(changeset.context, :actor) do
Ash.Changeset.put_context(changeset, :actor, actor)
else
changeset
end
sync_email(changeset_with_actor)
sync_email(changeset)
end
end
@ -52,7 +42,7 @@ defmodule Mv.EmailSync.Changes.SyncUserEmailToMember do
result = callback.(cs)
with {:ok, record} <- Helpers.extract_record(result),
{:ok, user, member} <- get_user_and_member(record, cs) do
{:ok, user, member} <- get_user_and_member(record) do
# When called from Member-side, we need to update the member in the result
# When called from User-side, we update the linked member in DB only
case record do
@ -71,19 +61,16 @@ defmodule Mv.EmailSync.Changes.SyncUserEmailToMember do
end
# Retrieves user and member - works for both resource types
defp get_user_and_member(%Mv.Accounts.User{} = user, changeset) do
actor = Map.get(changeset.context, :actor)
case Loader.get_linked_member(user, actor) do
# Uses system actor via Loader functions
defp get_user_and_member(%Mv.Accounts.User{} = user) do
case Loader.get_linked_member(user) do
nil -> {:error, :no_member}
member -> {:ok, user, member}
end
end
defp get_user_and_member(%Mv.Membership.Member{} = member, changeset) do
actor = Map.get(changeset.context, :actor)
case Loader.load_linked_user!(member, actor) do
defp get_user_and_member(%Mv.Membership.Member{} = member) do
case Loader.load_linked_user!(member) do
{:ok, user} -> {:ok, user, member}
error -> error
end

View file

@ -5,25 +5,26 @@ defmodule Mv.EmailSync.Loader do
## Authorization
This module runs systemically and accepts optional actor parameters.
When called from hooks/changes, actor is extracted from changeset context.
When called directly, actor should be provided for proper authorization.
This module runs systemically and uses the system actor for all operations.
This ensures that email synchronization always works, regardless of user permissions.
All functions accept an optional `actor` parameter that is passed to Ash operations
to ensure proper authorization checks are performed.
All functions use `Mv.Helpers.SystemActor.get_system_actor/0` to bypass
user permission checks, as email sync is a mandatory side effect.
"""
alias Mv.Helpers
alias Mv.Helpers.SystemActor
@doc """
Loads the member linked to a user, returns nil if not linked or on error.
Accepts optional actor for authorization.
Uses system actor for authorization to ensure email sync always works.
"""
def get_linked_member(user, actor \\ nil)
def get_linked_member(%{member_id: nil}, _actor), do: nil
def get_linked_member(user)
def get_linked_member(%{member_id: nil}), do: nil
def get_linked_member(%{member_id: id}, actor) do
opts = Helpers.ash_actor_opts(actor)
def get_linked_member(%{member_id: id}) do
system_actor = SystemActor.get_system_actor()
opts = Helpers.ash_actor_opts(system_actor)
case Ash.get(Mv.Membership.Member, id, opts) do
{:ok, member} -> member
@ -34,10 +35,11 @@ defmodule Mv.EmailSync.Loader do
@doc """
Loads the user linked to a member, returns nil if not linked or on error.
Accepts optional actor for authorization.
Uses system actor for authorization to ensure email sync always works.
"""
def get_linked_user(member, actor \\ nil) do
opts = Helpers.ash_actor_opts(actor)
def get_linked_user(member) do
system_actor = SystemActor.get_system_actor()
opts = Helpers.ash_actor_opts(system_actor)
case Ash.load(member, :user, opts) do
{:ok, %{user: user}} -> user
@ -49,10 +51,11 @@ defmodule Mv.EmailSync.Loader do
Loads the user linked to a member, returning an error tuple if not linked.
Useful when a link is required for the operation.
Accepts optional actor for authorization.
Uses system actor for authorization to ensure email sync always works.
"""
def load_linked_user!(member, actor \\ nil) do
opts = Helpers.ash_actor_opts(actor)
def load_linked_user!(member) do
system_actor = SystemActor.get_system_actor()
opts = Helpers.ash_actor_opts(system_actor)
case Ash.load(member, :user, opts) do
{:ok, %{user: user}} when not is_nil(user) -> {:ok, user}