Complete Permissions for Groups, Membership Fees, and User Role Assignment closes #404 #405
5 changed files with 449 additions and 113 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:**
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"*"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue