feat(auth): add User resource authorization policies

Implement bypass for READ + HasPermission for UPDATE pattern
Extend HasPermission check to support User resource scope :own
This commit is contained in:
Moritz 2026-01-22 19:19:22 +01:00
parent a9f9cab96a
commit 429042cbba
2 changed files with 72 additions and 5 deletions

View file

@ -95,11 +95,25 @@ defmodule Mv.Authorization.Checks.HasPermission do
resource_name
) do
:authorized ->
# For :all scope, authorize directly
{:ok, true}
{:filter, filter_expr} ->
# For strict_check on single records, evaluate the filter against the record
evaluate_filter_for_strict_check(filter_expr, actor, record, resource_name)
# For :own/:linked scope:
# - With a record, evaluate filter against record for strict authorization
# - Without a record (queries/lists), return false
#
# NOTE: Returning false here forces the use of expr-based bypass policies.
# This is necessary because Ash's policy evaluation doesn't reliably call auto_filter
# when strict_check returns :unknown. Instead, resources should use bypass policies
# with expr() directly for filter-based authorization (see User resource).
if record do
evaluate_filter_for_strict_check(filter_expr, actor, record, resource_name)
else
# No record yet (e.g., read/list queries) - deny at strict_check level
# Resources must use expr-based bypass policies for list filtering
{:ok, false}
end
false ->
{:ok, false}
@ -224,9 +238,18 @@ defmodule Mv.Authorization.Checks.HasPermission do
end
# Evaluate filter expression for strict_check on single records
# 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
{"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