MemberGroup: restrict bypass to own_data via MemberGroupReadLinkedForOwnData

- ActorPermissionSetIs check; bypass policy filters by member_id for own_data only.
- Admin with member_id still gets :all via HasPermission. Tests added.
This commit is contained in:
Moritz 2026-02-04 09:19:57 +01:00
parent 67ce514ba0
commit 890a4d3752
4 changed files with 143 additions and 4 deletions

View file

@ -0,0 +1,44 @@
defmodule Mv.Authorization.Checks.ActorPermissionSetIs do
@moduledoc """
Policy check: true when the actor's role has the given permission_set_name.
Used to restrict bypass policies (e.g. MemberGroup read by member_id) to actors
with a specific permission set (e.g. "own_data") so that admin with member_id
still gets :all scope from HasPermission, not the bypass filter.
## Usage
# In a resource policy (both conditions must hold for the bypass)
bypass action_type(:read) do
authorize_if expr(member_id == ^actor(:member_id))
authorize_if {Mv.Authorization.Checks.ActorPermissionSetIs, permission_set_name: "own_data"}
end
## Options
- `:permission_set_name` (required) - String or atom, e.g. `"own_data"` or `:own_data`
"""
use Ash.Policy.SimpleCheck
alias Mv.Authorization.Actor
@impl true
def describe(opts) do
name = opts[:permission_set_name] || "?"
"actor has permission set #{name}"
end
@impl true
def match?(actor, _context, opts) do
case opts[:permission_set_name] do
nil ->
false
expected ->
case Actor.permission_set_name(actor) do
nil -> false
actual -> to_string(expected) == to_string(actual)
end
end
end
end

View file

@ -0,0 +1,63 @@
defmodule Mv.Authorization.Checks.MemberGroupReadLinkedForOwnData do
@moduledoc """
Policy check for MemberGroup read: true only when actor has permission set "own_data"
AND record.member_id == actor.member_id.
Used in a bypass so that own_data gets the linked filter (via auto_filter for list queries),
while admin with member_id does not match and gets :all from HasPermission.
- With a record (e.g. get by id): returns true only when own_data and member_id match.
- Without a record (list query): strict_check returns false; auto_filter adds filter when own_data.
"""
use Ash.Policy.Check
alias Mv.Authorization.Checks.ActorPermissionSetIs
@impl true
def type, do: :filter
@impl true
def describe(_opts),
do: "own_data can read only member_groups where member_id == actor.member_id"
@impl true
def strict_check(actor, authorizer, _opts) do
record = get_record_from_authorizer(authorizer)
is_own_data = ActorPermissionSetIs.match?(actor, authorizer, permission_set_name: "own_data")
cond do
# List query + own_data: return :unknown so authorizer applies auto_filter (keyword list)
is_nil(record) and is_own_data ->
{:ok, :unknown}
is_nil(record) ->
{:ok, false}
not is_own_data ->
{:ok, false}
record.member_id == actor.member_id ->
{:ok, true}
true ->
{:ok, false}
end
end
@impl true
def auto_filter(actor, _authorizer, _opts) do
if ActorPermissionSetIs.match?(actor, nil, permission_set_name: "own_data") &&
Map.get(actor, :member_id) do
[member_id: actor.member_id]
else
[]
end
end
defp get_record_from_authorizer(authorizer) do
case authorizer.subject do
%{data: data} when not is_nil(data) -> data
_ -> nil
end
end
end