Docs: permission hardening Role and member user link

Role: Ash policies (HasPermission); read for all, create/update/destroy admin only.
User–member link: only admins may set :user on Member create/update (ForbidMemberUserLinkUnlessAdmin).
This commit is contained in:
Moritz 2026-02-04 12:54:15 +01:00
parent 26fbafdd9d
commit 54e419ed4c
Signed by: moritz
GPG key ID: 1020A035E5DD0824

View file

@ -1025,17 +1025,16 @@ defmodule Mv.Membership.Member do
authorize_if expr(id == ^actor(:member_id)) authorize_if expr(id == ^actor(:member_id))
end end
# 2. GENERAL: Check permissions from role # 2. GENERAL: Forbid user link unless admin; then check permissions from role
# - :own_data → can UPDATE linked member (scope :linked via HasPermission) # ForbidMemberUserLinkUnlessAdmin: only admins may pass :user on create/update (no-op for read/destroy)
# - :read_only → can READ all members (scope :all), no update permission # HasPermission: :own_data → update linked; :read_only → no update; :normal_user/admin → update all
# - :normal_user → can CRUD all members (scope :all)
# - :admin → can CRUD all members (scope :all)
policy action_type([:read, :create, :update, :destroy]) do policy action_type([:read, :create, :update, :destroy]) do
description "Check permissions from user's role" description "Check permissions and forbid user link unless admin"
forbid_if Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin
authorize_if Mv.Authorization.Checks.HasPermission authorize_if Mv.Authorization.Checks.HasPermission
end end
# 4. DEFAULT: Ash implicitly forbids if no policy authorizes (fail-closed) # 3. DEFAULT: Ash implicitly forbids if no policy authorizes (fail-closed)
end end
# Custom validation for email editing (see Special Cases section) # Custom validation for email editing (see Special Cases section)
@ -1054,6 +1053,8 @@ end
- **READ list queries**: No record at strict_check time → bypass with `expr(id == ^actor(:member_id))` needed for auto_filter ✅ - **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 ✅ - **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`.
**Permission Matrix:** **Permission Matrix:**
| Action | Mitglied | Vorstand | Kassenwart | Buchhaltung | Admin | | Action | Mitglied | Vorstand | Kassenwart | Buchhaltung | Admin |
@ -1148,23 +1149,20 @@ end
**Location:** `lib/mv/authorization/role.ex` **Location:** `lib/mv/authorization/role.ex`
**Special Protection:** System roles cannot be deleted. **Defense-in-depth:** The Role resource uses `authorizers: [Ash.Policy.Authorizer]` and policies with `Mv.Authorization.Checks.HasPermission`. **Read** is allowed for all permission sets (own_data, read_only, normal_user, admin) via `perm("Role", :read, :all)` in PermissionSets; reading roles is not a security concern. **Create, update, and destroy** are allowed only for admin (admin has full Role CRUD in PermissionSets). Seeds and bootstrap use `authorize?: false` where necessary.
**Special Protection:** System roles cannot be deleted (validation on destroy).
```elixir ```elixir
defmodule Mv.Authorization.Role do defmodule Mv.Authorization.Role do
use Ash.Resource, ... use Ash.Resource,
authorizers: [Ash.Policy.Authorizer]
policies do policies do
# Only admin can manage roles
policy action_type([:read, :create, :update, :destroy]) do policy action_type([:read, :create, :update, :destroy]) do
description "Check permissions from user's role" description "Check permissions from user's role (read all, create/update/destroy admin only)"
authorize_if Mv.Authorization.Checks.HasPermission authorize_if Mv.Authorization.Checks.HasPermission
end end
# DEFAULT: Forbid
policy action_type([:read, :create, :update, :destroy]) do
forbid_if always()
end
end end
# Prevent deletion of system roles # Prevent deletion of system roles
@ -1201,7 +1199,7 @@ end
| Action | Mitglied | Vorstand | Kassenwart | Buchhaltung | Admin | | Action | Mitglied | Vorstand | Kassenwart | Buchhaltung | Admin |
|--------|----------|----------|------------|-------------|-------| |--------|----------|----------|------------|-------------|-------|
| Read | ❌ | ❌ | ❌ | ❌ | ✅ | | Read | ✅ | ✅ | ✅ | ✅ | ✅ |
| Create | ❌ | ❌ | ❌ | ❌ | ✅ | | Create | ❌ | ❌ | ❌ | ❌ | ✅ |
| Update | ❌ | ❌ | ❌ | ❌ | ✅ | | Update | ❌ | ❌ | ❌ | ❌ | ✅ |
| Destroy* | ❌ | ❌ | ❌ | ❌ | ✅ | | Destroy* | ❌ | ❌ | ❌ | ❌ | ✅ |
@ -2045,7 +2043,10 @@ Users and Members are separate entities that can be linked. Special rules:
- A user cannot link themselves to an existing member - A user cannot link themselves to an existing member
- A user CAN create a new member and be directly linked to it (self-service) - A user CAN create a new member and be directly linked to it (self-service)
**Enforcement:** The User resource restricts the `update_user` action (which accepts the `member` argument for link/unlink) to admins only via `Mv.Authorization.Checks.ActorIsAdmin`. The UserLive.Form shows the Member-Linking UI and runs member link/unlink on save only when the current user is admin; non-admins use the `:update` action (email only) for profile edit. **Enforcement:**
- **User side:** The User resource restricts the `update_user` action (which accepts the `member` argument for link/unlink) to admins only via `Mv.Authorization.Checks.ActorIsAdmin`. The UserLive.Form shows the Member-Linking UI and runs member link/unlink on save only when the current user is admin; non-admins use the `:update` action (email only) for profile edit.
- **Member side:** Only admins may set or change the usermember link on **Member** create or update. When creating or updating a member, the `:user` argument (which links the member to a user account) is forbidden for non-admins. This is enforced by `Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin` in the Member resource policies (`forbid_if` before `authorize_if HasPermission`). Non-admins (e.g. normal_user / Kassenwart) can still create and update members as long as they do not pass the `:user` argument.
### Approach: Separate Ash Actions ### Approach: Separate Ash Actions