Member Email Validation for Linked Members closes #397 #399

Merged
moritz merged 7 commits from feature/397_emailsync_permission into main 2026-02-03 16:35:41 +01:00
2 changed files with 52 additions and 12 deletions
Showing only changes of commit 3d46ba655f - Show all commits

View file

@ -1,6 +1,7 @@
defmodule Mv.Authorization.Actor do
@moduledoc """
Helper functions for ensuring User actors have required data loaded.
Helper functions for ensuring User actors have required data loaded
and for querying actor capabilities (e.g. admin, permission set).
## Actor Invariant
@ -27,8 +28,11 @@ defmodule Mv.Authorization.Actor do
assign(socket, :current_user, user)
end
# In tests
user = Actor.ensure_loaded(user)
# Check if actor is admin (policy checks, validations)
if Actor.admin?(actor), do: ...
# Get permission set name (string or nil)
ps_name = Actor.permission_set_name(actor)
## Security Note
@ -47,6 +51,8 @@ defmodule Mv.Authorization.Actor do
require Logger
alias Mv.Helpers.SystemActor
@doc """
Ensures the actor (User) has their `:role` relationship loaded.
@ -96,4 +102,43 @@ defmodule Mv.Authorization.Actor do
actor
end
end
@doc """
Returns the actor's permission set name (string or atom) from their role, or nil.
Ensures role is loaded (including when role is nil). Supports both atom and
string keys for session/socket assigns. Use for capability checks consistent
with `ActorIsAdmin` and `HasPermission`.
"""
@spec permission_set_name(Mv.Accounts.User.t() | map() | nil) :: String.t() | atom() | nil
def permission_set_name(nil), do: nil
def permission_set_name(actor) do
actor = actor |> ensure_loaded() |> maybe_load_role()
get_in(actor, [Access.key(:role), Access.key(:permission_set_name)]) ||
get_in(actor, [Access.key("role"), Access.key("permission_set_name")])
end
@doc """
Returns true if the actor is the system user or has the admin permission set.
Use for validations and policy checks that require admin capability (e.g.
changing a linked member's email). Consistent with `ActorIsAdmin` policy check.
"""
@spec admin?(Mv.Accounts.User.t() | map() | nil) :: boolean()
def admin?(nil), do: false
def admin?(actor) do
SystemActor.system_user?(actor) or permission_set_name(actor) in ["admin", :admin]
end
defp maybe_load_role(%Mv.Accounts.User{role: nil} = user) do
case Ash.load(user, :role, domain: Mv.Accounts, authorize?: false) do
{:ok, loaded} -> loaded
_ -> user
end
end
defp maybe_load_role(actor), do: actor
end

View file

@ -3,20 +3,15 @@ defmodule Mv.Authorization.Checks.ActorIsAdmin do
Policy check: true when the actor's role has permission_set_name "admin".
Used to restrict actions (e.g. User.update_user for member link/unlink) to admins only.
Delegates to `Mv.Authorization.Actor.admin?/1` for consistency.
"""
use Ash.Policy.SimpleCheck
alias Mv.Authorization.Actor
@impl true
def describe(_opts), do: "actor has admin permission set"
@impl true
def match?(nil, _context, _opts), do: false
def match?(actor, _context, _opts) do
ps_name =
get_in(actor, [Access.key(:role), Access.key(:permission_set_name)]) ||
get_in(actor, [Access.key("role"), Access.key("permission_set_name")])
ps_name == "admin"
end
def match?(actor, _context, _opts), do: Actor.admin?(actor)
end