Ash FrameworkAF
Ash Framework3y ago
96 replies
barnabasj

get selected fields in create/update action

Hi, I have this policy to check if a user is only selecting fields that they are allowed to see. This worked well for reads because it was possible to get the selected/loaded fields from the query. Is it possible to do something similar for mutations

defmodule Demo.Policies.SelectsAllowedFields do
  @moduledoc """
  Checks if an action selects only fields that are allowed

  Takes a mapping of roles to allowed fields as well as a bypass option
  to circumvent the check for a given role

  e.g:

  user: [:id, :name, :last_name],
  super_user: [:id, :address],
  bypass: admin
  """
  use Ash.Policy.SimpleCheck

  require Logger

  @impl true
  def describe(_) do
    "Checks if only allowed fields are selected"
  end

  @impl true
  def match?(actor, context, options) do
    bypass_role = options[:bypass]

    matched =
      case get_role(actor) do
        role when is_atom(bypass_role) and role == bypass_role ->
          true

        role ->
          match(
            options[role],
            []
            |> Enum.concat(get_selects(context))
            |> Enum.concat(get_loads(context))
            |> Enum.concat(get_calculations(context))
            |> Enum.concat(get_aggregates(context))
          )
      end

    if not matched do
      dbg()
      allowed_fields = options[get_role(actor)] || []

      selected_fields =
        []
        |> Enum.concat(get_selects(context))
        |> Enum.concat(get_loads(context))
        |> Enum.concat(get_calculations(context))
        |> Enum.concat(get_aggregates(context))

      Logger.debug("""
      Actor (#{Map.get(actor, :id, "unknown")}) with role #{get_role(actor)} tried to access resource #{context.resource}"

      selecting: #{inspect(selected_fields)}
      allowed: #{inspect(allowed_fields)}
      diff: #{inspect(selected_fields -- allowed_fields)}
      """)
    end

    matched
  end

  def get_role(%{roles: roles}) when is_list(roles), do: List.first(roles)
  def get_role(_), do: nil

  # Get all attributes from the resource struct if no fields are selected
  #
  # @see https://www.ash-hq.org/docs/module/ash/2.4.10/ash-query#function-select-3
  #
  # ignore meta fields starting with `__`
  # and field with structs as values as those point to
  # calculations/aggregates/relationships
  defp get_selects(%{query: %{select: nil}, resource: resource}),
    do: get_resource_fields(resource)

  defp get_selects(%{query: nil, resource: resource}),
    do: get_resource_fields(resource)

  defp get_selects(%{query: %{select: select}}), do: select
  defp get_selects(e), do: raise(e)

  defp get_resource_fields(resource) do
    struct = resource.__struct__

    struct
    |> Map.keys()
    |> Enum.filter(fn key ->
      !String.starts_with?(to_string(key), "__") and
        !Enum.any?([:aggregates, :calculations], fn special_field -> special_field == key end) and
        !is_struct(
          Map.get(
            struct,
            key
          )
        )
    end)
  end

  defp get_loads(%{query: %{load: load}}), do: Keyword.keys(load)
  defp get_loads(_), do: []
  defp get_calculations(%{query: %{calculations: calculations}}), do: Map.keys(calculations)
  defp get_calculations(_), do: []
  defp get_aggregates(%{query: %{aggregates: aggregates}}), do: Map.keys(aggregates)
  defp get_aggregates(_), do: []

  defp match(allowed_fields, selected_fields)
       when is_list(allowed_fields) and is_list(selected_fields) do
    case selected_fields -- allowed_fields do
      [] ->
        true

      _ ->
        false
    end
  end

  defp match(_, _), do: false
end
Was this page helpful?