From 7bd4b4154696e83e76b665a5846f374bb167204b Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 27 Jan 2026 13:40:38 +0100 Subject: [PATCH] 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). --- docs/roles-and-permissions-architecture.md | 44 +++++++++++----------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/docs/roles-and-permissions-architecture.md b/docs/roles-and-permissions-architecture.md index 8934688..acea99e 100644 --- a/docs/roles-and-permissions-architecture.md +++ b/docs/roles-and-permissions-architecture.md @@ -501,9 +501,11 @@ defmodule Mv.Authorization.PermissionSets do %{resource: "Member", action: :read, 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: :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) %{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 | | **Member** (linked) | R, U | - | - | - | | **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 | | **CustomField** (all) | R | R | R | R, C, U, D | | **Role** (all) | - | - | - | R, C, U, D | @@ -1053,35 +1055,33 @@ end ### 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 defmodule Mv.Membership.CustomFieldValue do use Ash.Resource, ... policies do - # SPECIAL CASE: Users can access custom field values of their linked member - # Note: This uses member_id relationship (CustomFieldValue.member_id → Member.id → User.member_id) - policy action_type([:read, :update]) do - description "Users can access custom field values of their linked member" + # Bypass for READ (list queries; expr triggers auto_filter) + bypass action_type(:read) do + description "Users can read custom field values of their linked member" authorize_if expr(member_id == ^actor(:member_id)) end - # GENERAL: Check permissions from role - policy action_type([:read, :create, :update, :destroy]) do - description "Check permissions from user's role" - authorize_if Mv.Authorization.Checks.HasPermission + # CREATE: CustomFieldValueCreateScope (no filter; Ash rejects filters on create) + # own_data -> create when member_id == actor.member_id; normal_user/admin -> create (scope :all) + policy action_type(:create) do + authorize_if Mv.Authorization.Checks.CustomFieldValueCreateScope end - # DEFAULT: Forbid - policy action_type([:read, :create, :update, :destroy]) do - forbid_if always() + # READ/UPDATE/DESTROY: HasPermission (scope :linked / :all) + policy action_type([:read, :update, :destroy]) do + authorize_if Mv.Authorization.Checks.HasPermission end + # DEFAULT: Ash implicitly forbids if no policy authorized (fail-closed) end - - # ... end ``` @@ -1089,11 +1089,13 @@ end | Action | Mitglied | Vorstand | Kassenwart | Buchhaltung | Admin | |--------|----------|----------|------------|-------------|-------| -| Read linked | ✅ (special) | ✅ (if linked) | ✅ | ✅ (if linked) | ✅ | -| Update linked | ✅ (special) | ❌ | ✅ | ❌ | ✅ | +| Read linked | ✅ (bypass) | ✅ (if linked) | ✅ | ✅ (if linked) | ✅ | +| Update linked | ✅ (scope :linked) | ❌ | ✅ | ❌ | ✅ | +| Create linked | ✅ (CustomFieldValueCreateScope) | ❌ | ✅ | ❌ | ✅ | +| Destroy linked | ✅ (scope :linked) | ❌ | ✅ | ❌ | ✅ | | Read all | ❌ | ✅ | ✅ | ✅ | ✅ | -| Create | ❌ | ❌ | ✅ | ❌ | ✅ | -| Destroy | ❌ | ❌ | ✅ | ❌ | ✅ | +| Create all | ❌ | ❌ | ✅ | ❌ | ✅ | +| Destroy all | ❌ | ❌ | ✅ | ❌ | ✅ | ### CustomField Resource Policies