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
mutationsmutationsdefmodule 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
enddefmodule 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