diff --git a/lib/ash/actions/destroy/bulk.ex b/lib/ash/actions/destroy/bulk.ex index ed101d25..e51606f5 100644 --- a/lib/ash/actions/destroy/bulk.ex +++ b/lib/ash/actions/destroy/bulk.ex @@ -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} diff --git a/lib/ash/actions/read/relationships.ex b/lib/ash/actions/read/relationships.ex index b800d104..ff1b8d9c 100644 --- a/lib/ash/actions/read/relationships.ex +++ b/lib/ash/actions/read/relationships.ex @@ -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 diff --git a/lib/ash/actions/update/bulk.ex b/lib/ash/actions/update/bulk.ex index 2820ee57..042d0cf0 100644 --- a/lib/ash/actions/update/bulk.ex +++ b/lib/ash/actions/update/bulk.ex @@ -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} diff --git a/lib/ash/data_layer/ets/ets.ex b/lib/ash/data_layer/ets/ets.ex index 077ac3c0..7ac585e4 100644 --- a/lib/ash/data_layer/ets/ets.ex +++ b/lib/ash/data_layer/ets/ets.ex @@ -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) diff --git a/lib/ash/data_layer/mnesia/mnesia.ex b/lib/ash/data_layer/mnesia/mnesia.ex index d318616e..9e810ad9 100644 --- a/lib/ash/data_layer/mnesia/mnesia.ex +++ b/lib/ash/data_layer/mnesia/mnesia.ex @@ -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 diff --git a/lib/ash/data_layer/simple/simple.ex b/lib/ash/data_layer/simple/simple.ex index 321bcf2c..f93d0e3d 100644 --- a/lib/ash/data_layer/simple/simple.ex +++ b/lib/ash/data_layer/simple/simple.ex @@ -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} diff --git a/lib/ash/expr/expr.ex b/lib/ash/expr/expr.ex index f02b0b9b..4b049443 100644 --- a/lib/ash/expr/expr.ex +++ b/lib/ash/expr/expr.ex @@ -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 diff --git a/lib/ash/filter/runtime.ex b/lib/ash/filter/runtime.ex index 843121f1..642ba43d 100644 --- a/lib/ash/filter/runtime.ex +++ b/lib/ash/filter/runtime.ex @@ -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 diff --git a/lib/ash/policy/filter_check.ex b/lib/ash/policy/filter_check.ex index fa7de22d..19308b8e 100644 --- a/lib/ash/policy/filter_check.ex +++ b/lib/ash/policy/filter_check.ex @@ -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} diff --git a/lib/ash/query/query.ex b/lib/ash/query/query.ex index c41eca39..98eac3bb 100644 --- a/lib/ash/query/query.ex +++ b/lib/ash/query/query.ex @@ -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), diff --git a/lib/ash/resource/calculation/expression.ex b/lib/ash/resource/calculation/expression.ex index dfe620c2..82d7a7b3 100644 --- a/lib/ash/resource/calculation/expression.ex +++ b/lib/ash/resource/calculation/expression.ex @@ -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} -> diff --git a/lib/ash/type/struct.ex b/lib/ash/type/struct.ex index 3583b24d..2ce996c2 100644 --- a/lib/ash/type/struct.ex +++ b/lib/ash/type/struct.ex @@ -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