Harden member user-link check: argument presence, nil actor, policy scope

- Forbid on :user argument presence (not value) to block unlink via nil/empty
- Defensive nil actor handling; policy restricted to create/update only
- Test: Ash.load with actor; test non-admin cannot unlink via user: nil
- Docs: unlink behaviour and policy split
This commit is contained in:
Moritz 2026-02-04 13:46:49 +01:00
parent 34e049ef32
commit 543fded102
Signed by: moritz
GPG key ID: 1020A035E5DD0824
4 changed files with 79 additions and 36 deletions

View file

@ -432,7 +432,9 @@ defmodule Mv.Membership.MemberPoliciesTest do
assert member.first_name == "NoLink"
# Member has_one :user (FK on User side); ensure no user is linked
{:ok, member} = Ash.load(member, :user, domain: Mv.Membership)
{:ok, member} =
Ash.load(member, :user, domain: Mv.Membership, actor: normal_user)
assert is_nil(member.user)
end
@ -480,6 +482,29 @@ defmodule Mv.Membership.MemberPoliciesTest do
Membership.update_member(unlinked_member, params, actor: normal_user)
end
test "normal_user cannot update member with user: nil (unlink forbidden)", %{
normal_user: normal_user,
unlinked_member: unlinked_member
} do
# Link member first (via admin), then normal_user tries to unlink via user: nil
admin =
Mv.Fixtures.user_with_role_fixture("admin") |> Mv.Authorization.Actor.ensure_loaded()
link_target =
Mv.Fixtures.user_with_role_fixture("own_data") |> Mv.Authorization.Actor.ensure_loaded()
{:ok, linked_member} =
Membership.update_member(
unlinked_member,
%{user: %{id: link_target.id}},
actor: admin
)
# Passing user: nil explicitly tries to unlink; only admin may do that
assert {:error, %Ash.Error.Forbidden{}} =
Membership.update_member(linked_member, %{user: nil}, actor: normal_user)
end
test "admin can create member with :user argument", %{admin: admin} do
link_target =
Mv.Fixtures.user_with_role_fixture("own_data")