From c7c6b318acef273edb2110070bf805244ec21dd0 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 27 Jan 2026 13:40:17 +0100 Subject: [PATCH] Add CustomFieldValueCreateScope check for create actions Ash cannot apply filters to create; this check enforces :linked/:all scope via strict_check only (no filter). --- .../checks/custom_field_value_create_scope.ex | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 lib/mv/authorization/checks/custom_field_value_create_scope.ex diff --git a/lib/mv/authorization/checks/custom_field_value_create_scope.ex b/lib/mv/authorization/checks/custom_field_value_create_scope.ex new file mode 100644 index 0000000..f5be53d --- /dev/null +++ b/lib/mv/authorization/checks/custom_field_value_create_scope.ex @@ -0,0 +1,64 @@ +defmodule Mv.Authorization.Checks.CustomFieldValueCreateScope do + @moduledoc """ + Policy check for CustomFieldValue create actions only. + + Use this for create instead of HasPermission because Ash cannot apply + filters to create actions ("Cannot use a filter to authorize a create"). + This check performs the same scope logic as HasPermission for create + (PermissionSets + :linked/:all) but only implements strict_check, so it + never adds a filter. + + Used in CustomFieldValue policies: + policy action_type(:create) do + authorize_if Mv.Authorization.Checks.CustomFieldValueCreateScope + end + """ + use Ash.Policy.Check + alias Mv.Authorization.PermissionSets + require Logger + + @impl true + def describe(_opts), + do: "CustomFieldValue create allowed by permission set scope (:linked or :all)" + + @impl true + def strict_check(actor, authorizer, _opts) do + actor = ensure_role_loaded(actor) + + with %{role: %{permission_set_name: ps_name}} when not is_nil(ps_name) <- actor, + {:ok, ps_atom} <- PermissionSets.permission_set_name_to_atom(ps_name), + permissions <- PermissionSets.get_permissions(ps_atom), + perm <- find_custom_field_value_create(permissions.resources) do + case perm do + nil -> {:ok, false} + %{scope: :all} -> {:ok, true} + %{scope: :linked} -> {:ok, member_id_matches?(authorizer, actor)} + end + else + _ -> {:ok, false} + end + end + + defp find_custom_field_value_create(resources) do + Enum.find(resources, fn p -> + p.resource == "CustomFieldValue" and p.action == :create and p.granted + end) + end + + defp member_id_matches?(authorizer, actor) do + member_id = get_create_member_id(authorizer) + !is_nil(member_id) and member_id == actor.member_id + end + + defp get_create_member_id(authorizer) do + changeset = authorizer.changeset || authorizer.subject + + if changeset && function_exported?(Ash.Changeset, :get_attribute, 2) do + Ash.Changeset.get_attribute(changeset, :member_id) + else + nil + end + end + + defp ensure_role_loaded(actor), do: Mv.Authorization.Actor.ensure_loaded(actor) +end