fix: properly pass actor when running filters at runtime

fix: misplaced curly bracket when handling struct type casting
This commit is contained in:
Zach Daniel 2024-09-05 16:40:39 -04:00
parent ec12332e95
commit b6e1e80fc2
12 changed files with 211 additions and 81 deletions

View file

@ -67,10 +67,12 @@ defmodule Ash.Actions.Destroy.Bulk do
opts opts
end end
query = {query, opts} =
if query.__validated_for_action__ do if query.__validated_for_action__ do
query {query, opts}
else else
{query, opts} = Ash.Actions.Helpers.set_context_and_get_opts(domain, query, opts)
query = query =
Ash.Query.for_read( Ash.Query.for_read(
query, query,
@ -80,9 +82,7 @@ defmodule Ash.Actions.Destroy.Bulk do
tenant: opts[:tenant] tenant: opts[:tenant]
) )
{query, _opts} = Ash.Actions.Helpers.set_context_and_get_opts(domain, query, opts) {query, opts}
query
end end
query = %{query | domain: domain} query = %{query | domain: domain}

View file

@ -999,7 +999,9 @@ defmodule Ash.Actions.Read.Relationships do
case Ash.Filter.Runtime.filter_matches( case Ash.Filter.Runtime.filter_matches(
related_query.domain, related_query.domain,
value, value,
query.filter query.filter,
tenant: query.tenant,
actor: query.actor
) do ) do
{:ok, value} -> {:ok, value} ->
value value

View file

@ -22,10 +22,12 @@ defmodule Ash.Actions.Update.Bulk do
opts opts
end end
query = {query, opts} =
if query.__validated_for_action__ do if query.__validated_for_action__ do
query {query, opts}
else else
{query, opts} = Ash.Actions.Helpers.set_context_and_get_opts(domain, query, opts)
query = query =
Ash.Query.for_read( Ash.Query.for_read(
query, query,
@ -35,9 +37,7 @@ defmodule Ash.Actions.Update.Bulk do
tenant: opts[:tenant] tenant: opts[:tenant]
) )
{query, _opts} = Ash.Actions.Helpers.set_context_and_get_opts(domain, query, opts) {query, opts}
query
end end
query = %{query | domain: domain} query = %{query | domain: domain}

View file

@ -339,7 +339,12 @@ defmodule Ash.DataLayer.Ets do
}, },
{:ok, acc} -> {:ok, acc} ->
results results
|> filter_matches(Map.get(query || %{}, :filter), domain, context[:tenant]) |> filter_matches(
Map.get(query || %{}, :filter),
domain,
context[:tenant],
context[:actor]
)
|> case do |> case do
{:ok, matches} -> {:ok, matches} ->
field = field || Enum.at(Ash.Resource.Info.primary_key(resource), 0) field = field || Enum.at(Ash.Resource.Info.primary_key(resource), 0)
@ -386,7 +391,14 @@ defmodule Ash.DataLayer.Ets do
) do ) do
with {:ok, records} <- get_records(resource, tenant), with {:ok, records} <- get_records(resource, tenant),
{:ok, records} <- {:ok, records} <-
filter_matches(records, filter, domain, context[:private][:tenant], parent), filter_matches(
records,
filter,
domain,
context[:private][:tenant],
context[:private][:actor],
parent
),
records <- Sort.runtime_sort(records, distinct_sort || sort, domain: domain), records <- Sort.runtime_sort(records, distinct_sort || sort, domain: domain),
records <- Sort.runtime_distinct(records, distinct, domain: domain), records <- Sort.runtime_distinct(records, distinct, domain: domain),
records <- Sort.runtime_sort(records, sort, domain: domain), records <- Sort.runtime_sort(records, sort, domain: domain),
@ -625,7 +637,9 @@ defmodule Ash.DataLayer.Ets do
case Ash.Expr.eval_hydrated(expression, case Ash.Expr.eval_hydrated(expression,
record: record, record: record,
resource: resource, resource: resource,
domain: domain domain: domain,
actor: calculation.context.actor,
tenant: calculation.context.tenant
) do ) do
{:ok, value} -> {:ok, value} ->
if calculation.load do if calculation.load do
@ -722,7 +736,13 @@ defmodule Ash.DataLayer.Ets do
domain domain
), ),
{:ok, filtered} <- {:ok, filtered} <-
filter_matches(related, query.filter, domain, context[:tenant]), filter_matches(
related,
query.filter,
domain,
context[:tenant],
context[:actor]
),
sorted <- Sort.runtime_sort(filtered, query.sort, domain: domain) do sorted <- Sort.runtime_sort(filtered, query.sort, domain: domain) do
field = field || Enum.at(Ash.Resource.Info.primary_key(query.resource), 0) field = field || Enum.at(Ash.Resource.Info.primary_key(query.resource), 0)
@ -1015,14 +1035,23 @@ defmodule Ash.DataLayer.Ets do
filter, filter,
domain, domain,
_tenant, _tenant,
actor,
parent \\ nil, parent \\ nil,
conflicting_upsert_values \\ nil conflicting_upsert_values \\ nil
) )
defp filter_matches([], _, _domain, _tenant, _parent, _conflicting_upsert_values), defp filter_matches([], _, _domain, _tenant, _actor, _parent, _conflicting_upsert_values),
do: {:ok, []} do: {:ok, []}
defp filter_matches(records, nil, _domain, _tenant, _parent, _conflicting_upsert_values), defp filter_matches(
records,
nil,
_domain,
_tenant,
_actor,
_parent,
_conflicting_upsert_values
),
do: {:ok, records} do: {:ok, records}
defp filter_matches( defp filter_matches(
@ -1030,12 +1059,14 @@ defmodule Ash.DataLayer.Ets do
filter, filter,
domain, domain,
tenant, tenant,
actor,
parent, parent,
conflicting_upsert_values conflicting_upsert_values
) do ) do
Ash.Filter.Runtime.filter_matches(domain, records, filter, Ash.Filter.Runtime.filter_matches(domain, records, filter,
parent: parent, parent: parent,
tenant: tenant, tenant: tenant,
actor: actor,
conflicting_upsert_values: conflicting_upsert_values conflicting_upsert_values: conflicting_upsert_values
) )
end end
@ -1137,8 +1168,9 @@ defmodule Ash.DataLayer.Ets do
[result], [result],
filter, filter,
domain, domain,
context.private[:tenant],
context.private[:actor],
nil, nil,
context[:tenant],
conflicting_upsert_values conflicting_upsert_values
) )
end end
@ -1383,10 +1415,17 @@ defmodule Ash.DataLayer.Ets do
@doc false @doc false
@impl true @impl true
def destroy(resource, %{data: record, filter: filter} = changeset) do def destroy(resource, %{data: record, filter: filter} = changeset) do
do_destroy(resource, record, changeset.tenant, filter, changeset.domain) do_destroy(
resource,
record,
changeset.tenant,
filter,
changeset.domain,
changeset.context[:private][:actor]
)
end end
defp do_destroy(resource, record, tenant, filter, domain) do defp do_destroy(resource, record, tenant, filter, domain, actor) do
with {:ok, table} <- wrap_or_create_table(resource, tenant) do with {:ok, table} <- wrap_or_create_table(resource, tenant) do
pkey = Map.take(record, Ash.Resource.Info.primary_key(resource)) pkey = Map.take(record, Ash.Resource.Info.primary_key(resource))
@ -1394,7 +1433,7 @@ defmodule Ash.DataLayer.Ets do
case ETS.Set.get(table, pkey) do case ETS.Set.get(table, pkey) do
{:ok, {_key, record}} when is_map(record) -> {:ok, {_key, record}} when is_map(record) ->
with {:ok, record} <- cast_record(record, resource), with {:ok, record} <- cast_record(record, resource),
{:ok, [_]} <- filter_matches([record], filter, domain, tenant) do {:ok, [_]} <- filter_matches([record], filter, domain, tenant, actor) do
with {:ok, _} <- ETS.Set.delete(table, pkey) do with {:ok, _} <- ETS.Set.delete(table, pkey) do
:ok :ok
end end
@ -1489,7 +1528,8 @@ defmodule Ash.DataLayer.Ets do
{pkey, changeset.attributes, changeset.atomics, changeset.filter}, {pkey, changeset.attributes, changeset.atomics, changeset.filter},
changeset.domain, changeset.domain,
changeset.tenant, changeset.tenant,
resource resource,
changeset.context[:private][:actor]
), ),
{:ok, record} <- cast_record(record, resource) do {:ok, record} <- cast_record(record, resource) do
new_pkey = pkey_map(resource, record) new_pkey = pkey_map(resource, record)
@ -1534,7 +1574,14 @@ defmodule Ash.DataLayer.Ets do
end) end)
end end
defp do_update(table, {pkey, record, atomics, changeset_filter}, domain, tenant, resource) do defp do_update(
table,
{pkey, record, atomics, changeset_filter},
domain,
tenant,
resource,
actor
) do
attributes = resource |> Ash.Resource.Info.attributes() attributes = resource |> Ash.Resource.Info.attributes()
case dump_to_native(record, attributes) do case dump_to_native(record, attributes) do
@ -1543,7 +1590,7 @@ defmodule Ash.DataLayer.Ets do
{:ok, {_key, record}} when is_map(record) -> {:ok, {_key, record}} when is_map(record) ->
with {:ok, casted_record} <- cast_record(record, resource), with {:ok, casted_record} <- cast_record(record, resource),
{:ok, [casted_record]} <- {:ok, [casted_record]} <-
filter_matches([casted_record], changeset_filter, domain, tenant) do filter_matches([casted_record], changeset_filter, domain, tenant, actor) do
case atomics do case atomics do
empty when empty in [nil, []] -> empty when empty in [nil, []] ->
data = Map.merge(record, casted) data = Map.merge(record, casted)

View file

@ -70,6 +70,7 @@ defmodule Ash.DataLayer.Mnesia do
:limit, :limit,
:tenant, :tenant,
:sort, :sort,
context: %{},
relationships: %{}, relationships: %{},
offset: 0, offset: 0,
aggregates: [], aggregates: [],
@ -203,7 +204,12 @@ defmodule Ash.DataLayer.Mnesia do
}, },
{:ok, acc} -> {:ok, acc} ->
results results
|> filter_matches(Map.get(query || %{}, :filter), domain, query.tenant) |> filter_matches(
Map.get(query || %{}, :filter),
domain,
query.tenant,
query.context[:private][:actor]
)
|> case do |> case do
{:ok, matches} -> {:ok, matches} ->
field = field || Enum.at(Ash.Resource.Info.primary_key(resource), 0) field = field || Enum.at(Ash.Resource.Info.primary_key(resource), 0)
@ -243,6 +249,12 @@ defmodule Ash.DataLayer.Mnesia do
{:ok, %{query | tenant: tenant}} {:ok, %{query | tenant: tenant}}
end end
@doc false
@impl true
def set_context(_resource, query, context) do
{:ok, %{query | context: context}}
end
@doc false @doc false
@impl true @impl true
def run_query( def run_query(
@ -255,7 +267,8 @@ defmodule Ash.DataLayer.Mnesia do
limit: limit, limit: limit,
sort: sort, sort: sort,
aggregates: aggregates, aggregates: aggregates,
tenant: tenant tenant: tenant,
context: context
}, },
_resource _resource
) do ) do
@ -265,7 +278,8 @@ defmodule Ash.DataLayer.Mnesia do
end), end),
{:ok, records} <- {:ok, records} <-
records |> Enum.map(&elem(&1, 2)) |> Ash.DataLayer.Ets.cast_records(resource), records |> Enum.map(&elem(&1, 2)) |> Ash.DataLayer.Ets.cast_records(resource),
{:ok, filtered} <- filter_matches(records, filter, domain, tenant), {:ok, filtered} <-
filter_matches(records, filter, domain, tenant, context[:private][:actor]),
offset_records <- offset_records <-
filtered |> Sort.runtime_sort(sort, domain: domain) |> Enum.drop(offset || 0), filtered |> Sort.runtime_sort(sort, domain: domain) |> Enum.drop(offset || 0),
limited_records <- do_limit(offset_records, limit), limited_records <- do_limit(offset_records, limit),
@ -296,10 +310,10 @@ defmodule Ash.DataLayer.Mnesia do
defp do_limit(records, nil), do: records defp do_limit(records, nil), do: records
defp do_limit(records, limit), do: Enum.take(records, limit) defp do_limit(records, limit), do: Enum.take(records, limit)
defp filter_matches(records, nil, _domain, _tenant), do: {:ok, records} defp filter_matches(records, nil, _domain, _tenant, _), do: {:ok, records}
defp filter_matches(records, filter, domain, tenant) do defp filter_matches(records, filter, domain, tenant, actor) do
Ash.Filter.Runtime.filter_matches(domain, records, filter, tenant: tenant) Ash.Filter.Runtime.filter_matches(domain, records, filter, tenant: tenant, actor: actor)
end end
@doc false @doc false

View file

@ -29,7 +29,18 @@ defmodule Ash.DataLayer.Simple do
defmodule Query do defmodule Query do
@moduledoc false @moduledoc false
defstruct [:data, :resource, :filter, :domain, :limit, :offset, sort: [], data_set?: false] defstruct [
:data,
:resource,
:filter,
:domain,
:limit,
:offset,
:tenant,
sort: [],
data_set?: false,
context: %{}
]
end end
@doc """ @doc """
@ -60,11 +71,20 @@ defmodule Ash.DataLayer.Simple do
end end
def run_query( def run_query(
%{data: data, sort: sort, domain: domain, filter: filter, limit: limit, offset: offset}, %{
data: data,
sort: sort,
domain: domain,
filter: filter,
limit: limit,
offset: offset,
tenant: tenant,
context: context
},
_resource _resource
) do ) do
data data
|> do_filter_matches(filter, domain) |> do_filter_matches(filter, domain, tenant, context)
|> case do |> case do
{:ok, results} -> {:ok, results} ->
{:ok, {:ok,
@ -90,8 +110,11 @@ defmodule Ash.DataLayer.Simple do
end end
end end
defp do_filter_matches(data, filter, domain) do defp do_filter_matches(data, filter, domain, tenant, context) do
Ash.Filter.Runtime.filter_matches(domain, data, filter) Ash.Filter.Runtime.filter_matches(domain, data, filter,
actor: context[:private][:actor],
tenant: tenant
)
end end
@doc false @doc false
@ -105,7 +128,7 @@ defmodule Ash.DataLayer.Simple do
end end
@doc false @doc false
def set_tenant(_, query, _), do: {:ok, query} def set_tenant(_, query, tenant), do: {:ok, %{query | tenant: tenant}}
@doc false @doc false
def filter(query, filter, _resource) do def filter(query, filter, _resource) do
@ -122,7 +145,7 @@ defmodule Ash.DataLayer.Simple do
with {:ok, data_layer_context} <- Map.fetch(context, :data_layer), with {:ok, data_layer_context} <- Map.fetch(context, :data_layer),
{:ok, data} <- Map.fetch(data_layer_context, :data), {:ok, data} <- Map.fetch(data_layer_context, :data),
{:ok, resource_data} <- Map.fetch(data, query.resource) do {:ok, resource_data} <- Map.fetch(data, query.resource) do
{:ok, %{query | data_set?: true, data: resource_data || []}} {:ok, %{query | data_set?: true, data: resource_data || [], context: context}}
else else
_ -> _ ->
{:ok, query} {:ok, query}

View file

@ -111,7 +111,9 @@ defmodule Ash.Expr do
opts[:parent], opts[:parent],
opts[:resource], opts[:resource],
opts[:domain], opts[:domain],
opts[:unknown_on_unknown_refs?] opts[:unknown_on_unknown_refs?],
opts[:actor],
opts[:tenant]
) )
end end

View file

@ -54,7 +54,12 @@ defmodule Ash.Filter.Runtime do
|> load_all(refs_to_load) |> load_all(refs_to_load)
|> Ash.Query.set_context(%{private: %{internal?: true}}) |> Ash.Query.set_context(%{private: %{internal?: true}})
Ash.load!(records, load, authorize?: false, domain: domain, tenant: opts[:tenant]) Ash.load!(records, load,
authorize?: false,
domain: domain,
tenant: opts[:tenant],
actor: opts[:actor]
)
end end
Enum.reduce_while(records, {:ok, []}, fn record, {:ok, records} -> Enum.reduce_while(records, {:ok, []}, fn record, {:ok, records} ->
@ -166,7 +171,9 @@ defmodule Ash.Filter.Runtime do
parent \\ nil, parent \\ nil,
resource \\ nil, resource \\ nil,
domain \\ nil, domain \\ nil,
unknown_on_unknown_refs? \\ false unknown_on_unknown_refs? \\ false,
actor \\ nil,
tenant \\ nil
) do ) do
if domain && record do if domain && record do
refs_to_load = refs_to_load =
@ -187,7 +194,12 @@ defmodule Ash.Filter.Runtime do
|> load_all(refs) |> load_all(refs)
|> Ash.Query.set_context(%{private: %{internal?: true}}) |> Ash.Query.set_context(%{private: %{internal?: true}})
Ash.load!(record, load, domain: domain, authorize?: false) Ash.load!(record, load,
domain: domain,
authorize?: false,
tenant: tenant,
actor: actor
)
end end
do_match(record, expression, parent, resource, unknown_on_unknown_refs?) do_match(record, expression, parent, resource, unknown_on_unknown_refs?)
@ -451,7 +463,7 @@ defmodule Ash.Filter.Runtime do
if unknown_on_unknown_refs? do if unknown_on_unknown_refs? do
:unknown :unknown
else else
nil {:ok, nil}
end end
end end

View file

@ -121,7 +121,7 @@ defmodule Ash.Policy.FilterCheck do
defp try_eval(expression, %{ defp try_eval(expression, %{
resource: resource, resource: resource,
action_input: %Ash.ActionInput{} = action_input, action_input: %Ash.ActionInput{tenant: tenant} = action_input,
actor: actor actor: actor
}) do }) do
expression = expression =
@ -140,22 +140,12 @@ defmodule Ash.Policy.FilterCheck do
public?: false public?: false
}) do }) do
{:ok, hydrated} -> {:ok, hydrated} ->
Ash.Expr.eval_hydrated(hydrated, resource: resource, unknown_on_unknown_refs?: true) Ash.Expr.eval_hydrated(hydrated,
{:error, error} ->
{:error, error}
end
end
defp try_eval(expression, %{resource: resource, query: %Ash.Query{} = query}) do
case Ash.Filter.hydrate_refs(expression, %{
resource: resource, resource: resource,
aggregates: query.aggregates, unknown_on_unknown_refs?: true,
calculations: query.calculations, actor: actor,
public?: false tenant: tenant
}) do )
{:ok, hydrated} ->
Ash.Expr.eval_hydrated(hydrated, resource: resource, unknown_on_unknown_refs?: true)
{:error, error} -> {:error, error} ->
{:error, error} {:error, error}
@ -164,7 +154,31 @@ defmodule Ash.Policy.FilterCheck do
defp try_eval(expression, %{ defp try_eval(expression, %{
resource: resource, resource: resource,
changeset: %Ash.Changeset{action_type: :create} = changeset, query: %Ash.Query{tenant: tenant} = query,
actor: actor
}) do
case Ash.Filter.hydrate_refs(expression, %{
resource: resource,
aggregates: query.aggregates,
calculations: query.calculations,
public?: false
}) do
{:ok, hydrated} ->
Ash.Expr.eval_hydrated(hydrated,
resource: resource,
unknown_on_unknown_refs?: true,
actor: actor,
tenant: tenant
)
{:error, error} ->
{:error, error}
end
end
defp try_eval(expression, %{
resource: resource,
changeset: %Ash.Changeset{action_type: :create, tenant: tenant} = changeset,
actor: actor actor: actor
}) do }) do
expression = expression =
@ -184,7 +198,12 @@ defmodule Ash.Policy.FilterCheck do
public?: false public?: false
}) do }) do
{:ok, hydrated} -> {:ok, hydrated} ->
Ash.Expr.eval_hydrated(hydrated, resource: resource, unknown_on_unknown_refs?: true) Ash.Expr.eval_hydrated(hydrated,
resource: resource,
unknown_on_unknown_refs?: true,
actor: actor,
tenant: tenant
)
{:error, error} -> {:error, error} ->
{:error, error} {:error, error}
@ -193,7 +212,8 @@ defmodule Ash.Policy.FilterCheck do
defp try_eval(expression, %{ defp try_eval(expression, %{
resource: resource, resource: resource,
changeset: %Ash.Changeset{data: data} = changeset changeset: %Ash.Changeset{data: data, tenant: tenant} = changeset,
actor: actor
}) do }) do
case Ash.Filter.hydrate_refs(expression, %{ case Ash.Filter.hydrate_refs(expression, %{
resource: resource, resource: resource,
@ -204,7 +224,9 @@ defmodule Ash.Policy.FilterCheck do
{:ok, hydrated} -> {:ok, hydrated} ->
opts = [ opts = [
resource: resource, resource: resource,
unknown_on_unknown_refs?: true unknown_on_unknown_refs?: true,
actor: actor,
tenant: tenant
] ]
# We don't want to authorize on stale data in real life # We don't want to authorize on stale data in real life
@ -231,7 +253,7 @@ defmodule Ash.Policy.FilterCheck do
end end
end end
defp try_eval(expression, %{resource: resource}) do defp try_eval(expression, %{resource: resource, actor: actor}) do
case Ash.Filter.hydrate_refs(expression, %{ case Ash.Filter.hydrate_refs(expression, %{
resource: resource, resource: resource,
aggregates: %{}, aggregates: %{},
@ -239,7 +261,11 @@ defmodule Ash.Policy.FilterCheck do
public?: false public?: false
}) do }) do
{:ok, hydrated} -> {:ok, hydrated} ->
Ash.Expr.eval_hydrated(hydrated, resource: resource, unknown_on_unknown_refs?: true) Ash.Expr.eval_hydrated(hydrated,
resource: resource,
unknown_on_unknown_refs?: true,
actor: actor
)
{:error, error} -> {:error, error} ->
{:error, error} {:error, error}

View file

@ -2913,7 +2913,11 @@ defmodule Ash.Query do
"Could not determine domain for #{inspect(query)}, please provide the `:domain` option." "Could not determine domain for #{inspect(query)}, please provide the `:domain` option."
with {:ok, records} <- with {:ok, records} <-
Ash.Filter.Runtime.filter_matches(domain, records, query.filter, parent: opts[:parent]), Ash.Filter.Runtime.filter_matches(domain, records, query.filter,
parent: opts[:parent],
actor: opts[:actor] || query.context[:private][:actor],
tenant: opts[:tenant] || query.tenant
),
records <- Sort.runtime_sort(records, query.distinct_sort || query.sort, domain: domain), records <- Sort.runtime_sort(records, query.distinct_sort || query.sort, domain: domain),
records <- Sort.runtime_distinct(records, query.distinct, domain: domain), records <- Sort.runtime_distinct(records, query.distinct, domain: domain),
records <- Sort.runtime_sort(records, query.sort, domain: domain), records <- Sort.runtime_sort(records, query.sort, domain: domain),

View file

@ -45,6 +45,7 @@ defmodule Ash.Resource.Calculation.Expression do
case Ash.Expr.eval_hydrated(expression, case Ash.Expr.eval_hydrated(expression,
record: record, record: record,
resource: resource, resource: resource,
actor: context.actor,
unknown_on_unknown_refs?: true unknown_on_unknown_refs?: true
) do ) do
{:ok, value} -> {:ok, value} ->

View file

@ -264,12 +264,11 @@ defmodule Ash.Type.Struct do
else else
keys = Map.keys(value) keys = Map.keys(value)
cond do if Enum.all?(keys, &is_atom/1) do
Enum.all?(keys, &is_atom/1) ->
{:ok, struct(struct, value)} {:ok, struct(struct, value)}
else
Enum.all?(keys, &is_binary/1) -> {:ok,
{:ok, Map.delete(struct.__struct__, :__struct__)} Map.delete(struct.__struct__, :__struct__)
|> Enum.reduce({:ok, struct(struct)}, fn {key, _value}, {:ok, acc} -> |> Enum.reduce({:ok, struct(struct)}, fn {key, _value}, {:ok, acc} ->
case Map.fetch(value, to_string(key)) do case Map.fetch(value, to_string(key)) do
{:ok, val} -> {:ok, val} ->
@ -278,7 +277,7 @@ defmodule Ash.Type.Struct do
:error -> :error ->
{:ok, acc} {:ok, acc}
end end
end) end)}
end end
end end
end end