refactor(authorization): unify own_data read check across linked resources
This commit is contained in:
parent
7d712f6ce2
commit
070d9d1fc3
5 changed files with 76 additions and 127 deletions
|
|
@ -63,7 +63,7 @@ defmodule Mv.Membership.MemberGroup do
|
||||||
policies do
|
policies do
|
||||||
bypass action_type(:read) do
|
bypass action_type(:read) do
|
||||||
description "own_data: read only member_groups where member_id == actor.member_id"
|
description "own_data: read only member_groups where member_id == actor.member_id"
|
||||||
authorize_if Mv.Authorization.Checks.MemberGroupReadLinkedForOwnData
|
authorize_if {Mv.Authorization.Checks.ReadLinkedForOwnData, member_id_field: :member_id}
|
||||||
end
|
end
|
||||||
|
|
||||||
policy action_type(:read) do
|
policy action_type(:read) do
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ defmodule Mv.MembershipFees.MembershipFeeCycle do
|
||||||
policies do
|
policies do
|
||||||
bypass action_type(:read) do
|
bypass action_type(:read) do
|
||||||
description "own_data: read only cycles where member_id == actor.member_id"
|
description "own_data: read only cycles where member_id == actor.member_id"
|
||||||
authorize_if Mv.Authorization.Checks.MembershipFeeCycleReadLinkedForOwnData
|
authorize_if {Mv.Authorization.Checks.ReadLinkedForOwnData, member_id_field: :member_id}
|
||||||
end
|
end
|
||||||
|
|
||||||
policy action_type([:read, :create, :update, :destroy]) do
|
policy action_type([:read, :create, :update, :destroy]) do
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
defmodule Mv.Authorization.Checks.MembershipFeeCycleReadLinkedForOwnData do
|
|
||||||
@moduledoc """
|
|
||||||
Policy check for MembershipFeeCycle 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): return :unknown so authorizer applies auto_filter.
|
|
||||||
"""
|
|
||||||
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 membership_fee_cycles 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
|
|
||||||
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
|
|
||||||
74
lib/mv/authorization/checks/read_linked_for_own_data.ex
Normal file
74
lib/mv/authorization/checks/read_linked_for_own_data.ex
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
defmodule Mv.Authorization.Checks.ReadLinkedForOwnData do
|
||||||
|
@moduledoc """
|
||||||
|
Generic policy check for resources that link to a member via a member-id
|
||||||
|
attribute: read is allowed only when the actor has the "own_data" permission
|
||||||
|
set AND `record.<member_id_field> == actor.member_id`.
|
||||||
|
|
||||||
|
Used in a read bypass so that own_data gets the linked filter (via auto_filter
|
||||||
|
for list queries), while admin with a member_id does not match and falls
|
||||||
|
through to `HasPermission` for `:all`.
|
||||||
|
|
||||||
|
- With a record (e.g. get by id): returns true only when own_data and the
|
||||||
|
member ids match.
|
||||||
|
- Without a record (list query) + own_data: returns `:unknown` so the
|
||||||
|
authorizer applies `auto_filter`.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
- `:member_id_field` - the attribute on the resource holding the member id.
|
||||||
|
Defaults to `:member_id`.
|
||||||
|
"""
|
||||||
|
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 records where #{member_id_field(opts)} == actor.member_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def strict_check(actor, authorizer, opts) do
|
||||||
|
field = member_id_field(opts)
|
||||||
|
record = get_record_from_authorizer(authorizer)
|
||||||
|
is_own_data = ActorPermissionSetIs.match?(actor, authorizer, permission_set_name: "own_data")
|
||||||
|
|
||||||
|
cond do
|
||||||
|
is_nil(record) and is_own_data ->
|
||||||
|
{:ok, :unknown}
|
||||||
|
|
||||||
|
is_nil(record) ->
|
||||||
|
{:ok, false}
|
||||||
|
|
||||||
|
not is_own_data ->
|
||||||
|
{:ok, false}
|
||||||
|
|
||||||
|
Map.get(record, field) == 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_field(opts), actor.member_id}]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp member_id_field(opts), do: Keyword.get(opts, :member_id_field, :member_id)
|
||||||
|
|
||||||
|
defp get_record_from_authorizer(authorizer) do
|
||||||
|
case authorizer.subject do
|
||||||
|
%{data: data} when not is_nil(data) -> data
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue