Merge branch 'main' into feature/330_import_service_skeleton
This commit is contained in:
commit
4b41ab37bb
50 changed files with 2594 additions and 1103 deletions
|
|
@ -0,0 +1,44 @@
|
|||
defmodule Mv.Authorization.Checks.HasPermissionFailClosedTest do
|
||||
@moduledoc """
|
||||
Regression tests to ensure deny-filter behavior is fail-closed (matches no records).
|
||||
|
||||
These tests verify that when HasPermission.auto_filter returns a deny-filter
|
||||
(e.g., when actor is nil or no permission is found), the filter actually
|
||||
matches zero records in the database.
|
||||
|
||||
This prevents regressions like the previous bug where [id: {:not, {:in, []}}]
|
||||
was used, which logically evaluates to "NOT (id IN [])" = true for all IDs,
|
||||
effectively allowing all records instead of denying them.
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Authorization.Checks.HasPermission
|
||||
|
||||
import Mv.Fixtures
|
||||
|
||||
test "auto_filter deny-filter matches no records (regression for NOT IN [] allow-all bug)" do
|
||||
# Arrange: create some members in DB
|
||||
_m1 = member_fixture()
|
||||
_m2 = member_fixture()
|
||||
|
||||
# Build a minimal authorizer with a stable action type (:read)
|
||||
authorizer = %Ash.Policy.Authorizer{
|
||||
resource: Mv.Membership.Member,
|
||||
action: %{type: :read}
|
||||
}
|
||||
|
||||
# Act: missing actor must yield a deny-all filter (fail-closed)
|
||||
deny_filter = HasPermission.auto_filter(nil, authorizer, [])
|
||||
|
||||
# Apply the returned filter to a real DB query (no authorization involved)
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.new()
|
||||
|> Ash.Query.filter_input(deny_filter)
|
||||
|
||||
{:ok, results} = Ash.read(query, domain: Mv.Membership, authorize?: false)
|
||||
|
||||
# Assert: deny-filter must match nothing
|
||||
assert results == []
|
||||
end
|
||||
end
|
||||
|
|
@ -14,16 +14,22 @@ defmodule Mv.Authorization.Checks.HasPermissionIntegrationTest do
|
|||
alias Mv.Authorization.Checks.HasPermission
|
||||
|
||||
# Helper to create mock actor with role
|
||||
defp create_actor_with_role(permission_set_name) do
|
||||
%{
|
||||
defp create_actor_with_role(permission_set_name, opts \\ []) do
|
||||
actor = %{
|
||||
id: "user-#{System.unique_integer([:positive])}",
|
||||
role: %{permission_set_name: permission_set_name}
|
||||
}
|
||||
|
||||
# Add member_id if provided (needed for :linked scope tests)
|
||||
case Keyword.get(opts, :member_id) do
|
||||
nil -> actor
|
||||
member_id -> Map.put(actor, :member_id, member_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Filter Expression Structure - :linked scope" do
|
||||
test "Member filter uses user.id relationship path" do
|
||||
actor = create_actor_with_role("own_data")
|
||||
test "Member filter uses actor.member_id (inverse relationship)" do
|
||||
actor = create_actor_with_role("own_data", member_id: "member-123")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
filter = HasPermission.auto_filter(actor, authorizer, [])
|
||||
|
|
@ -36,8 +42,8 @@ defmodule Mv.Authorization.Checks.HasPermissionIntegrationTest do
|
|||
assert is_list(filter) or is_map(filter)
|
||||
end
|
||||
|
||||
test "CustomFieldValue filter uses member.user.id relationship path" do
|
||||
actor = create_actor_with_role("own_data")
|
||||
test "CustomFieldValue filter uses actor.member_id (via member relationship)" do
|
||||
actor = create_actor_with_role("own_data", member_id: "member-123")
|
||||
authorizer = create_authorizer(Mv.Membership.CustomFieldValue, :read)
|
||||
|
||||
filter = HasPermission.auto_filter(actor, authorizer, [])
|
||||
|
|
@ -66,14 +72,15 @@ defmodule Mv.Authorization.Checks.HasPermissionIntegrationTest do
|
|||
end
|
||||
|
||||
describe "Filter Expression Structure - :all scope" do
|
||||
test "Admin can read all members without filter" do
|
||||
test "Admin can read all members without filter (returns expr(true))" do
|
||||
actor = create_actor_with_role("admin")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
filter = HasPermission.auto_filter(actor, authorizer, [])
|
||||
|
||||
# :all scope should return nil (no filter needed)
|
||||
assert is_nil(filter)
|
||||
# :all scope should return [] (empty keyword list = no filter = allow all records)
|
||||
# After auto_filter fix: no longer returns nil, returns [] instead
|
||||
assert filter == []
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -81,7 +88,10 @@ defmodule Mv.Authorization.Checks.HasPermissionIntegrationTest do
|
|||
defp create_authorizer(resource, action) do
|
||||
%Ash.Policy.Authorizer{
|
||||
resource: resource,
|
||||
subject: %{action: %{name: action}}
|
||||
subject: %{
|
||||
action: %{type: action},
|
||||
data: nil
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,16 +13,25 @@ defmodule Mv.Authorization.Checks.HasPermissionTest do
|
|||
defp create_authorizer(resource, action) do
|
||||
%Ash.Policy.Authorizer{
|
||||
resource: resource,
|
||||
subject: %{action: %{name: action}}
|
||||
subject: %{
|
||||
action: %{type: action},
|
||||
data: nil
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# Helper to create actor with role
|
||||
defp create_actor(id, permission_set_name) do
|
||||
%{
|
||||
defp create_actor(id, permission_set_name, opts \\ []) do
|
||||
actor = %{
|
||||
id: id,
|
||||
role: %{permission_set_name: permission_set_name}
|
||||
}
|
||||
|
||||
# Add member_id if provided (needed for :linked scope tests)
|
||||
case Keyword.get(opts, :member_id) do
|
||||
nil -> actor
|
||||
member_id -> Map.put(actor, :member_id, member_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "describe/1" do
|
||||
|
|
@ -120,7 +129,7 @@ defmodule Mv.Authorization.Checks.HasPermissionTest do
|
|||
|
||||
describe "auto_filter/3 - Scope :linked" do
|
||||
test "scope :linked for Member returns user_id filter" do
|
||||
user = create_actor("user-123", "own_data")
|
||||
user = create_actor("user-123", "own_data", member_id: "member-456")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
filter = HasPermission.auto_filter(user, authorizer, [])
|
||||
|
|
@ -130,7 +139,7 @@ defmodule Mv.Authorization.Checks.HasPermissionTest do
|
|||
end
|
||||
|
||||
test "scope :linked for CustomFieldValue returns member.user_id filter" do
|
||||
user = create_actor("user-123", "own_data")
|
||||
user = create_actor("user-123", "own_data", member_id: "member-456")
|
||||
authorizer = create_authorizer(Mv.Membership.CustomFieldValue, :update)
|
||||
|
||||
filter = HasPermission.auto_filter(user, authorizer, [])
|
||||
|
|
|
|||
430
test/mv/membership/member_policies_test.exs
Normal file
430
test/mv/membership/member_policies_test.exs
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
defmodule Mv.Membership.MemberPoliciesTest do
|
||||
@moduledoc """
|
||||
Tests for Member resource authorization policies.
|
||||
|
||||
Tests all 4 permission sets (own_data, read_only, normal_user, admin)
|
||||
and verifies that policies correctly enforce access control based on
|
||||
user roles and permission sets.
|
||||
"""
|
||||
# async: false because we need database commits to be visible across queries
|
||||
# in the same test (especially for unlinked members)
|
||||
use Mv.DataCase, async: false
|
||||
|
||||
alias Mv.Membership
|
||||
alias Mv.Accounts
|
||||
alias Mv.Authorization
|
||||
|
||||
require Ash.Query
|
||||
|
||||
# Helper to create a role with a specific permission set
|
||||
defp create_role_with_permission_set(permission_set_name) do
|
||||
role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
|
||||
|
||||
case Authorization.create_role(%{
|
||||
name: role_name,
|
||||
description: "Test role for #{permission_set_name}",
|
||||
permission_set_name: permission_set_name
|
||||
}) do
|
||||
{:ok, role} -> role
|
||||
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to create a user with a specific permission set
|
||||
# Returns user with role preloaded (required for authorization)
|
||||
defp create_user_with_permission_set(permission_set_name) do
|
||||
# Create role with permission set
|
||||
role = create_role_with_permission_set(permission_set_name)
|
||||
|
||||
# Create user
|
||||
{:ok, user} =
|
||||
Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||
email: "user#{System.unique_integer([:positive])}@example.com",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Assign role to user
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
||||
|> Ash.update()
|
||||
|
||||
# Reload user with role preloaded (critical for authorization!)
|
||||
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts)
|
||||
user_with_role
|
||||
end
|
||||
|
||||
# Helper to create an admin user (for creating test fixtures)
|
||||
defp create_admin_user do
|
||||
create_user_with_permission_set("admin")
|
||||
end
|
||||
|
||||
# Helper to create a member linked to a user
|
||||
defp create_linked_member_for_user(user) do
|
||||
admin = create_admin_user()
|
||||
|
||||
# Create member
|
||||
# NOTE: We need to ensure the member is actually persisted to the database
|
||||
# before we try to link it. Ash may delay writes, so we explicitly return the struct.
|
||||
{:ok, member} =
|
||||
Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Linked",
|
||||
last_name: "Member",
|
||||
email: "linked#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create(actor: admin, return_notifications?: false)
|
||||
|
||||
# Link member to user (User.member_id = member.id)
|
||||
# We use force_change_attribute because the member already exists and we just
|
||||
# need to set the foreign key. This avoids the issue where manage_relationship
|
||||
# tries to query the member without the actor context.
|
||||
result =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.force_change_attribute(:member_id, member.id)
|
||||
|> Ash.update(actor: admin, domain: Mv.Accounts, return_notifications?: false)
|
||||
|
||||
{:ok, _user} = result
|
||||
|
||||
# Return the member struct directly - no need to reload since we just created it
|
||||
# and we're in the same transaction/sandbox
|
||||
member
|
||||
end
|
||||
|
||||
# Helper to create an unlinked member (no user relationship)
|
||||
defp create_unlinked_member do
|
||||
admin = create_admin_user()
|
||||
|
||||
{:ok, member} =
|
||||
Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Unlinked",
|
||||
last_name: "Member",
|
||||
email: "unlinked#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create(actor: admin)
|
||||
|
||||
member
|
||||
end
|
||||
|
||||
describe "own_data permission set (Mitglied)" do
|
||||
setup do
|
||||
user = create_user_with_permission_set("own_data")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
unlinked_member = create_unlinked_member()
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
|
||||
|
||||
%{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
|
||||
end
|
||||
|
||||
test "can read linked member", %{user: user, linked_member: linked_member} do
|
||||
{:ok, member} =
|
||||
Ash.get(Membership.Member, linked_member.id, actor: user, domain: Mv.Membership)
|
||||
|
||||
assert member.id == linked_member.id
|
||||
end
|
||||
|
||||
test "can update linked member", %{user: user, linked_member: linked_member} do
|
||||
# Update is allowed via HasPermission check with :linked scope (not via special case)
|
||||
# The special case policy only applies to :read actions
|
||||
{:ok, updated_member} =
|
||||
linked_member
|
||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
||||
|> Ash.update(actor: user)
|
||||
|
||||
assert updated_member.first_name == "Updated"
|
||||
end
|
||||
|
||||
test "cannot read unlinked member (returns forbidden)", %{
|
||||
user: user,
|
||||
unlinked_member: unlinked_member
|
||||
} do
|
||||
# Note: With auto_filter policies, when a user tries to read a member that doesn't
|
||||
# match the filter (id == actor.member_id), Ash returns NotFound, not Forbidden.
|
||||
# This is the expected behavior - the filter makes the record "invisible" to the user.
|
||||
assert_raise Ash.Error.Invalid, fn ->
|
||||
Ash.get!(Membership.Member, unlinked_member.id, actor: user, domain: Mv.Membership)
|
||||
end
|
||||
end
|
||||
|
||||
test "cannot update unlinked member (returns forbidden)", %{
|
||||
user: user,
|
||||
unlinked_member: unlinked_member
|
||||
} do
|
||||
assert_raise Ash.Error.Forbidden, fn ->
|
||||
unlinked_member
|
||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
||||
|> Ash.update!(actor: user)
|
||||
end
|
||||
end
|
||||
|
||||
test "list members returns only linked member", %{user: user, linked_member: linked_member} do
|
||||
{:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership)
|
||||
|
||||
# Should only return the linked member (scope :linked filters)
|
||||
assert length(members) == 1
|
||||
assert hd(members).id == linked_member.id
|
||||
end
|
||||
|
||||
test "cannot create member (returns forbidden)", %{user: user} do
|
||||
assert_raise Ash.Error.Forbidden, fn ->
|
||||
Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "New",
|
||||
last_name: "Member",
|
||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create!(actor: user)
|
||||
end
|
||||
end
|
||||
|
||||
test "cannot destroy member (returns forbidden)", %{user: user, linked_member: linked_member} do
|
||||
assert_raise Ash.Error.Forbidden, fn ->
|
||||
Ash.destroy!(linked_member, actor: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "read_only permission set (Vorstand/Buchhaltung)" do
|
||||
setup do
|
||||
user = create_user_with_permission_set("read_only")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
unlinked_member = create_unlinked_member()
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
|
||||
%{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
|
||||
end
|
||||
|
||||
test "can read all members", %{
|
||||
user: user,
|
||||
linked_member: linked_member,
|
||||
unlinked_member: unlinked_member
|
||||
} do
|
||||
{:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership)
|
||||
|
||||
# Should return all members (scope :all)
|
||||
member_ids = Enum.map(members, & &1.id)
|
||||
assert linked_member.id in member_ids
|
||||
assert unlinked_member.id in member_ids
|
||||
end
|
||||
|
||||
test "can read individual member", %{user: user, unlinked_member: unlinked_member} do
|
||||
{:ok, member} =
|
||||
Ash.get(Membership.Member, unlinked_member.id, actor: user, domain: Mv.Membership)
|
||||
|
||||
assert member.id == unlinked_member.id
|
||||
end
|
||||
|
||||
test "cannot create member (returns forbidden)", %{user: user} do
|
||||
assert_raise Ash.Error.Forbidden, fn ->
|
||||
Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "New",
|
||||
last_name: "Member",
|
||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create!(actor: user)
|
||||
end
|
||||
end
|
||||
|
||||
test "cannot update any member (returns forbidden)", %{
|
||||
user: user,
|
||||
linked_member: linked_member
|
||||
} do
|
||||
assert_raise Ash.Error.Forbidden, fn ->
|
||||
linked_member
|
||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
||||
|> Ash.update!(actor: user)
|
||||
end
|
||||
end
|
||||
|
||||
test "cannot destroy any member (returns forbidden)", %{
|
||||
user: user,
|
||||
unlinked_member: unlinked_member
|
||||
} do
|
||||
assert_raise Ash.Error.Forbidden, fn ->
|
||||
Ash.destroy!(unlinked_member, actor: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "normal_user permission set (Kassenwart)" do
|
||||
setup do
|
||||
user = create_user_with_permission_set("normal_user")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
unlinked_member = create_unlinked_member()
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
|
||||
%{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
|
||||
end
|
||||
|
||||
test "can read all members", %{
|
||||
user: user,
|
||||
linked_member: linked_member,
|
||||
unlinked_member: unlinked_member
|
||||
} do
|
||||
{:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership)
|
||||
|
||||
# Should return all members (scope :all)
|
||||
member_ids = Enum.map(members, & &1.id)
|
||||
assert linked_member.id in member_ids
|
||||
assert unlinked_member.id in member_ids
|
||||
end
|
||||
|
||||
test "can create member", %{user: user} do
|
||||
{:ok, member} =
|
||||
Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "New",
|
||||
last_name: "Member",
|
||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create(actor: user)
|
||||
|
||||
assert member.first_name == "New"
|
||||
end
|
||||
|
||||
test "can update any member", %{user: user, unlinked_member: unlinked_member} do
|
||||
{:ok, updated_member} =
|
||||
unlinked_member
|
||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
||||
|> Ash.update(actor: user)
|
||||
|
||||
assert updated_member.first_name == "Updated"
|
||||
end
|
||||
|
||||
test "cannot destroy member (safety - not in permission set)", %{
|
||||
user: user,
|
||||
unlinked_member: unlinked_member
|
||||
} do
|
||||
assert_raise Ash.Error.Forbidden, fn ->
|
||||
Ash.destroy!(unlinked_member, actor: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "admin permission set" do
|
||||
setup do
|
||||
user = create_user_with_permission_set("admin")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
unlinked_member = create_unlinked_member()
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
|
||||
%{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
|
||||
end
|
||||
|
||||
test "can read all members", %{
|
||||
user: user,
|
||||
linked_member: linked_member,
|
||||
unlinked_member: unlinked_member
|
||||
} do
|
||||
{:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership)
|
||||
|
||||
# Should return all members (scope :all)
|
||||
member_ids = Enum.map(members, & &1.id)
|
||||
assert linked_member.id in member_ids
|
||||
assert unlinked_member.id in member_ids
|
||||
end
|
||||
|
||||
test "can create member", %{user: user} do
|
||||
{:ok, member} =
|
||||
Membership.Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "New",
|
||||
last_name: "Member",
|
||||
email: "new#{System.unique_integer([:positive])}@example.com"
|
||||
})
|
||||
|> Ash.create(actor: user)
|
||||
|
||||
assert member.first_name == "New"
|
||||
end
|
||||
|
||||
test "can update any member", %{user: user, unlinked_member: unlinked_member} do
|
||||
{:ok, updated_member} =
|
||||
unlinked_member
|
||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
||||
|> Ash.update(actor: user)
|
||||
|
||||
assert updated_member.first_name == "Updated"
|
||||
end
|
||||
|
||||
test "can destroy any member", %{user: user, unlinked_member: unlinked_member} do
|
||||
:ok = Ash.destroy(unlinked_member, actor: user)
|
||||
|
||||
# Verify member is deleted
|
||||
assert {:error, _} = Ash.get(Membership.Member, unlinked_member.id, domain: Mv.Membership)
|
||||
end
|
||||
end
|
||||
|
||||
describe "special case: user can always READ linked member" do
|
||||
# Note: The special case policy only applies to :read actions.
|
||||
# Updates are handled by HasPermission with :linked scope (if permission exists).
|
||||
|
||||
test "read_only user can read linked member (via special case bypass)" do
|
||||
# read_only has Member.read scope :all, but the special case ensures
|
||||
# users can ALWAYS read their linked member, even if they had no read permission.
|
||||
# This test verifies the special case works independently of permission sets.
|
||||
user = create_user_with_permission_set("read_only")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
|
||||
|
||||
# Should succeed (special case bypass policy for :read takes precedence)
|
||||
{:ok, member} =
|
||||
Ash.get(Membership.Member, linked_member.id, actor: user, domain: Mv.Membership)
|
||||
|
||||
assert member.id == linked_member.id
|
||||
end
|
||||
|
||||
test "own_data user can read linked member (via special case bypass)" do
|
||||
# own_data has Member.read scope :linked, but the special case ensures
|
||||
# users can ALWAYS read their linked member regardless of permission set.
|
||||
user = create_user_with_permission_set("own_data")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
|
||||
|
||||
# Should succeed (special case bypass policy for :read takes precedence)
|
||||
{:ok, member} =
|
||||
Ash.get(Membership.Member, linked_member.id, actor: user, domain: Mv.Membership)
|
||||
|
||||
assert member.id == linked_member.id
|
||||
end
|
||||
|
||||
test "own_data user can update linked member (via HasPermission :linked scope)" do
|
||||
# Update is NOT handled by special case - it's handled by HasPermission
|
||||
# with :linked scope. own_data has Member.update scope :linked.
|
||||
user = create_user_with_permission_set("own_data")
|
||||
linked_member = create_linked_member_for_user(user)
|
||||
|
||||
# Reload user to get updated member_id
|
||||
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
|
||||
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
|
||||
|
||||
# Should succeed via HasPermission check (not special case)
|
||||
{:ok, updated_member} =
|
||||
linked_member
|
||||
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
|
||||
|> Ash.update(actor: user)
|
||||
|
||||
assert updated_member.first_name == "Updated"
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue