Document CustomFieldValue policies and own_data create/destroy in architecture

Update roles-and-permissions-architecture.md with policy layout and
permission matrix for CustomFieldValue (linked).
This commit is contained in:
Moritz 2026-01-27 13:40:38 +01:00 committed by moritz
parent 4e032ea778
commit db95979bf5

View file

@ -501,9 +501,11 @@ defmodule Mv.Authorization.PermissionSets do
%{resource: "Member", action: :read, scope: :linked, granted: true}, %{resource: "Member", action: :read, scope: :linked, granted: true},
%{resource: "Member", action: :update, scope: :linked, granted: true}, %{resource: "Member", action: :update, scope: :linked, granted: true},
# CustomFieldValue: Can read/update custom field values of linked member # CustomFieldValue: Can read/update/create/destroy custom field values of linked member
%{resource: "CustomFieldValue", action: :read, scope: :linked, granted: true}, %{resource: "CustomFieldValue", action: :read, scope: :linked, granted: true},
%{resource: "CustomFieldValue", action: :update, scope: :linked, granted: true}, %{resource: "CustomFieldValue", action: :update, scope: :linked, granted: true},
%{resource: "CustomFieldValue", action: :create, scope: :linked, granted: true},
%{resource: "CustomFieldValue", action: :destroy, scope: :linked, granted: true},
# CustomField: Can read all (needed for forms) # CustomField: Can read all (needed for forms)
%{resource: "CustomField", action: :read, scope: :all, granted: true} %{resource: "CustomField", action: :read, scope: :all, granted: true}
@ -678,7 +680,7 @@ Quick reference table showing what each permission set allows:
| **User** (all) | - | - | - | R, C, U, D | | **User** (all) | - | - | - | R, C, U, D |
| **Member** (linked) | R, U | - | - | - | | **Member** (linked) | R, U | - | - | - |
| **Member** (all) | - | R | R, C, U | R, C, U, D | | **Member** (all) | - | R | R, C, U | R, C, U, D |
| **CustomFieldValue** (linked) | R, U | - | - | - | | **CustomFieldValue** (linked) | R, U, C, D | - | - | - |
| **CustomFieldValue** (all) | - | R | R, C, U, D | R, C, U, D | | **CustomFieldValue** (all) | - | R | R, C, U, D | R, C, U, D |
| **CustomField** (all) | R | R | R | R, C, U, D | | **CustomField** (all) | R | R | R | R, C, U, D |
| **Role** (all) | - | - | - | R, C, U, D | | **Role** (all) | - | - | - | R, C, U, D |
@ -1053,47 +1055,47 @@ end
### CustomFieldValue Resource Policies ### CustomFieldValue Resource Policies
**Location:** `lib/mv/membership/custom_field_value.ex` **Location:** `lib/membership/custom_field_value.ex`
**Special Case:** Users can access custom field values of their linked member. **Pattern:** Bypass for READ (list queries), CustomFieldValueCreateScope for create (no filter), HasPermission for read/update/destroy. Create uses a dedicated check because Ash cannot apply filters to create actions.
```elixir ```elixir
defmodule Mv.Membership.CustomFieldValue do defmodule Mv.Membership.CustomFieldValue do
use Ash.Resource, ... use Ash.Resource, ...
policies do policies do
# SPECIAL CASE: Users can access custom field values of their linked member # Bypass for READ (list queries; expr triggers auto_filter)
# Note: This uses member_id relationship (CustomFieldValue.member_id → Member.id → User.member_id) bypass action_type(:read) do
policy action_type([:read, :update]) do description "Users can read custom field values of their linked member"
description "Users can access custom field values of their linked member"
authorize_if expr(member_id == ^actor(:member_id)) authorize_if expr(member_id == ^actor(:member_id))
end end
# GENERAL: Check permissions from role # CREATE: CustomFieldValueCreateScope (no filter; Ash rejects filters on create)
policy action_type([:read, :create, :update, :destroy]) do # own_data -> create when member_id == actor.member_id; normal_user/admin -> create (scope :all)
description "Check permissions from user's role" policy action_type(:create) do
authorize_if Mv.Authorization.Checks.CustomFieldValueCreateScope
end
# READ/UPDATE/DESTROY: HasPermission (scope :linked / :all)
policy action_type([:read, :update, :destroy]) do
authorize_if Mv.Authorization.Checks.HasPermission authorize_if Mv.Authorization.Checks.HasPermission
end end
# DEFAULT: Ash implicitly forbids if no policy authorized (fail-closed)
# DEFAULT: Forbid
policy action_type([:read, :create, :update, :destroy]) do
forbid_if always()
end end
end end
# ...
end
``` ```
**Permission Matrix:** **Permission Matrix:**
| Action | Mitglied | Vorstand | Kassenwart | Buchhaltung | Admin | | Action | Mitglied | Vorstand | Kassenwart | Buchhaltung | Admin |
|--------|----------|----------|------------|-------------|-------| |--------|----------|----------|------------|-------------|-------|
| Read linked | ✅ (special) | ✅ (if linked) | ✅ | ✅ (if linked) | ✅ | | Read linked | ✅ (bypass) | ✅ (if linked) | ✅ | ✅ (if linked) | ✅ |
| Update linked | ✅ (special) | ❌ | ✅ | ❌ | ✅ | | Update linked | ✅ (scope :linked) | ❌ | ✅ | ❌ | ✅ |
| Create linked | ✅ (CustomFieldValueCreateScope) | ❌ | ✅ | ❌ | ✅ |
| Destroy linked | ✅ (scope :linked) | ❌ | ✅ | ❌ | ✅ |
| Read all | ❌ | ✅ | ✅ | ✅ | ✅ | | Read all | ❌ | ✅ | ✅ | ✅ | ✅ |
| Create | ❌ | ❌ | ✅ | ❌ | ✅ | | Create all | ❌ | ❌ | ✅ | ❌ | ✅ |
| Destroy | ❌ | ❌ | ✅ | ❌ | ✅ | | Destroy all | ❌ | ❌ | ✅ | ❌ | ✅ |
### CustomField Resource Policies ### CustomField Resource Policies