mitgliederverwaltung/test/mv/membership/member_group_policies_test.exs
Moritz 890a4d3752 MemberGroup: restrict bypass to own_data via MemberGroupReadLinkedForOwnData
- ActorPermissionSetIs check; bypass policy filters by member_id for own_data only.
- Admin with member_id still gets :all via HasPermission. Tests added.
2026-02-04 09:19:57 +01:00

234 lines
7.7 KiB
Elixir

defmodule Mv.Membership.MemberGroupPoliciesTest do
@moduledoc """
Tests for MemberGroup resource authorization policies.
Verifies own_data can only read linked member's associations;
read_only can read all, cannot create/destroy;
normal_user and admin can read, create, destroy.
"""
use Mv.DataCase, async: false
alias Mv.Membership
require Ash.Query
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
defp create_member_fixture do
Mv.Fixtures.member_fixture()
end
defp create_group_fixture do
Mv.Fixtures.group_fixture()
end
defp create_member_group_fixture(member_id, group_id) do
admin = Mv.Fixtures.user_with_role_fixture("admin")
{:ok, member_group} =
Membership.create_member_group(%{member_id: member_id, group_id: group_id}, actor: admin)
member_group
end
describe "own_data permission set" do
setup %{actor: actor} do
user = Mv.Fixtures.user_with_role_fixture("own_data")
member = create_member_fixture()
group = create_group_fixture()
# Link user to member so actor.member_id is set
admin = Mv.Fixtures.user_with_role_fixture("admin")
user =
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.force_change_attribute(:member_id, member.id)
|> Ash.update(actor: admin)
{:ok, user} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
mg_linked = create_member_group_fixture(member.id, group.id)
# MemberGroup for another member (not linked to user)
other_member = create_member_fixture()
other_group = create_group_fixture()
mg_other = create_member_group_fixture(other_member.id, other_group.id)
%{user: user, member: member, group: group, mg_linked: mg_linked, mg_other: mg_other}
end
test "can read member_groups for linked member only", %{user: user, mg_linked: mg_linked} do
{:ok, list} =
Mv.Membership.MemberGroup
|> Ash.read(actor: user, domain: Mv.Membership)
ids = Enum.map(list, & &1.id)
assert mg_linked.id in ids
refute Enum.empty?(list)
end
test "list returns only member_groups where member_id == actor.member_id", %{
user: user,
mg_linked: mg_linked,
mg_other: mg_other
} do
{:ok, list} =
Mv.Membership.MemberGroup
|> Ash.read(actor: user, domain: Mv.Membership)
ids = Enum.map(list, & &1.id)
assert mg_linked.id in ids
refute mg_other.id in ids
end
test "cannot create member_group (returns forbidden)", %{user: user, actor: _actor} do
# Use fresh member/group so we assert on Forbidden, not on duplicate validation
other_member = create_member_fixture()
other_group = create_group_fixture()
assert {:error, %Ash.Error.Forbidden{}} =
Membership.create_member_group(
%{member_id: other_member.id, group_id: other_group.id},
actor: user
)
end
test "cannot destroy member_group (returns forbidden)", %{user: user, mg_linked: mg_linked} do
assert {:error, %Ash.Error.Forbidden{}} =
Membership.destroy_member_group(mg_linked, actor: user)
end
end
describe "read_only permission set" do
setup %{actor: actor} do
user = Mv.Fixtures.user_with_role_fixture("read_only")
member = create_member_fixture()
group = create_group_fixture()
mg = create_member_group_fixture(member.id, group.id)
%{actor: actor, user: user, member: member, group: group, mg: mg}
end
test "can read all member_groups", %{user: user, mg: mg} do
{:ok, list} =
Mv.Membership.MemberGroup
|> Ash.read(actor: user, domain: Mv.Membership)
ids = Enum.map(list, & &1.id)
assert mg.id in ids
end
test "cannot create member_group (returns forbidden)", %{user: user, actor: _actor} do
member = create_member_fixture()
group = create_group_fixture()
assert {:error, %Ash.Error.Forbidden{}} =
Membership.create_member_group(%{member_id: member.id, group_id: group.id},
actor: user
)
end
test "cannot destroy member_group (returns forbidden)", %{user: user, mg: mg} do
assert {:error, %Ash.Error.Forbidden{}} =
Membership.destroy_member_group(mg, actor: user)
end
end
describe "normal_user permission set" do
setup %{actor: actor} do
user = Mv.Fixtures.user_with_role_fixture("normal_user")
member = create_member_fixture()
group = create_group_fixture()
mg = create_member_group_fixture(member.id, group.id)
%{actor: actor, user: user, member: member, group: group, mg: mg}
end
test "can read all member_groups", %{user: user, mg: mg} do
{:ok, list} =
Mv.Membership.MemberGroup
|> Ash.read(actor: user, domain: Mv.Membership)
ids = Enum.map(list, & &1.id)
assert mg.id in ids
end
test "can create member_group", %{user: user, actor: _actor} do
member = create_member_fixture()
group = create_group_fixture()
assert {:ok, _mg} =
Membership.create_member_group(%{member_id: member.id, group_id: group.id},
actor: user
)
end
test "can destroy member_group", %{user: user, mg: mg} do
assert :ok = Membership.destroy_member_group(mg, actor: user)
end
end
describe "admin permission set" do
setup %{actor: actor} do
user = Mv.Fixtures.user_with_role_fixture("admin")
member = create_member_fixture()
group = create_group_fixture()
mg = create_member_group_fixture(member.id, group.id)
%{actor: actor, user: user, member: member, group: group, mg: mg}
end
test "can read all member_groups", %{user: user, mg: mg} do
{:ok, list} =
Mv.Membership.MemberGroup
|> Ash.read(actor: user, domain: Mv.Membership)
ids = Enum.map(list, & &1.id)
assert mg.id in ids
end
test "admin with member_id set (linked to member) still reads all member_groups", %{
actor: actor
} do
# Admin linked to a member (e.g. viewing as member context) must still get :all scope,
# not restricted to linked member's groups (bypass is only for own_data).
admin = Mv.Fixtures.user_with_role_fixture("admin")
linked_member = create_member_fixture()
other_member = create_member_fixture()
group_a = create_group_fixture()
group_b = create_group_fixture()
admin =
admin
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.force_change_attribute(:member_id, linked_member.id)
|> Ash.update(actor: actor)
{:ok, admin} = Ash.load(admin, :role, domain: Mv.Accounts, actor: actor)
mg_linked = create_member_group_fixture(linked_member.id, group_a.id)
mg_other = create_member_group_fixture(other_member.id, group_b.id)
{:ok, list} =
Mv.Membership.MemberGroup
|> Ash.read(actor: admin, domain: Mv.Membership)
ids = Enum.map(list, & &1.id)
assert mg_linked.id in ids, "Admin with member_id must see linked member's MemberGroups"
assert mg_other.id in ids,
"Admin with member_id must see all MemberGroups (:all), not only linked"
end
test "can create member_group", %{user: user, actor: _actor} do
member = create_member_fixture()
group = create_group_fixture()
assert {:ok, _mg} =
Membership.create_member_group(%{member_id: member.id, group_id: group.id},
actor: user
)
end
test "can destroy member_group", %{user: user, mg: mg} do
assert :ok = Membership.destroy_member_group(mg, actor: user)
end
end
end