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

@ -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 ->