System Actor Mode for Systemic Flows closes #348 #361

Merged
moritz merged 16 commits from feature/348_system_actor into main 2026-01-21 08:36:41 +01:00
3 changed files with 27 additions and 39 deletions
Showing only changes of commit 8acd92e8d4 - Show all commits

View file

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

View file

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

View file

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