From 893f9453bdfc23d8d1a4adea9576832b710631fd Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 3 Feb 2026 23:52:09 +0100 Subject: [PATCH] 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 --- docs/roles-and-permissions-architecture.md | 42 +++ ...les-and-permissions-implementation-plan.md | 5 +- lib/mv/authorization/checks/has_permission.ex | 80 +++--- lib/mv/authorization/permission_sets.ex | 164 ++++++----- .../mv/authorization/permission_sets_test.exs | 271 ++++++++++++++++++ 5 files changed, 449 insertions(+), 113 deletions(-) diff --git a/docs/roles-and-permissions-architecture.md b/docs/roles-and-permissions-architecture.md index dbf2353..461f5ec 100644 --- a/docs/roles-and-permissions-architecture.md +++ b/docs/roles-and-permissions-architecture.md @@ -97,6 +97,10 @@ Control CRUD operations on: - CustomFieldValue (custom field values) - CustomField (custom field definitions) - Role (role management) +- Group (group definitions; read all, create/update/destroy admin only) +- MemberGroup (member–group associations; own_data read :linked, read_only read :all, normal_user/admin create/destroy) +- MembershipFeeType (fee type definitions; all read, admin-only create/update/destroy) +- MembershipFeeCycle (fee cycles; all read, normal_user/admin read+create+update+destroy; manual "Regenerate Cycles" for normal_user and admin) **4. Page-Level Permissions** @@ -105,6 +109,7 @@ Control access to LiveView pages: - Show pages (detail views) - Form pages (create/edit) - Admin pages +- Settings pages: `/settings` and `/membership_fee_settings` are admin-only (explicit in PermissionSets) **5. Granular Scopes** @@ -121,6 +126,8 @@ Three scope levels for permissions: - **Linked Member Email:** Only admins can edit email of member linked to user - **System Roles:** "Mitglied" role cannot be deleted (is_system_role flag) - **User-Member Linking:** Only admins can link/unlink users and members +- **User Role Assignment:** Only admins can change a user's role (via `update_user` with `role_id`). Last-admin validation ensures at least one user keeps the Admin role. +- **Settings Pages:** `/settings` and `/membership_fee_settings` are admin-only (explicit in PermissionSets pages). **7. UI Consistency** @@ -684,6 +691,11 @@ Quick reference table showing what each permission set allows: | **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 | +| **Group** (all) | R | R | R | R, C, U, D | +| **MemberGroup** (linked) | R | - | - | - | +| **MemberGroup** (all) | - | R | R, C, D | R, C, D | +| **MembershipFeeType** (all) | R | R | R | R, C, U, D | +| **MembershipFeeCycle** (all) | R | R | R, C, U, D | R, C, U, D | **Legend:** R=Read, C=Create, U=Update, D=Destroy @@ -1195,6 +1207,36 @@ end *Cannot destroy if `is_system_role=true` +### User Role Assignment (Admin-Only) + +**Location:** `lib/accounts/user.ex` (update_user action), `lib/mv_web/live/user_live/form.ex` + +Only admins can change a user's role. The `update_user` action accepts `role_id`; the User form shows a role dropdown when `can?(actor, :update, Mv.Authorization.Role)`. **Last-admin validation:** If the only non-system admin tries to change their role, the change is rejected with "At least one user must keep the Admin role." (System user is excluded from the admin count.) See [User-Member Linking](#user-member-linking) for the same admin-only pattern. + +### Group Resource Policies + +**Location:** `lib/membership/group.ex` + +Policies use `HasPermission` for read/create/update/destroy. All permission sets can read; only admin can create, update, destroy. No bypass (scope :all only in PermissionSets). + +### MemberGroup Resource Policies + +**Location:** `lib/membership/member_group.ex` + +Bypass for read with `expr(member_id == ^actor(:member_id))` (own_data list); HasPermission for read (read_only/normal_user/admin :all) and create/destroy (normal_user + admin only). HasPermission applies `:linked` scope for MemberGroup (see HasPermission apply_scope). + +### MembershipFeeType Resource Policies + +**Location:** `lib/membership_fees/membership_fee_type.ex` + +Policies use `HasPermission` for read/create/update/destroy. All permission sets can read; only admin can create, update, destroy. + +### MembershipFeeCycle Resource Policies + +**Location:** `lib/membership_fees/membership_fee_cycle.ex` + +Policies use `HasPermission` for read/create/update/destroy. All can read; read_only cannot update/create/destroy; normal_user and admin can read, create, update, and destroy (including mark_as_paid and manual "Regenerate Cycles" in the member detail view; UI button is shown when `can_create_cycle`). + --- ## Page Permission System diff --git a/docs/roles-and-permissions-implementation-plan.md b/docs/roles-and-permissions-implementation-plan.md index 23b045c..95db031 100644 --- a/docs/roles-and-permissions-implementation-plan.md +++ b/docs/roles-and-permissions-implementation-plan.md @@ -78,10 +78,11 @@ Stored in database `roles` table, each referencing a `permission_set_name`: - ✅ Hardcoded PermissionSets module with 4 permission sets - ✅ Role database table and CRUD interface - ✅ Custom Ash Policy Check (`HasPermission`) that reads from PermissionSets -- ✅ Policies on all resources (Member, User, CustomFieldValue, CustomField, Role) -- ✅ Page-level permissions via Phoenix Plug +- ✅ Policies on all resources (Member, User, CustomFieldValue, CustomField, Role, Group, MemberGroup, MembershipFeeType, MembershipFeeCycle) +- ✅ Page-level permissions via Phoenix Plug (including admin-only `/settings` and `/membership_fee_settings`) - ✅ UI authorization helpers for conditional rendering - ✅ Special case: Member email validation for linked users +- ✅ User role assignment: admin-only `role_id` in update_user; Last-Admin validation; role dropdown in User form when `can?(actor, :update, Role)` - ✅ Seed data for 5 roles **Benefits of Hardcoded Approach:** diff --git a/lib/mv/authorization/checks/has_permission.ex b/lib/mv/authorization/checks/has_permission.ex index 774e767..f2b302d 100644 --- a/lib/mv/authorization/checks/has_permission.ex +++ b/lib/mv/authorization/checks/has_permission.ex @@ -50,6 +50,7 @@ defmodule Mv.Authorization.Checks.HasPermission do - **:linked** - Filters based on resource type: - Member: `id == actor.member_id` (User.member_id → Member.id, inverse relationship) - CustomFieldValue: `member_id == actor.member_id` (CustomFieldValue.member_id → Member.id → User.member_id) + - MemberGroup: `member_id == actor.member_id` (MemberGroup.member_id → Member.id → User.member_id) ## Error Handling @@ -278,36 +279,28 @@ defmodule Mv.Authorization.Checks.HasPermission do # For :own scope with User resource: id == actor.id # For :linked scope with Member resource: id == actor.member_id defp evaluate_filter_for_strict_check(_filter_expr, actor, record, resource_name) do - case {resource_name, record} do - {"User", %{id: user_id}} when not is_nil(user_id) -> - # Check if this user's ID matches the actor's ID (scope :own) - if user_id == actor.id do - {:ok, true} - else - {:ok, false} - end + result = + case {resource_name, record} do + # Scope :own + {"User", %{id: user_id}} when not is_nil(user_id) -> + user_id == actor.id - {"Member", %{id: member_id}} when not is_nil(member_id) -> - # Check if this member's ID matches the actor's member_id - if member_id == actor.member_id do - {:ok, true} - else - {:ok, false} - end + # Scope :linked + {"Member", %{id: member_id}} when not is_nil(member_id) -> + member_id == actor.member_id - {"CustomFieldValue", %{member_id: cfv_member_id}} when not is_nil(cfv_member_id) -> - # Check if this CFV's member_id matches the actor's member_id - if cfv_member_id == actor.member_id do - {:ok, true} - else - {:ok, false} - end + {"CustomFieldValue", %{member_id: cfv_member_id}} when not is_nil(cfv_member_id) -> + cfv_member_id == actor.member_id - _ -> - # For other cases or when record is not available, return :unknown - # This will cause Ash to use auto_filter instead - {:ok, :unknown} - end + {"MemberGroup", %{member_id: mg_member_id}} when not is_nil(mg_member_id) -> + mg_member_id == actor.member_id + + _ -> + :unknown + end + + out = if result == :unknown, do: {:ok, :unknown}, else: {:ok, result} + out end # Extract resource name from module (e.g., Mv.Membership.Member -> "Member") @@ -347,24 +340,16 @@ defmodule Mv.Authorization.Checks.HasPermission do defp apply_scope(:linked, actor, resource_name) do case resource_name do "Member" -> - # User.member_id → Member.id (inverse relationship) - # Filter: member.id == actor.member_id - # If actor has no member_id, return no results (use false or impossible condition) - if is_nil(actor.member_id) do - {:filter, expr(false)} - else - {:filter, expr(id == ^actor.member_id)} - end + # User.member_id → Member.id (inverse relationship). Filter: member.id == actor.member_id + linked_filter_by_member_id(actor, :id) "CustomFieldValue" -> # CustomFieldValue.member_id → Member.id → User.member_id - # Filter: custom_field_value.member_id == actor.member_id - # If actor has no member_id, return no results - if is_nil(actor.member_id) do - {:filter, expr(false)} - else - {:filter, expr(member_id == ^actor.member_id)} - end + linked_filter_by_member_id(actor, :member_id) + + "MemberGroup" -> + # MemberGroup.member_id → Member.id → User.member_id (own linked member's group associations) + linked_filter_by_member_id(actor, :member_id) _ -> # Fallback for other resources @@ -372,6 +357,17 @@ defmodule Mv.Authorization.Checks.HasPermission do end end + # Returns {:filter, expr(false)} if actor has no member_id; otherwise {:filter, expr(field == ^actor.member_id)}. + # Used for :linked scope on Member (field :id), CustomFieldValue and MemberGroup (field :member_id). + defp linked_filter_by_member_id(actor, _field) when is_nil(actor.member_id) do + {:filter, expr(false)} + end + + defp linked_filter_by_member_id(actor, :id), do: {:filter, expr(id == ^actor.member_id)} + + defp linked_filter_by_member_id(actor, :member_id), + do: {:filter, expr(member_id == ^actor.member_id)} + # Log authorization failures for debugging (lazy evaluation) defp log_auth_failure(actor, resource, action, reason) do Logger.debug(fn -> diff --git a/lib/mv/authorization/permission_sets.ex b/lib/mv/authorization/permission_sets.ex index 858748d..61c3fbf 100644 --- a/lib/mv/authorization/permission_sets.ex +++ b/lib/mv/authorization/permission_sets.ex @@ -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 "*" ] diff --git a/test/mv/authorization/permission_sets_test.exs b/test/mv/authorization/permission_sets_test.exs index 404a87e..9cd38fb 100644 --- a/test/mv/authorization/permission_sets_test.exs +++ b/test/mv/authorization/permission_sets_test.exs @@ -496,6 +496,277 @@ defmodule Mv.Authorization.PermissionSetsTest do assert "*" in permissions.pages end + + test "admin pages include explicit /settings and /membership_fee_settings" do + permissions = PermissionSets.get_permissions(:admin) + + assert "/settings" in permissions.pages + assert "/membership_fee_settings" in permissions.pages + end + end + + describe "get_permissions/1 - MemberGroup resource" do + test "own_data has MemberGroup read with scope :linked only" do + permissions = PermissionSets.get_permissions(:own_data) + + mg_read = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :read + end) + + mg_create = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :create + end) + + assert mg_read != nil + assert mg_read.scope == :linked + assert mg_read.granted == true + assert mg_create == nil || mg_create.granted == false + end + + test "read_only has MemberGroup read with scope :all, no create/destroy" do + permissions = PermissionSets.get_permissions(:read_only) + + mg_read = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :read + end) + + mg_create = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :create + end) + + mg_destroy = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :destroy + end) + + assert mg_read != nil + assert mg_read.scope == :all + assert mg_read.granted == true + assert mg_create == nil || mg_create.granted == false + assert mg_destroy == nil || mg_destroy.granted == false + end + + test "normal_user has MemberGroup read/create/destroy with scope :all" do + permissions = PermissionSets.get_permissions(:normal_user) + + mg_read = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :read + end) + + mg_create = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :create + end) + + mg_destroy = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :destroy + end) + + assert mg_read != nil + assert mg_read.scope == :all + assert mg_read.granted == true + assert mg_create != nil + assert mg_create.scope == :all + assert mg_create.granted == true + assert mg_destroy != nil + assert mg_destroy.scope == :all + assert mg_destroy.granted == true + end + + test "admin has MemberGroup read/create/destroy with scope :all" do + permissions = PermissionSets.get_permissions(:admin) + + mg_read = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :read + end) + + mg_create = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :create + end) + + mg_destroy = + Enum.find(permissions.resources, fn p -> + p.resource == "MemberGroup" && p.action == :destroy + end) + + assert mg_read != nil + assert mg_read.scope == :all + assert mg_read.granted == true + assert mg_create != nil + assert mg_create.granted == true + assert mg_destroy != nil + assert mg_destroy.granted == true + end + end + + describe "get_permissions/1 - MembershipFeeType resource" do + test "all permission sets have MembershipFeeType read with scope :all" do + for set <- PermissionSets.all_permission_sets() do + permissions = PermissionSets.get_permissions(set) + + mft_read = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeType" && p.action == :read + end) + + assert mft_read != nil, "Permission set #{set} should have MembershipFeeType read" + assert mft_read.scope == :all + assert mft_read.granted == true + end + end + + test "only admin has MembershipFeeType create/update/destroy" do + for set <- [:own_data, :read_only, :normal_user] do + permissions = PermissionSets.get_permissions(set) + + mft_create = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeType" && p.action == :create + end) + + mft_update = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeType" && p.action == :update + end) + + mft_destroy = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeType" && p.action == :destroy + end) + + assert mft_create == nil || mft_create.granted == false, + "Permission set #{set} should not allow MembershipFeeType create" + + assert mft_update == nil || mft_update.granted == false, + "Permission set #{set} should not allow MembershipFeeType update" + + assert mft_destroy == nil || mft_destroy.granted == false, + "Permission set #{set} should not allow MembershipFeeType destroy" + end + + admin_permissions = PermissionSets.get_permissions(:admin) + + mft_create = + Enum.find(admin_permissions.resources, fn p -> + p.resource == "MembershipFeeType" && p.action == :create + end) + + mft_update = + Enum.find(admin_permissions.resources, fn p -> + p.resource == "MembershipFeeType" && p.action == :update + end) + + mft_destroy = + Enum.find(admin_permissions.resources, fn p -> + p.resource == "MembershipFeeType" && p.action == :destroy + end) + + assert mft_create != nil + assert mft_create.scope == :all + assert mft_create.granted == true + assert mft_update != nil + assert mft_update.granted == true + assert mft_destroy != nil + assert mft_destroy.granted == true + end + end + + describe "get_permissions/1 - MembershipFeeCycle resource" do + test "all permission sets have MembershipFeeCycle read with scope :all" do + for set <- PermissionSets.all_permission_sets() do + permissions = PermissionSets.get_permissions(set) + + mfc_read = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :read + end) + + assert mfc_read != nil, "Permission set #{set} should have MembershipFeeCycle read" + assert mfc_read.scope == :all + assert mfc_read.granted == true + end + end + + test "read_only has MembershipFeeCycle read only, no update" do + permissions = PermissionSets.get_permissions(:read_only) + + mfc_update = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :update + end) + + assert mfc_update == nil || mfc_update.granted == false + end + + test "normal_user has MembershipFeeCycle read/create/update/destroy with scope :all" do + permissions = PermissionSets.get_permissions(:normal_user) + + mfc_read = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :read + end) + + mfc_create = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :create + end) + + mfc_update = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :update + end) + + mfc_destroy = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :destroy + end) + + assert mfc_read != nil && mfc_read.granted == true + assert mfc_create != nil && mfc_create.scope == :all && mfc_create.granted == true + assert mfc_update != nil && mfc_update.granted == true + assert mfc_destroy != nil && mfc_destroy.scope == :all && mfc_destroy.granted == true + end + + test "admin has MembershipFeeCycle read/create/update/destroy with scope :all" do + permissions = PermissionSets.get_permissions(:admin) + + mfc_read = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :read + end) + + mfc_create = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :create + end) + + mfc_update = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :update + end) + + mfc_destroy = + Enum.find(permissions.resources, fn p -> + p.resource == "MembershipFeeCycle" && p.action == :destroy + end) + + assert mfc_read != nil + assert mfc_read.granted == true + assert mfc_create != nil + assert mfc_create.granted == true + assert mfc_update != nil + assert mfc_update.granted == true + assert mfc_destroy != nil + assert mfc_destroy.granted == true + end end describe "valid_permission_set?/1" do