System Actor Mode for Systemic Flows closes #348 #361
3 changed files with 73 additions and 8 deletions
|
|
@ -641,7 +641,54 @@ def card(assigns) do
|
|||
end
|
||||
```
|
||||
|
||||
### 3.3 Ash Framework
|
||||
### 3.3 System Actor Pattern
|
||||
|
||||
**When to Use System Actor:**
|
||||
|
||||
Some operations must always run regardless of user permissions. These are **systemic operations** that are mandatory side effects:
|
||||
|
||||
- **Email synchronization** (Member ↔ User)
|
||||
- **Email uniqueness validation** (data integrity requirement)
|
||||
- **Cycle generation** (if defined as mandatory side effect)
|
||||
- **Background jobs**
|
||||
- **Seeds**
|
||||
|
||||
**Implementation:**
|
||||
|
||||
Use `Mv.Helpers.SystemActor.get_system_actor/0` for all systemic operations:
|
||||
|
||||
```elixir
|
||||
# Good - Email sync uses system actor
|
||||
def get_linked_member(user) 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
|
||||
{:error, _} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
# Bad - Using user actor for systemic operation
|
||||
def get_linked_member(user, actor) do
|
||||
opts = Helpers.ash_actor_opts(actor) # May fail if user lacks permissions!
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
**System Actor Details:**
|
||||
|
||||
- System actor is a user with admin role (email: "system@mila.local")
|
||||
- Cached in Agent for performance
|
||||
- Falls back to admin user from seeds if system user doesn't exist
|
||||
- Should NEVER be used for user-initiated actions (only systemic operations)
|
||||
|
||||
**User Mode vs System Mode:**
|
||||
|
||||
- **User Mode**: User-initiated actions use the actual user actor, policies are enforced
|
||||
- **System Mode**: Systemic operations use system actor, bypass user permissions
|
||||
|
||||
### 3.4 Ash Framework
|
||||
|
||||
**Resource Definition Best Practices:**
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,9 @@ defmodule Mv.Helpers.SystemActor do
|
|||
{:ok, nil} ->
|
||||
# System user doesn't exist - fall back to admin user
|
||||
case load_admin_user_fallback() do
|
||||
{:ok, admin_user} -> admin_user
|
||||
{:ok, admin_user} ->
|
||||
admin_user
|
||||
|
||||
{:error, _} ->
|
||||
# In test environment, create a temporary admin user if none exists
|
||||
if Mix.env() == :test do
|
||||
|
|
@ -137,7 +139,9 @@ defmodule Mv.Helpers.SystemActor do
|
|||
{:error, _reason} = error ->
|
||||
# Database error - try fallback
|
||||
case load_admin_user_fallback() do
|
||||
{:ok, admin_user} -> admin_user
|
||||
{:ok, admin_user} ->
|
||||
admin_user
|
||||
|
||||
{:error, _} ->
|
||||
# In test environment, create a temporary admin user if none exists
|
||||
if Mix.env() == :test do
|
||||
|
|
@ -166,16 +170,21 @@ defmodule Mv.Helpers.SystemActor do
|
|||
description: "Administrator with full access",
|
||||
permission_set_name: "admin"
|
||||
}) do
|
||||
{:ok, role} -> role
|
||||
{:error, %Ash.Error.Invalid{errors: [%{field: :name, message: "has already been taken"}]}} ->
|
||||
{:ok, role} ->
|
||||
role
|
||||
|
||||
{:error,
|
||||
%Ash.Error.Invalid{errors: [%{field: :name, message: "has already been taken"}]}} ->
|
||||
# Role was created by another process - find it
|
||||
case Authorization.list_roles() do
|
||||
{:ok, updated_roles} ->
|
||||
Enum.find(updated_roles, &(&1.permission_set_name == "admin")) ||
|
||||
raise "Admin role should exist but was not found"
|
||||
|
||||
_ ->
|
||||
raise "Failed to find admin role after creation attempt"
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
raise "Failed to create admin role: #{inspect(error)}"
|
||||
end
|
||||
|
|
@ -191,16 +200,21 @@ defmodule Mv.Helpers.SystemActor do
|
|||
description: "Administrator with full access",
|
||||
permission_set_name: "admin"
|
||||
}) do
|
||||
{:ok, role} -> role
|
||||
{:error, %Ash.Error.Invalid{errors: [%{field: :name, message: "has already been taken"}]}} ->
|
||||
{:ok, role} ->
|
||||
role
|
||||
|
||||
{:error,
|
||||
%Ash.Error.Invalid{errors: [%{field: :name, message: "has already been taken"}]}} ->
|
||||
# Role exists - try to find it
|
||||
case Authorization.list_roles() do
|
||||
{:ok, roles} ->
|
||||
Enum.find(roles, &(&1.permission_set_name == "admin")) ||
|
||||
raise "Admin role should exist but was not found"
|
||||
|
||||
_ ->
|
||||
raise "Failed to find admin role"
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
raise "Failed to create admin role: #{inspect(error)}"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -79,7 +79,10 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
|> Ash.load!(:role, domain: Mv.Accounts)
|
||||
|
||||
_ ->
|
||||
Accounts.create_user!(%{email: admin_email}, upsert?: true, upsert_identity: :unique_email)
|
||||
Accounts.create_user!(%{email: admin_email},
|
||||
upsert?: true,
|
||||
upsert_identity: :unique_email
|
||||
)
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||
|> Ash.update!()
|
||||
|
|
@ -152,6 +155,7 @@ defmodule Mv.Helpers.SystemActorTest do
|
|||
end
|
||||
|
||||
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
|
||||
|
||||
case Accounts.User
|
||||
|> Ash.Query.filter(email == ^admin_email)
|
||||
|> Ash.read_one(domain: Mv.Accounts) do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue