Add PermissionSets for Group, MemberGroup, MembershipFeeType, MembershipFeeCycle

- Extend permission_sets.ex with resources and pages for new domains
- Adjust HasPermission check for resource/action/scope
- Update roles-and-permissions and implementation-plan docs
- Add permission_sets_test.exs coverage
This commit is contained in:
Moritz 2026-02-03 23:52:09 +01:00
parent 36b7031dca
commit 893f9453bd
5 changed files with 449 additions and 113 deletions

View file

@ -58,6 +58,18 @@ defmodule Mv.Authorization.PermissionSets do
pages: [String.t()]
}
# DRY helpers for shared resource permission lists (used in own_data, read_only, normal_user)
defp perm(resource, action, scope),
do: %{resource: resource, action: action, scope: scope, granted: true}
# User: read/update own credentials only (all non-admin sets allow password changes)
defp user_own_credentials, do: [perm("User", :read, :own), perm("User", :update, :own)]
defp group_read_all, do: [perm("Group", :read, :all)]
defp custom_field_read_all, do: [perm("CustomField", :read, :all)]
defp membership_fee_type_read_all, do: [perm("MembershipFeeType", :read, :all)]
defp membership_fee_cycle_read_all, do: [perm("MembershipFeeCycle", :read, :all)]
@doc """
Returns the list of all valid permission set names.
@ -94,29 +106,21 @@ defmodule Mv.Authorization.PermissionSets do
def get_permissions(:own_data) do
%{
resources: [
# User: Can read/update own credentials only
# IMPORTANT: "read_only" refers to member data, NOT user credentials.
# All permission sets grant User.update :own to allow password changes.
%{resource: "User", action: :read, scope: :own, granted: true},
%{resource: "User", action: :update, scope: :own, granted: true},
# Member: Can read/update linked member
%{resource: "Member", action: :read, scope: :linked, granted: true},
%{resource: "Member", action: :update, scope: :linked, granted: true},
# 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},
# Group: Can read all (needed for viewing groups)
%{resource: "Group", action: :read, scope: :all, granted: true}
],
resources:
user_own_credentials() ++
[
perm("Member", :read, :linked),
perm("Member", :update, :linked),
perm("CustomFieldValue", :read, :linked),
perm("CustomFieldValue", :update, :linked),
perm("CustomFieldValue", :create, :linked),
perm("CustomFieldValue", :destroy, :linked)
] ++
custom_field_read_all() ++
group_read_all() ++
[perm("MemberGroup", :read, :linked)] ++
membership_fee_type_read_all() ++
membership_fee_cycle_read_all(),
pages: [
# No "/" - Mitglied must not see member index at root (same content as /members).
# Own profile (sidebar links to /users/:id) and own user edit
@ -133,25 +137,17 @@ defmodule Mv.Authorization.PermissionSets do
def get_permissions(:read_only) do
%{
resources: [
# User: Can read/update own credentials only
# IMPORTANT: "read_only" refers to member data, NOT user credentials.
# All permission sets grant User.update :own to allow password changes.
%{resource: "User", action: :read, scope: :own, granted: true},
%{resource: "User", action: :update, scope: :own, granted: true},
# Member: Can read all members, no modifications
%{resource: "Member", action: :read, scope: :all, granted: true},
# CustomFieldValue: Can read all custom field values
%{resource: "CustomFieldValue", action: :read, scope: :all, granted: true},
# CustomField: Can read all
%{resource: "CustomField", action: :read, scope: :all, granted: true},
# Group: Can read all
%{resource: "Group", action: :read, scope: :all, granted: true}
],
resources:
user_own_credentials() ++
[
perm("Member", :read, :all),
perm("CustomFieldValue", :read, :all)
] ++
custom_field_read_all() ++
group_read_all() ++
[perm("MemberGroup", :read, :all)] ++
membership_fee_type_read_all() ++
membership_fee_cycle_read_all(),
pages: [
"/",
# Own profile (sidebar links to /users/:id; redirect target must be allowed)
@ -176,31 +172,37 @@ defmodule Mv.Authorization.PermissionSets do
def get_permissions(:normal_user) do
%{
resources: [
# User: Can read/update own credentials only
# IMPORTANT: "read_only" refers to member data, NOT user credentials.
# All permission sets grant User.update :own to allow password changes.
%{resource: "User", action: :read, scope: :own, granted: true},
%{resource: "User", action: :update, scope: :own, granted: true},
# Member: Full CRUD except destroy (safety)
%{resource: "Member", action: :read, scope: :all, granted: true},
%{resource: "Member", action: :create, scope: :all, granted: true},
%{resource: "Member", action: :update, scope: :all, granted: true},
# Note: destroy intentionally omitted for safety
# CustomFieldValue: Full CRUD
%{resource: "CustomFieldValue", action: :read, scope: :all, granted: true},
%{resource: "CustomFieldValue", action: :create, scope: :all, granted: true},
%{resource: "CustomFieldValue", action: :update, scope: :all, granted: true},
%{resource: "CustomFieldValue", action: :destroy, scope: :all, granted: true},
# CustomField: Read only (admin manages definitions)
%{resource: "CustomField", action: :read, scope: :all, granted: true},
# Group: Can read all
%{resource: "Group", action: :read, scope: :all, granted: true}
],
resources:
user_own_credentials() ++
[
perm("Member", :read, :all),
perm("Member", :create, :all),
perm("Member", :update, :all),
# destroy intentionally omitted for safety
perm("CustomFieldValue", :read, :all),
perm("CustomFieldValue", :create, :all),
perm("CustomFieldValue", :update, :all),
perm("CustomFieldValue", :destroy, :all)
] ++
custom_field_read_all() ++
[
perm("Group", :read, :all),
perm("Group", :create, :all),
perm("Group", :update, :all),
perm("Group", :destroy, :all)
] ++
[
perm("MemberGroup", :read, :all),
perm("MemberGroup", :create, :all),
perm("MemberGroup", :destroy, :all)
] ++
membership_fee_type_read_all() ++
[
perm("MembershipFeeCycle", :read, :all),
perm("MembershipFeeCycle", :create, :all),
perm("MembershipFeeCycle", :update, :all),
perm("MembershipFeeCycle", :destroy, :all)
],
pages: [
"/",
# Own profile (sidebar links to /users/:id; redirect target must be allowed)
@ -221,8 +223,12 @@ defmodule Mv.Authorization.PermissionSets do
"/custom_field_values/:id/edit",
# Groups overview
"/groups",
# Create group
"/groups/new",
# Group detail
"/groups/:slug"
"/groups/:slug",
# Edit group
"/groups/:slug/edit"
]
}
end
@ -264,9 +270,29 @@ defmodule Mv.Authorization.PermissionSets do
%{resource: "Group", action: :read, scope: :all, granted: true},
%{resource: "Group", action: :create, scope: :all, granted: true},
%{resource: "Group", action: :update, scope: :all, granted: true},
%{resource: "Group", action: :destroy, scope: :all, granted: true}
%{resource: "Group", action: :destroy, scope: :all, granted: true},
# MemberGroup: Full CRUD
%{resource: "MemberGroup", action: :read, scope: :all, granted: true},
%{resource: "MemberGroup", action: :create, scope: :all, granted: true},
%{resource: "MemberGroup", action: :destroy, scope: :all, granted: true},
# MembershipFeeType: Full CRUD (admin manages fee types)
%{resource: "MembershipFeeType", action: :read, scope: :all, granted: true},
%{resource: "MembershipFeeType", action: :create, scope: :all, granted: true},
%{resource: "MembershipFeeType", action: :update, scope: :all, granted: true},
%{resource: "MembershipFeeType", action: :destroy, scope: :all, granted: true},
# MembershipFeeCycle: Full CRUD
%{resource: "MembershipFeeCycle", action: :read, scope: :all, granted: true},
%{resource: "MembershipFeeCycle", action: :create, scope: :all, granted: true},
%{resource: "MembershipFeeCycle", action: :update, scope: :all, granted: true},
%{resource: "MembershipFeeCycle", action: :destroy, scope: :all, granted: true}
],
pages: [
# Explicit admin-only pages (for clarity and future restrictions)
"/settings",
"/membership_fee_settings",
# Wildcard: Admin can access all pages
"*"
]