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

@ -1025,16 +1025,22 @@ defmodule Mv.Membership.Member do
authorize_if expr(id == ^actor(:member_id))
end
# 2. GENERAL: Forbid user link unless admin; then check permissions from role
# ForbidMemberUserLinkUnlessAdmin: only admins may pass :user on create/update (no-op for read/destroy)
# 2. READ/DESTROY: Check permissions only (no :user argument on these actions)
policy action_type([:read, :destroy]) do
description "Check permissions from user's role"
authorize_if Mv.Authorization.Checks.HasPermission
end
# 3. CREATE/UPDATE: Forbid user link unless admin; then check permissions
# ForbidMemberUserLinkUnlessAdmin: only admins may pass :user (link or unlink via nil/empty).
# 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 and forbid user link unless admin"
policy action_type([:create, :update]) do
description "Forbid user link unless admin; then check permissions"
forbid_if Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin
authorize_if Mv.Authorization.Checks.HasPermission
end
# 3. DEFAULT: Ash implicitly forbids if no policy authorizes (fail-closed)
# 4. DEFAULT: Ash implicitly forbids if no policy authorizes (fail-closed)
end
# Custom validation for email editing (see Special Cases section)
@ -1053,7 +1059,7 @@ end
- **READ list queries**: No record at strict_check time → bypass with `expr(id == ^actor(:member_id))` needed for auto_filter ✅
- **UPDATE operations**: Changeset contains record → HasPermission evaluates `scope :linked` correctly ✅
**Usermember link:** Only admins may set or change the `:user` argument on create_member or update_member (see [User-Member Linking](#user-member-linking)). Non-admins can create/update members without passing `:user`.
**Usermember link:** Only admins may pass the `:user` argument on create_member or update_member (link or unlink via `user: nil`/`user: %{}`). The check uses **argument presence** (key in arguments), not value, to avoid bypass (see [User-Member Linking](#user-member-linking)).
**Permission Matrix:**