Restrict member user link to admins (forbid policy)

Add ForbidMemberUserLinkUnlessAdmin check; forbid_if on Member create/update.
Fix member user-link tests: pass :user in params, assert via reload.
This commit is contained in:
Moritz 2026-02-04 12:50:10 +01:00
parent 4d3a64c177
commit 26fbafdd9d
Signed by: moritz
GPG key ID: 1020A035E5DD0824
3 changed files with 186 additions and 7 deletions

View file

@ -312,14 +312,12 @@ defmodule Mv.Membership.Member do
authorize_if expr(id == ^actor(:member_id))
end
# GENERAL: Check permissions from user's role
# HasPermission handles update permissions correctly:
# - :own_data → can update linked member (scope :linked)
# - :read_only → cannot update any member (no update permission)
# - :normal_user → can update all members (scope :all)
# - :admin → can update all members (scope :all)
# GENERAL: Check permissions from user's role; forbid memberuser link unless admin
# ForbidMemberUserLinkUnlessAdmin: only admins may pass :user on create/update (no-op for read/destroy).
# HasPermission: :own_data → update linked; :read_only → no update; :normal_user/admin → update all.
policy action_type([:read, :create, :update, :destroy]) do
description "Check permissions from user's role and permission set"
description "Check permissions and forbid user link unless admin"
forbid_if Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin
authorize_if Mv.Authorization.Checks.HasPermission
end

View file

@ -0,0 +1,65 @@
defmodule Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin do
@moduledoc """
Policy check: forbids setting or changing the memberuser link unless the actor is admin.
Used on Member create_member and update_member actions. When the `:user` argument
is present (linking a member to a user account), only admins may perform the action.
Non-admin users (e.g. normal_user / Kassenwart) can still create and update members
as long as they do not pass the `:user` argument.
## Usage
In Member resource policies, add **before** the general HasPermission policy:
policy action_type([:create, :update]) do
forbid_if Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin
authorize_if Mv.Authorization.Checks.HasPermission
end
## Behaviour
- If the action has no `:user` argument or it is nil/empty does not forbid.
- If `:user` is set (e.g. `%{id: user_id}`) and actor is not admin forbids (returns true).
- If actor is admin (or system actor) does not forbid.
"""
use Ash.Policy.Check
alias Mv.Authorization.Actor
@impl true
def describe(_opts), do: "forbid setting memberuser link unless actor is admin"
@impl true
def strict_check(actor, authorizer, _opts) do
actor = Actor.ensure_loaded(actor)
if user_argument_set?(authorizer) and not Actor.admin?(actor) do
{:ok, true}
else
{:ok, false}
end
end
defp user_argument_set?(authorizer) do
user_arg = get_user_argument(authorizer)
not is_nil(user_arg) and not empty_user_arg?(user_arg)
end
defp get_user_argument(authorizer) do
changeset = authorizer.changeset || authorizer.subject
cond do
is_struct(changeset, Ash.Changeset) ->
Ash.Changeset.get_argument(changeset, :user)
is_struct(changeset, Ash.ActionInput) ->
Map.get(changeset.arguments || %{}, :user)
true ->
nil
end
end
defp empty_user_arg?(%{} = m), do: map_size(m) == 0
defp empty_user_arg?(_), do: false
end