Add email-change permission validation for linked members
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Only admins or the linked user may change a linked member's email. - New validation EmailChangePermission (uses Actor.admin?, Loader.get_linked_user). - Register on Member update_member; docs and gettext.
This commit is contained in:
parent
ad02f8914f
commit
4ea31f0f37
7 changed files with 324 additions and 28 deletions
237
test/mv/membership/member_email_validation_test.exs
Normal file
237
test/mv/membership/member_email_validation_test.exs
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
defmodule Mv.Membership.MemberEmailValidationTest do
|
||||
@moduledoc """
|
||||
Tests for Member email-change permission validation.
|
||||
|
||||
When a member is linked to a user, only admins or the linked user may change
|
||||
that member's email. Unlinked members and non-email updates are unaffected.
|
||||
"""
|
||||
use Mv.DataCase, async: false
|
||||
|
||||
alias Mv.Accounts
|
||||
alias Mv.Authorization
|
||||
alias Mv.Helpers.SystemActor
|
||||
alias Mv.Membership
|
||||
|
||||
setup do
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
%{actor: system_actor}
|
||||
end
|
||||
|
||||
defp create_role_with_permission_set(permission_set_name, actor) 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
|
||||
},
|
||||
actor: actor
|
||||
) do
|
||||
{:ok, role} -> role
|
||||
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp create_user_with_permission_set(permission_set_name, actor) do
|
||||
role = create_role_with_permission_set(permission_set_name, actor)
|
||||
|
||||
{:ok, user} =
|
||||
Accounts.User
|
||||
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||
email: "user#{System.unique_integer([:positive])}@example.com",
|
||||
password: "testpassword123"
|
||||
})
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:update, %{})
|
||||
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
||||
|> Ash.update(actor: actor)
|
||||
|
||||
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
|
||||
user_with_role
|
||||
end
|
||||
|
||||
defp create_admin_user(actor) do
|
||||
create_user_with_permission_set("admin", actor)
|
||||
end
|
||||
|
||||
defp create_linked_member_for_user(user, actor) do
|
||||
admin = create_admin_user(actor)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Linked",
|
||||
last_name: "Member",
|
||||
email: "linked#{System.unique_integer([:positive])}@example.com"
|
||||
},
|
||||
actor: admin
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
member
|
||||
end
|
||||
|
||||
defp create_unlinked_member(actor) do
|
||||
admin = create_admin_user(actor)
|
||||
|
||||
{:ok, member} =
|
||||
Membership.create_member(
|
||||
%{
|
||||
first_name: "Unlinked",
|
||||
last_name: "Member",
|
||||
email: "unlinked#{System.unique_integer([:positive])}@example.com"
|
||||
},
|
||||
actor: admin
|
||||
)
|
||||
|
||||
member
|
||||
end
|
||||
|
||||
describe "unlinked member" do
|
||||
test "normal_user can update email of unlinked member", %{actor: actor} do
|
||||
normal_user = create_user_with_permission_set("normal_user", actor)
|
||||
unlinked_member = create_unlinked_member(actor)
|
||||
|
||||
new_email = "new#{System.unique_integer([:positive])}@example.com"
|
||||
|
||||
assert {:ok, updated} =
|
||||
Membership.update_member(unlinked_member, %{email: new_email}, actor: normal_user)
|
||||
|
||||
assert updated.email == new_email
|
||||
end
|
||||
|
||||
test "validation does not block when member has no linked user", %{actor: actor} do
|
||||
normal_user = create_user_with_permission_set("normal_user", actor)
|
||||
unlinked_member = create_unlinked_member(actor)
|
||||
|
||||
new_email = "other#{System.unique_integer([:positive])}@example.com"
|
||||
|
||||
assert {:ok, _} =
|
||||
Membership.update_member(unlinked_member, %{email: new_email}, actor: normal_user)
|
||||
end
|
||||
end
|
||||
|
||||
describe "linked member – another user's member" do
|
||||
test "normal_user cannot update email of another user's linked member", %{actor: actor} do
|
||||
user_a = create_user_with_permission_set("own_data", actor)
|
||||
linked_member = create_linked_member_for_user(user_a, actor)
|
||||
|
||||
normal_user_b = create_user_with_permission_set("normal_user", actor)
|
||||
new_email = "other#{System.unique_integer([:positive])}@example.com"
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{} = error} =
|
||||
Membership.update_member(linked_member, %{email: new_email}, actor: normal_user_b)
|
||||
|
||||
error_str = Exception.message(error)
|
||||
assert error_str =~ "administrators"
|
||||
assert error_str =~ "linked to users"
|
||||
end
|
||||
|
||||
test "admin can update email of linked member", %{actor: actor} do
|
||||
user_a = create_user_with_permission_set("own_data", actor)
|
||||
linked_member = create_linked_member_for_user(user_a, actor)
|
||||
admin = create_admin_user(actor)
|
||||
|
||||
new_email = "admin_changed#{System.unique_integer([:positive])}@example.com"
|
||||
|
||||
assert {:ok, updated} =
|
||||
Membership.update_member(linked_member, %{email: new_email}, actor: admin)
|
||||
|
||||
assert updated.email == new_email
|
||||
end
|
||||
end
|
||||
|
||||
describe "linked member – own member" do
|
||||
test "own_data user can update email of their own linked member", %{actor: actor} do
|
||||
own_data_user = create_user_with_permission_set("own_data", actor)
|
||||
linked_member = create_linked_member_for_user(own_data_user, actor)
|
||||
|
||||
{:ok, own_data_user} =
|
||||
Ash.get(Accounts.User, own_data_user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
{:ok, own_data_user} =
|
||||
Ash.load(own_data_user, :member, domain: Mv.Accounts, actor: actor)
|
||||
|
||||
new_email = "own_updated#{System.unique_integer([:positive])}@example.com"
|
||||
|
||||
assert {:ok, updated} =
|
||||
Membership.update_member(linked_member, %{email: new_email}, actor: own_data_user)
|
||||
|
||||
assert updated.email == new_email
|
||||
end
|
||||
|
||||
test "normal_user with linked member can update email of that same member", %{actor: actor} do
|
||||
normal_user = create_user_with_permission_set("normal_user", actor)
|
||||
linked_member = create_linked_member_for_user(normal_user, actor)
|
||||
|
||||
{:ok, normal_user} =
|
||||
Ash.get(Accounts.User, normal_user.id, domain: Mv.Accounts, load: [:role], actor: actor)
|
||||
|
||||
{:ok, normal_user} = Ash.load(normal_user, :member, domain: Mv.Accounts, actor: actor)
|
||||
|
||||
new_email = "normal_own#{System.unique_integer([:positive])}@example.com"
|
||||
|
||||
assert {:ok, updated} =
|
||||
Membership.update_member(linked_member, %{email: new_email}, actor: normal_user)
|
||||
|
||||
assert updated.email == new_email
|
||||
end
|
||||
end
|
||||
|
||||
describe "no-op / other fields" do
|
||||
test "updating only other attributes on linked member as normal_user does not trigger validation error",
|
||||
%{actor: actor} do
|
||||
user_a = create_user_with_permission_set("own_data", actor)
|
||||
linked_member = create_linked_member_for_user(user_a, actor)
|
||||
normal_user_b = create_user_with_permission_set("normal_user", actor)
|
||||
|
||||
assert {:ok, updated} =
|
||||
Membership.update_member(linked_member, %{first_name: "UpdatedName"},
|
||||
actor: normal_user_b
|
||||
)
|
||||
|
||||
assert updated.first_name == "UpdatedName"
|
||||
assert updated.email == linked_member.email
|
||||
end
|
||||
|
||||
test "updating email of linked member as admin succeeds", %{actor: actor} do
|
||||
user_a = create_user_with_permission_set("own_data", actor)
|
||||
linked_member = create_linked_member_for_user(user_a, actor)
|
||||
admin = create_admin_user(actor)
|
||||
|
||||
new_email = "admin_ok#{System.unique_integer([:positive])}@example.com"
|
||||
|
||||
assert {:ok, updated} =
|
||||
Membership.update_member(linked_member, %{email: new_email}, actor: admin)
|
||||
|
||||
assert updated.email == new_email
|
||||
end
|
||||
end
|
||||
|
||||
describe "read_only" do
|
||||
test "read_only cannot update any member (policy rejects before validation)", %{actor: actor} do
|
||||
read_only_user = create_user_with_permission_set("read_only", actor)
|
||||
linked_member = create_linked_member_for_user(read_only_user, actor)
|
||||
|
||||
{:ok, read_only_user} =
|
||||
Ash.get(Accounts.User, read_only_user.id,
|
||||
domain: Mv.Accounts,
|
||||
load: [:role],
|
||||
actor: actor
|
||||
)
|
||||
|
||||
assert {:error, %Ash.Error.Forbidden{}} =
|
||||
Membership.update_member(linked_member, %{email: "changed@example.com"},
|
||||
actor: read_only_user
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue