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

View file

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

View file

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

View file

@ -339,7 +339,12 @@ defmodule Ash.DataLayer.Ets do
},
{:ok, acc} ->
results
|> filter_matches(Map.get(query || %{}, :filter), domain, context[:tenant])
|> filter_matches(
Map.get(query || %{}, :filter),
domain,
context[:tenant],
context[:actor]
)
|> case do
{:ok, matches} ->
field = field || Enum.at(Ash.Resource.Info.primary_key(resource), 0)
@ -386,7 +391,14 @@ defmodule Ash.DataLayer.Ets do
) do
with {:ok, records} <- get_records(resource, tenant),
{: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_distinct(records, distinct, 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,
record: record,
resource: resource,
domain: domain
domain: domain,
actor: calculation.context.actor,
tenant: calculation.context.tenant
) do
{:ok, value} ->
if calculation.load do
@ -722,7 +736,13 @@ defmodule Ash.DataLayer.Ets do
domain
),
{: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
field = field || Enum.at(Ash.Resource.Info.primary_key(query.resource), 0)
@ -1015,27 +1035,38 @@ defmodule Ash.DataLayer.Ets do
filter,
domain,
_tenant,
actor,
parent \\ 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, []}
defp filter_matches(records, nil, _domain, _tenant, _parent, _conflicting_upsert_values),
do: {:ok, records}
defp filter_matches(
records,
nil,
_domain,
_tenant,
_actor,
_parent,
_conflicting_upsert_values
),
do: {:ok, records}
defp filter_matches(
records,
filter,
domain,
tenant,
actor,
parent,
conflicting_upsert_values
) do
Ash.Filter.Runtime.filter_matches(domain, records, filter,
parent: parent,
tenant: tenant,
actor: actor,
conflicting_upsert_values: conflicting_upsert_values
)
end
@ -1137,8 +1168,9 @@ defmodule Ash.DataLayer.Ets do
[result],
filter,
domain,
context.private[:tenant],
context.private[:actor],
nil,
context[:tenant],
conflicting_upsert_values
)
end
@ -1383,10 +1415,17 @@ defmodule Ash.DataLayer.Ets do
@doc false
@impl true
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
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
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
{:ok, {_key, record}} when is_map(record) ->
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
:ok
end
@ -1489,7 +1528,8 @@ defmodule Ash.DataLayer.Ets do
{pkey, changeset.attributes, changeset.atomics, changeset.filter},
changeset.domain,
changeset.tenant,
resource
resource,
changeset.context[:private][:actor]
),
{:ok, record} <- cast_record(record, resource) do
new_pkey = pkey_map(resource, record)
@ -1534,7 +1574,14 @@ defmodule Ash.DataLayer.Ets do
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()
case dump_to_native(record, attributes) do
@ -1543,7 +1590,7 @@ defmodule Ash.DataLayer.Ets do
{:ok, {_key, record}} when is_map(record) ->
with {:ok, casted_record} <- cast_record(record, resource),
{: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
empty when empty in [nil, []] ->
data = Map.merge(record, casted)

View file

@ -70,6 +70,7 @@ defmodule Ash.DataLayer.Mnesia do
:limit,
:tenant,
:sort,
context: %{},
relationships: %{},
offset: 0,
aggregates: [],
@ -203,7 +204,12 @@ defmodule Ash.DataLayer.Mnesia do
},
{:ok, acc} ->
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
{:ok, matches} ->
field = field || Enum.at(Ash.Resource.Info.primary_key(resource), 0)
@ -243,6 +249,12 @@ defmodule Ash.DataLayer.Mnesia do
{:ok, %{query | tenant: tenant}}
end
@doc false
@impl true
def set_context(_resource, query, context) do
{:ok, %{query | context: context}}
end
@doc false
@impl true
def run_query(
@ -255,7 +267,8 @@ defmodule Ash.DataLayer.Mnesia do
limit: limit,
sort: sort,
aggregates: aggregates,
tenant: tenant
tenant: tenant,
context: context
},
_resource
) do
@ -265,7 +278,8 @@ defmodule Ash.DataLayer.Mnesia do
end),
{:ok, records} <-
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 <-
filtered |> Sort.runtime_sort(sort, domain: domain) |> Enum.drop(offset || 0),
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, 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
Ash.Filter.Runtime.filter_matches(domain, records, filter, tenant: tenant)
defp filter_matches(records, filter, domain, tenant, actor) do
Ash.Filter.Runtime.filter_matches(domain, records, filter, tenant: tenant, actor: actor)
end
@doc false

View file

@ -29,7 +29,18 @@ defmodule Ash.DataLayer.Simple do
defmodule Query do
@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
@doc """
@ -60,11 +71,20 @@ defmodule Ash.DataLayer.Simple do
end
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
) do
data
|> do_filter_matches(filter, domain)
|> do_filter_matches(filter, domain, tenant, context)
|> case do
{:ok, results} ->
{:ok,
@ -90,8 +110,11 @@ defmodule Ash.DataLayer.Simple do
end
end
defp do_filter_matches(data, filter, domain) do
Ash.Filter.Runtime.filter_matches(domain, data, filter)
defp do_filter_matches(data, filter, domain, tenant, context) do
Ash.Filter.Runtime.filter_matches(domain, data, filter,
actor: context[:private][:actor],
tenant: tenant
)
end
@doc false
@ -105,7 +128,7 @@ defmodule Ash.DataLayer.Simple do
end
@doc false
def set_tenant(_, query, _), do: {:ok, query}
def set_tenant(_, query, tenant), do: {:ok, %{query | tenant: tenant}}
@doc false
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),
{:ok, data} <- Map.fetch(data_layer_context, :data),
{: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
_ ->
{:ok, query}

View file

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

View file

@ -54,7 +54,12 @@ defmodule Ash.Filter.Runtime do
|> load_all(refs_to_load)
|> 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
Enum.reduce_while(records, {:ok, []}, fn record, {:ok, records} ->
@ -166,7 +171,9 @@ defmodule Ash.Filter.Runtime do
parent \\ nil,
resource \\ nil,
domain \\ nil,
unknown_on_unknown_refs? \\ false
unknown_on_unknown_refs? \\ false,
actor \\ nil,
tenant \\ nil
) do
if domain && record do
refs_to_load =
@ -187,7 +194,12 @@ defmodule Ash.Filter.Runtime do
|> load_all(refs)
|> 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
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
:unknown
else
nil
{:ok, nil}
end
end

View file

@ -121,7 +121,7 @@ defmodule Ash.Policy.FilterCheck do
defp try_eval(expression, %{
resource: resource,
action_input: %Ash.ActionInput{} = action_input,
action_input: %Ash.ActionInput{tenant: tenant} = action_input,
actor: actor
}) do
expression =
@ -140,22 +140,12 @@ defmodule Ash.Policy.FilterCheck do
public?: false
}) do
{:ok, hydrated} ->
Ash.Expr.eval_hydrated(hydrated, resource: resource, unknown_on_unknown_refs?: true)
{: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,
aggregates: query.aggregates,
calculations: query.calculations,
public?: false
}) do
{: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}
@ -164,7 +154,31 @@ defmodule Ash.Policy.FilterCheck do
defp try_eval(expression, %{
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
}) do
expression =
@ -184,7 +198,12 @@ defmodule Ash.Policy.FilterCheck do
public?: false
}) do
{: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}
@ -193,7 +212,8 @@ defmodule Ash.Policy.FilterCheck do
defp try_eval(expression, %{
resource: resource,
changeset: %Ash.Changeset{data: data} = changeset
changeset: %Ash.Changeset{data: data, tenant: tenant} = changeset,
actor: actor
}) do
case Ash.Filter.hydrate_refs(expression, %{
resource: resource,
@ -204,7 +224,9 @@ defmodule Ash.Policy.FilterCheck do
{:ok, hydrated} ->
opts = [
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
@ -231,7 +253,7 @@ defmodule Ash.Policy.FilterCheck do
end
end
defp try_eval(expression, %{resource: resource}) do
defp try_eval(expression, %{resource: resource, actor: actor}) do
case Ash.Filter.hydrate_refs(expression, %{
resource: resource,
aggregates: %{},
@ -239,7 +261,11 @@ defmodule Ash.Policy.FilterCheck do
public?: false
}) do
{: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}

View file

@ -2913,7 +2913,11 @@ defmodule Ash.Query do
"Could not determine domain for #{inspect(query)}, please provide the `:domain` option."
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_distinct(records, query.distinct, 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,
record: record,
resource: resource,
actor: context.actor,
unknown_on_unknown_refs?: true
) do
{:ok, value} ->

View file

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