Packages

ash

1.12.0
3.29.3 3.29.2 3.29.1 3.29.0 3.28.0 3.27.8 3.27.7 3.27.6 3.27.5 3.27.4 3.27.3 3.27.2 3.27.1 3.27.0 3.26.0 3.25.2 3.25.1 3.25.0 retired 3.24.7 3.24.6 3.24.5 3.24.4 3.24.3 3.24.2 3.24.1 3.24.0 3.23.1 3.23.0 3.22.2 3.22.1 3.22.0 3.21.3 3.21.2 3.21.1 3.21.0 3.20.0 3.19.3 3.19.2 3.19.1 3.19.0 3.18.0 3.17.1 3.17.0 3.16.0 3.15.0 3.14.1 3.14.0 retired 3.13.2 3.13.1 3.13.0 3.12.0 3.11.3 3.11.2 3.11.1 3.11.0 3.10.1 3.10.0 3.9.0 3.8.0 3.7.6 3.7.5 3.7.4 3.7.3 3.7.2 3.7.1 3.7.0 retired 3.6.3 retired 3.6.2 3.6.1 3.6.0 3.5.43 3.5.42 3.5.41 3.5.40 3.5.39 3.5.38 3.5.37 3.5.36 3.5.35 3.5.34 3.5.33 3.5.32 3.5.31 3.5.30 3.5.29 3.5.28 3.5.27 3.5.26 3.5.25 3.5.24 3.5.23 3.5.22 3.5.21 3.5.20 3.5.19 3.5.18 3.5.17 3.5.16 3.5.15 3.5.14 3.5.13 3.5.12 3.5.11 3.5.10 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.74 retired 3.4.73 3.4.72 3.4.71 3.4.70 3.4.69 3.4.68 3.4.67 3.4.66 3.4.65 3.4.64 3.4.63 3.4.62 3.4.61 3.4.60 3.4.59 3.4.58 3.4.57 3.4.56 3.4.55 3.4.54 3.4.53 3.4.52 3.4.51 3.4.50 3.4.49 3.4.48 3.4.47 3.4.46 3.4.45 3.4.44 3.4.43 3.4.42 3.4.41 3.4.40 3.4.39 3.4.38 3.4.37 3.4.36 3.4.35 3.4.34 3.4.33 3.4.32 3.4.31 3.4.30 3.4.29 3.4.28 3.4.27 3.4.26 3.4.25 3.4.24 3.4.23 3.4.22 3.4.21 3.4.20 3.4.19 3.4.18 3.4.17 3.4.16 3.4.15 3.4.14 3.4.13 3.4.12 3.4.11 3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.5 3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.3 3.3.2 3.3.1 3.3.0 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 3.2.0 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.16 3.0.15 3.0.14 3.0.13 3.0.12 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 3.0.0-rc.46 3.0.0-rc.45 3.0.0-rc.44 3.0.0-rc.43 3.0.0-rc.42 3.0.0-rc.41 3.0.0-rc.40 3.0.0-rc.39 3.0.0-rc.38 3.0.0-rc.37 3.0.0-rc.36 3.0.0-rc.35 3.0.0-rc.34 3.0.0-rc.33 3.0.0-rc.32 3.0.0-rc.31 3.0.0-rc.29 3.0.0-rc.28 3.0.0-rc.27 3.0.0-rc.26 3.0.0-rc.25 3.0.0-rc.24 3.0.0-rc.23 3.0.0-rc.22 3.0.0-rc.21 3.0.0-rc.20 3.0.0-rc.19 3.0.0-rc.18 3.0.0-rc.17 3.0.0-rc.16 3.0.0-rc.15 3.0.0-rc.14 3.0.0-rc.13 3.0.0-rc.12 3.0.0-rc.11 3.0.0-rc.10 3.0.0-rc.9 3.0.0-rc.8 3.0.0-rc.7 3.0.0-rc.6 3.0.0-rc.5 3.0.0-rc.4 3.0.0-rc.3 3.0.0-rc.1 3.0.0-rc.0 2.21.15 2.21.14 2.21.13 2.21.12 2.21.11 2.21.10 2.21.9 2.21.8 2.21.7 2.21.6 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.3 2.20.2 2.20.1 2.20.0 2.19.14 2.19.13 2.19.12 2.19.11 2.19.10 2.19.9 2.19.8 2.19.7 2.19.6 2.19.5 2.19.4 2.19.3 retired 2.19.2 retired 2.19.1 retired 2.19.0 retired 2.18.2 2.18.1 2.18.0 2.17.24 2.17.23 2.17.22 2.17.21 2.17.20 2.17.19 2.17.18 2.17.17 2.17.16 2.17.15 2.17.14 2.17.13 2.17.12 2.17.11 2.17.10 2.17.9 2.17.8 2.17.7 2.17.6 2.17.5 2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.1 2.16.0 2.15.20 2.15.19 2.15.18 2.15.17 2.15.16 2.15.15 2.15.14 2.15.13 2.15.12 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.2 2.15.1 2.15.0 2.14.21 2.14.20 2.14.19 2.14.18 2.14.17 2.14.16 2.14.15 2.14.14 2.14.13 2.14.12 2.14.11 2.14.10 2.14.9 2.14.8 2.14.7 2.14.6 2.14.5 2.14.4 2.14.3 2.14.2 2.14.1 2.14.0 2.13.4 retired 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.11 2.11.10 2.11.9 2.11.8 2.11.7 2.11.6 2.11.5 2.11.4 2.11.3 2.11.2 2.11.1 2.11.0 2.11.0-rc.3 2.11.0-rc.2 2.11.0-rc.1 2.11.0-rc.0 2.10.2 2.10.1 2.10.0 2.9.29 2.9.28 2.9.27 2.9.26 2.9.25 2.9.24 2.9.23 2.9.22 2.9.21 2.9.20 2.9.19 2.9.18 2.9.17 2.9.16 2.9.15 2.9.14 2.9.13 2.9.12 2.9.11 2.9.10 2.9.9 2.9.8 2.9.7 2.9.6 2.9.5 2.9.4 2.9.3 2.9.2 2.9.1 2.9.0 2.8.1 2.8.0 2.7.1 2.7.0 2.6.31 2.6.30 2.6.29 2.6.28 2.6.27 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.16 2.5.15 2.5.14 2.5.13 2.5.12 2.5.11 2.5.10 2.5.9 2.5.8 2.5.7 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.5.0-rc.6 2.5.0-rc.5 2.5.0-rc.4 2.5.0-rc.3 2.5.0-rc.2 2.5.0-rc.1 2.5.0-rc.0 2.4.30 2.4.29 2.4.28 2.4.27 2.4.26 2.4.25 2.4.24 2.4.23 2.4.22 2.4.21 2.4.20 2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 2.0.0-rc.15 2.0.0-rc.14 2.0.0-rc.13 2.0.0-rc.12 2.0.0-rc.11 2.0.0-rc.10 2.0.0-rc.9 2.0.0-rc.8 2.0.0-rc.7 2.0.0-rc.6 2.0.0-rc.5 2.0.0-rc.4 2.0.0-rc.3 2.0.0-rc.2 2.0.0-rc.1 2.0.0-rc.0 2.0.0-pre.8 2.0.0-pre.7 2.0.0-pre.6 2.0.0-pre.5 2.0.0-pre.4 2.0.0-pre.3 2.0.0-pre.2 2.0.0-pre.1 2.0.0-pre.0 1.53.3 1.53.2 1.53.0 1.52.0-rc.22 1.52.0-rc.21 1.52.0-rc.20 1.52.0-rc.19 1.52.0-rc.18 1.52.0-rc.17 1.52.0-rc.16 1.52.0-rc.15 1.52.0-rc.14 1.52.0-rc.13 1.52.0-rc.12 1.52.0-rc.11 1.52.0-rc.10 1.52.0-rc.9 1.52.0-rc.8 1.52.0-rc.7 1.52.0-rc.6 1.52.0-rc.5 1.52.0-rc.4 1.52.0-rc.3 1.52.0-rc.2 1.52.0-rc.1 1.52.0-rc.0 1.51.2 1.51.1 retired 1.51.0 1.50.21 1.50.20 1.50.19 1.50.18 1.50.17 1.50.16 1.50.15 1.50.14 1.50.13 1.50.12 1.50.11 1.50.10 1.50.9 1.50.8 1.50.7 1.50.6 1.50.5 1.50.4 1.50.3 1.50.2 1.50.1 1.50.0 1.49.0 1.48.0-rc.30 1.48.0-rc.29 1.48.0-rc.28 1.48.0-rc.27 1.48.0-rc.26 1.48.0-rc.25 1.48.0-rc.24 1.48.0-rc.23 1.48.0-rc.22 1.48.0-rc.21 1.48.0-rc.20 1.48.0-rc.19 1.48.0-rc.18 1.48.0-rc.17 1.48.0-rc.16 1.48.0-rc.15 1.48.0-rc.14 1.48.0-rc.13 1.48.0-rc.12 1.48.0-rc.11 1.48.0-rc.10 1.48.0-rc.9 1.48.0-rc.8 1.48.0-rc.7 1.48.0-rc.6 1.48.0-rc.5 1.48.0-rc.4 1.48.0-rc.3 1.48.0-rc.2 1.48.0-rc.1 1.48.0-rc.0 1.47.12 1.47.11 1.47.10 1.47.9 1.47.8 1.47.7 1.47.6 1.47.5 1.47.4 1.47.3 1.47.2 1.47.1 1.47.0 1.46.13 1.46.12 1.46.11 1.46.10 1.46.9 1.46.8 1.46.7 1.46.6 1.46.5 1.46.4 1.46.3 1.46.2 1.46.1 1.46.0 1.45.0-rc9 1.45.0-rc8 1.45.0-rc7 1.45.0-rc6 1.45.0-rc5 1.45.0-rc4 1.45.0-rc3 1.45.0-rc20 1.45.0-rc2 1.45.0-rc19 1.45.0-rc18 1.45.0-rc17 1.45.0-rc16 1.45.0-rc15 1.45.0-rc14 1.45.0-rc13 1.45.0-rc12 1.45.0-rc11 1.45.0-rc10 1.45.0-rc1 1.45.0-rc0 1.44.13 1.44.12 1.44.11 1.44.10 1.44.9 1.44.8 1.44.7 1.44.6 1.44.5 1.44.4 1.44.3 1.44.2 1.44.1 1.44.0 1.43.12 1.43.11 1.43.10 1.43.9 1.43.8 1.43.7 1.43.6 1.43.5 1.43.4 1.43.3 1.43.2 1.43.1 1.43.0 1.42.0 1.41.12 1.41.11 1.41.10 1.41.9 1.41.8 1.41.7 1.41.6 1.41.5 1.41.4 1.41.3 1.41.2 1.41.1 1.41.0 1.40.0 1.39.7 1.39.6 1.39.5 1.39.4 1.39.3 1.39.2 1.39.1 1.39.0 1.38.0 1.37.2 1.37.1 1.37.0 1.36.22 1.36.21 1.36.19 1.36.18 1.36.17 1.36.16 1.36.15 1.36.14 1.36.13 1.36.12 1.36.11 1.36.10 1.36.9 1.36.8 1.36.7 1.36.6 1.36.5 1.36.4 1.36.3 1.36.2 1.36.0 1.35.1 1.35.0 1.34.9 1.34.8 1.34.7 1.34.6 1.34.5 1.34.4 1.34.3 1.34.2 1.34.1 1.34.0 1.33.0 1.32.2 1.32.1 1.32.0 1.31.1 1.31.0 1.30.2 1.30.1 1.29.0-rc1 1.29.0-rc0 1.28.1 1.28.0 1.27.1 1.27.0 1.26.13 1.26.12 1.26.11 1.26.10 1.26.9 1.26.8 1.26.7 1.26.6 1.26.5 1.26.4 1.26.2 1.26.1 1.26.0 1.25.8 1.25.7 1.25.6 1.25.5 1.25.4 1.25.3 1.25.2 1.25.1 1.25.0 1.24.2 1.24.1 1.24.0 1.23.3 1.23.2 1.23.1 1.23.0 1.22.1 1.22.0 1.20.1 1.20.0 1.19.1 1.19.0 1.18.1 1.18.0 1.17.1 1.17.0 1.16.2 1.15.1 1.15.0 1.14.0 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.12.0 1.11.1 1.11.0 1.10.0 1.9.0 1.8.0 1.7.0 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.1 1.5.0 1.4.1 1.4.0 1.3.1 1.3.0 1.2.1 1.2.0 1.1.3 1.1.2 1.1.0 1.0.3 1.0.2 1.0.1 1.0.0 0.13.1 0.13.0 0.12.0 0.10.0 0.9.1 0.9.0 0.8.0 0.7.0 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.2 0.5.1 0.5.0 0.4.0 0.3.0 0.2.0 0.1.9 0.1.8 0.1.3 0.1.1 0.1.0

A declarative, extensible framework for building Elixir applications.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ash lib ash engine request.ex
Raw

lib/ash/engine/request.ex

defmodule Ash.Engine.Request do
@moduledoc false
alias Ash.Error.Forbidden.MustPassStrictCheck
alias Ash.Error.Framework.AssumptionFailed
alias Ash.Error.Invalid.{DuplicatedPath, ImpossiblePath}
defmodule UnresolvedField do
@moduledoc false
defstruct [:resolver, deps: [], data?: false]
def new(dependencies, func) do
%__MODULE__{
resolver: func,
deps: deps(dependencies)
}
end
defp deps(deps) do
deps
|> List.wrap()
|> Enum.map(fn dep -> List.wrap(dep) end)
end
end
defimpl Inspect, for: UnresolvedField do
import Inspect.Algebra
def inspect(field, opts) do
concat([
"#UnresolvedField<",
to_doc(field.deps, opts),
">"
])
end
end
defstruct [
:id,
:async?,
:error?,
:resource,
:changeset,
:path,
:action_type,
:action,
:data,
:name,
:api,
:query,
:authorization_filter,
:write_to_data?,
:strict_check_only?,
:verbose?,
:state,
:actor,
:authorize?,
:engine_pid,
authorized?: false,
authorizer_state: %{},
dependencies_to_send: %{},
dependency_data: %{},
dependencies_requested: []
]
@type t :: %__MODULE__{}
require Logger
alias Ash.Authorizer
def resolve(dependencies \\ [], func) do
UnresolvedField.new(dependencies, func)
end
def new(opts) do
query =
case opts[:query] do
%UnresolvedField{} = query ->
query
%Ash.Query{} = query ->
query
nil ->
nil
other ->
raise "Got a weird thing #{inspect(other)}"
end
id = Ecto.UUID.generate()
data =
case opts[:data] do
%UnresolvedField{} = unresolved ->
%{unresolved | data?: true}
other ->
other
end
%__MODULE__{
id: id,
resource: opts[:resource],
changeset: opts[:changeset],
path: List.wrap(opts[:path]),
action_type: opts[:action_type],
action: opts[:action],
async?: Keyword.get(opts, :async?, true),
data: data,
query: query,
api: opts[:api],
name: opts[:name],
strict_check_only?: opts[:strict_check_only?],
state: :strict_check,
actor: opts[:actor],
authorized?: opts[:authorize?] == false,
verbose?: opts[:verbose?] || false,
authorize?: opts[:authorize?] || true,
write_to_data?: Keyword.get(opts, :write_to_data?, true)
}
end
def next(request) do
case do_next(request) do
{:complete, new_request, notifications, dependencies} ->
if request.state != :complete do
{:complete, new_request, notifications, dependencies}
else
{:already_complete, new_request, notifications, dependencies}
end
{:waiting, new_request, notifications, dependencies} ->
{:wait, new_request, notifications, dependencies}
{:continue, new_request, notifications} ->
{:continue, new_request, notifications}
{:error, error, request} ->
{:error, error, request}
end
end
def do_next(%{state: :strict_check, authorize?: false} = request) do
log(request, "Skipping strict check due to authorize?: false")
{:continue, %{request | state: :fetch_data}, []}
end
def do_next(%{state: :strict_check} = request) do
case Ash.Resource.authorizers(request.resource) do
[] ->
log(request, "No authorizers found, skipping strict check")
{:continue, %{request | state: :fetch_data}, []}
authorizers ->
case strict_check(authorizers, request) do
{:ok, new_request, notifications, []} ->
new_request = set_authorized(new_request)
log(new_request, "Strict check complete")
{:continue, %{new_request | state: :fetch_data}, notifications}
{:ok, new_request, notifications, dependencies} ->
log(new_request, "Strict check incomplete, waiting on dependencies")
{:waiting, new_request, notifications, dependencies}
{:error, error} ->
log(request, "Strict checking failed")
{:error, error, request}
end
end
end
def do_next(%{state: :fetch_data} = request) do
case try_resolve_local(request, :data, true) do
{:skipped, _, _, _} ->
{:error, AssumptionFailed.exception(message: "Skipped fetching data"), request}
{:ok, request, notifications, []} ->
log(request, "data fetched: #{inspect(notifications)}")
{:continue, %{request | state: :check}, notifications}
{:ok, new_request, notifications, waiting_for} ->
log(request, "data waiting on dependencies: #{inspect(waiting_for)}")
{:waiting, new_request, notifications, waiting_for}
{:error, error} ->
log(request, "error fetching data: #{inspect(error)}")
{:error, error, request}
end
end
def do_next(%{state: :check, authorize?: false} = request) do
log(request, "Skipping check due to `authorize?: false`")
{:complete, %{request | state: :complete}, [], []}
end
def do_next(%{state: :check} = request) do
case Ash.Resource.authorizers(request.resource) do
[] ->
log(request, "No authorizers found, skipping check")
{:complete, %{request | state: :complete}, [], []}
authorizers ->
case check(authorizers, request) do
{:ok, new_request, notifications, []} ->
log(new_request, "Check complete")
new_request = set_authorized(new_request)
{:complete, %{new_request | state: :complete}, notifications, []}
{:ok, new_request, notifications, waiting} ->
log(request, "Check incomplete, waiting on dependencies")
{:waiting, new_request, notifications, waiting}
{:error, error} ->
log(request, "Check failed")
{:error, error, request}
end
end
end
def do_next(%{state: :complete} = request) do
if request.dependencies_to_send == %{} do
{:complete, request, [], []}
else
Enum.reduce_while(request.dependencies_to_send, {:complete, request, [], []}, fn
{field, _paths}, {:complete, request, notifications, deps} ->
case try_resolve_local(request, field, false) do
{:skipped, new_request, new_notifications, other_deps} ->
{:cont,
{:complete, new_request, new_notifications ++ notifications, other_deps ++ deps}}
{:ok, new_request, new_notifications, other_deps} ->
{:cont,
{:complete, new_request, new_notifications ++ notifications, other_deps ++ deps}}
{:error, error} ->
{:halt, {:error, error, request}}
end
end)
end
end
def wont_receive(request, path, field) do
log(request, "Request failed due to failed dependency #{inspect(path ++ [field])}")
{:stop, :dependency_failed, request}
end
def send_field(request, receiver_path, field) do
log(request, "Attempting to provide #{inspect(field)} for #{inspect(receiver_path)}")
case store_dependency(request, receiver_path, field) do
{:value, value, new_request} ->
{:ok, new_request, [{receiver_path, request.path, field, value}]}
{:ok, new_request, notifications} ->
{:ok, new_request, notifications}
{:waiting, new_request, notifications, []} ->
{:ok, new_request, notifications}
{:waiting, new_request, notifications, waiting_for} ->
{:waiting, new_request, notifications, waiting_for}
{:error, error, new_request} ->
log(request, "Error resolving #{field}: #{inspect(error)}")
{:error, error, new_request}
end
end
def receive_field(request, path, field, value) do
log(request, "Receiving field #{field} from #{inspect(path)}")
new_request = put_dependency_data(request, path ++ [field], value)
{:continue, new_request}
end
defp set_authorized(%{authorized?: false, resource: resource} = request) do
authorized? =
resource
|> Ash.Resource.authorizers()
|> Enum.all?(fn authorizer ->
authorizer_state(request, authorizer) == :authorizer
end)
%{request | authorized?: authorized?}
end
defp set_authorized(request), do: request
def put_dependency_data(request, dep, value) do
%{request | dependency_data: Map.put(request.dependency_data, dep, value)}
end
def store_dependency(request, receiver_path, field, internal? \\ false) do
request = do_store_dependency(request, field, receiver_path)
case try_resolve_local(request, field, internal?) do
{:skipped, new_request, notifications, []} ->
log(request, "Field #{field} was skipped, no additional dependencies")
{:ok, new_request, notifications}
{:skipped, new_request, notifications, waiting} ->
log(request, "Field #{field} was skipped, registering dependencies: #{inspect(waiting)}")
{:waiting, new_request, notifications, waiting}
{:ok, new_request, _, _} ->
case Map.get(new_request, field) do
%UnresolvedField{} ->
log(request, "Field could not be resolved #{field}, registering dependency")
{:ok, new_request, []}
value ->
log(request, "Field #{field}, was resolved and provided")
{:value, value, new_request}
end
{:error, error} ->
{:error, error, request}
end
end
defp do_store_dependency(request, field, receiver_path) do
log(request, "storing dependency on #{field} from #{inspect(receiver_path)}")
new_deps_to_send =
Map.update(request.dependencies_to_send, field, [receiver_path], fn paths ->
paths = Enum.reject(paths, &Kernel.==(&1, receiver_path))
[receiver_path | paths]
end)
%{request | dependencies_to_send: new_deps_to_send}
end
defp strict_check(authorizers, request) do
authorizers
|> Enum.reject(&(authorizer_state(request, &1) == :authorized))
|> Enum.reduce_while({:ok, request, [], []}, fn authorizer,
{:ok, request, notifications, waiting_for} ->
log(request, "strict checking")
case do_strict_check(authorizer, request) do
{:ok, new_request} ->
log(new_request, "strict check succeeded for #{inspect(authorizer)}")
{:cont, {:ok, new_request, notifications, waiting_for}}
{:ok, new_request, new_notifications, new_deps} ->
log(new_request, "strict check succeeded for #{inspect(authorizer)}")
{:cont, {:ok, new_request, new_notifications ++ notifications, waiting_for ++ new_deps}}
{:waiting, new_request, new_notifications, new_deps} ->
log(
new_request,
"waiting on dependencies: #{inspect(new_deps)} for #{inspect(authorizer)}"
)
{:cont, {:ok, new_request, notifications ++ new_notifications, new_deps ++ waiting_for}}
{:error, error} ->
log(request, "strict check failed for #{inspect(authorizer)}: #{inspect(error)}")
{:halt, {:error, error}}
end
end)
end
defp do_strict_check(authorizer, request, notifications \\ []) do
strict_check_only? = request.strict_check_only?
case missing_strict_check_dependencies?(authorizer, request) do
[] ->
case strict_check_authorizer(authorizer, request) do
:authorized ->
{:ok, set_authorizer_state(request, authorizer, :authorized)}
{:filter, filter} ->
request
|> Map.update!(:query, &Ash.Query.filter(&1, filter))
|> Map.update(
:authorization_filter,
filter,
&add_to_or_parse(&1, filter, request.resource)
)
|> set_authorizer_state(authorizer, :authorized)
|> try_resolve([request.path ++ [:query]], false)
{:filter_and_continue, _, _} when strict_check_only? ->
{:error, MustPassStrictCheck.exception(resource: request.resource)}
{:filter_and_continue, filter, new_authorizer_state} ->
new_request =
request
|> Map.update!(:query, &Ash.Query.filter(&1, filter))
|> Map.update(
:authorization_filter,
filter,
&add_to_or_parse(&1, filter, request.resource)
)
|> set_authorizer_state(authorizer, new_authorizer_state)
{:ok, new_request}
{:continue, _} when strict_check_only? ->
{:error, MustPassStrictCheck.exception(resource: request.resource)}
{:continue, authorizer_state} ->
{:ok, set_authorizer_state(request, authorizer, authorizer_state)}
{:error, error} ->
{:error, error}
end
deps ->
deps =
Enum.map(deps, fn dep ->
request.path ++ [dep]
end)
case try_resolve(request, deps, true) do
{:ok, new_request, new_notifications, []} ->
do_strict_check(authorizer, new_request, notifications ++ new_notifications)
{:ok, new_request, new_notifications, waiting_for} ->
{:waiting, new_request, notifications ++ new_notifications, waiting_for}
{:error, error} ->
{:error, error}
end
end
end
defp add_to_or_parse(existing_authorization_filter, filter, resource) do
if existing_authorization_filter do
Ash.Filter.add_to_filter(existing_authorization_filter, filter)
else
Ash.Filter.parse!(resource, filter)
end
end
defp check(authorizers, request) do
authorizers
|> Enum.reject(&(authorizer_state(request, &1) == :authorized))
|> Enum.reduce_while({:ok, request, [], []}, fn authorizer,
{:ok, request, notifications, waiting_for} ->
case do_check(authorizer, request) do
{:ok, new_request} ->
log(request, "check succeeded for #{inspect(authorizer)}")
{:cont, {:ok, new_request, notifications, waiting_for}}
{:ok, new_request, new_notifications, new_deps} ->
log(request, "check succeeded for #{inspect(authorizer)}")
{:cont, {:ok, new_request, new_notifications ++ notifications, new_deps ++ waiting_for}}
{:waiting, new_request, new_notifications, new_deps} ->
log(
request,
"waiting on dependencies: #{inspect(new_deps)} for #{inspect(authorizer)}"
)
{:cont, {:ok, new_request, new_notifications ++ notifications, new_deps ++ waiting_for}}
{:error, error} ->
log(request, "check failed for #{inspect(authorizer)}: #{inspect(error)}")
{:halt, {:error, error}}
end
end)
end
defp do_check(authorizer, request, notifications \\ []) do
case missing_check_dependencies(authorizer, request) do
[] ->
case check_authorizer(authorizer, request) do
:authorized ->
{:ok, set_authorizer_state(request, authorizer, :authorized)}
{:filter, filter} ->
request
|> set_authorizer_state(authorizer, :authorized)
|> Map.update(
:authorization_filter,
filter,
&Ash.Filter.add_to_filter(&1, filter)
)
|> runtime_filter(authorizer, filter)
{:error, error} ->
{:error, error}
end
deps ->
deps =
Enum.map(deps, fn dep ->
request.path ++ [dep]
end)
case try_resolve(request, deps, true) do
{:ok, new_request, new_notifications, []} ->
do_check(authorizer, new_request, notifications ++ new_notifications)
{:ok, new_request, new_notifications, waiting_for} ->
{:waiting, new_request, new_notifications ++ notifications, waiting_for}
{:error, error} ->
{:error, error}
end
end
end
defp runtime_filter(request, authorizer, filter) do
case do_runtime_filter(request, filter) do
{:ok, request} ->
request
|> set_authorizer_state(authorizer, :authorized)
|> try_resolve([request.path ++ [:data], request.path ++ [:query]], false)
{:error, error} ->
{:error, error}
end
end
defp do_runtime_filter(%{data: empty} = request, _filter) when empty in [nil, []],
do: {:ok, request}
defp do_runtime_filter(request, filter) do
pkey = Ash.Resource.primary_key(request.resource)
pkeys =
request.data
|> List.wrap()
|> Enum.map(fn record ->
record |> Map.take(pkey) |> Map.to_list()
end)
primary_key_filter =
case pkeys do
[pkey] -> [pkey]
pkeys -> [or: pkeys]
end
new_query =
request.query
|> Ash.Query.filter(primary_key_filter)
|> Ash.Query.filter(filter)
request.api.read(new_query)
|> case do
{:ok, results} ->
pkey = Ash.Resource.primary_key(request.resource)
pkeys = Enum.map(results, &Map.take(&1, pkey))
new_data = Enum.filter(request.data, &(Map.take(&1, pkey) in pkeys))
{:ok, %{request | data: new_data, query: new_query}}
{:error, error} ->
{:error, error}
end
end
defp try_resolve(request, deps, internal?) do
Enum.reduce_while(deps, {:ok, request, [], []}, fn dep,
{:ok, request, notifications, skipped} ->
case get_dependency_data(request, dep) do
{:ok, _value} ->
{:cont, {:ok, request, notifications, skipped}}
:error ->
do_try_resolve(request, notifications, skipped, dep, internal?)
end
end)
end
defp do_try_resolve(request, notifications, skipped, dep, internal?) do
if local_dep?(request, dep) do
case try_resolve_local(request, List.last(dep), internal?) do
{:skipped, request, new_notifications, other_deps} ->
{:cont, {:ok, request, new_notifications ++ notifications, skipped ++ other_deps}}
{:ok, request, new_notifications, other_deps} ->
{:cont, {:ok, request, new_notifications ++ notifications, skipped ++ other_deps}}
{:error, error} ->
{:halt, {:error, error}}
end
else
{:cont, {:ok, request, notifications, [dep | skipped]}}
end
end
defp try_resolve_local(request, field, internal?) do
authorized? = Enum.all?(Map.values(request.authorizer_state), &(&1 == :authorized))
# Don't fetch honor requests for dat until the request is authorized
if field in [:data, :query] and not authorized? and not internal? do
try_resolve_dependencies_of(request, field, internal?)
else
case Map.get(request, field) do
%UnresolvedField{} = unresolved ->
do_try_resolve_local(request, field, unresolved, internal?)
value ->
notify_existing_value(request, field, value, internal?)
end
end
end
defp try_resolve_dependencies_of(request, field, internal?) do
case Map.get(request, field) do
%UnresolvedField{deps: deps} ->
case try_resolve(request, deps, internal?) do
{:ok, new_request, notifications, remaining_deps} ->
{:skipped, new_request, notifications, remaining_deps}
error ->
error
end
_ ->
{:skipped, request, [], []}
end
end
defp notify_existing_value(request, field, value, internal?) do
if internal? do
{new_request, notifications} = notifications(request, field, value)
{:ok, new_request, notifications, []}
else
{:ok, request, [], []}
end
end
defp do_try_resolve_local(request, field, unresolved, internal?) do
%{deps: deps, resolver: resolver} = unresolved
with {:ok, new_request, notifications, []} <-
try_resolve(request, deps, internal?) do
resolver_context = resolver_context(new_request, deps)
log(request, "resolving #{field}")
case resolver.(resolver_context) do
{:ok, value} ->
value = process_resolved_field(field, value, request)
{new_request, notifications} =
if internal? do
{new_request, new_notifications} = notifications(new_request, field, value)
notifications =
Enum.concat([
notifications,
new_notifications
])
{new_request, notifications}
else
{request, []}
end
new_request = Map.put(new_request, field, value)
{:ok, new_request, notifications, []}
{:error, error} ->
{:error, error}
end
end
end
defp process_resolved_field(:query, %Ash.Query{} = query, request) do
Ash.Query.set_datalayer_context(query, %{
authorize?: request.authorize?,
actor: request.actor
})
end
defp process_resolved_field(:changeset, %Ash.Changeset{} = changeset, request) do
Ash.Changeset.set_datalayer_context(changeset, %{
authorize?: request.authorize?,
actor: request.actor
})
end
defp process_resolved_field(_, value, _), do: value
defp get_dependency_data(request, dep) do
if local_dep?(request, dep) do
case Map.fetch(request, List.last(dep)) do
{:ok, %UnresolvedField{}} -> :error
{:ok, value} -> {:ok, value}
:error -> :error
end
else
Map.fetch(request.dependency_data, dep)
end
end
defp notifications(request, field, value) do
case Map.fetch(request.dependencies_to_send, field) do
{:ok, paths} ->
new_request = %{
request
| dependencies_to_send: Map.delete(request.dependencies_to_send, field)
}
notifications =
Enum.map(paths, fn path ->
{path, request.path, field, value}
end)
{new_request, notifications}
:error ->
{request, []}
end
end
defp resolver_context(request, deps) do
Enum.reduce(deps, %{}, fn dep, resolver_context ->
case get_dependency_data(request, dep) do
{:ok, value} ->
Ash.Engine.put_nested_key(resolver_context, dep, value)
:error ->
resolver_context
end
end)
end
defp local_dep?(request, dep) do
:lists.droplast(dep) == request.path
end
def add_initial_authorizer_state(request) do
request.resource
|> Ash.Resource.authorizers()
|> Enum.reduce(request, fn authorizer, request ->
if request.authorize? do
initial_state =
Authorizer.initial_state(
authorizer,
request.actor,
request.resource,
request.action,
request.verbose?
)
set_authorizer_state(request, authorizer, initial_state)
else
set_authorizer_state(request, authorizer, :authorized)
end
end)
end
defp missing_strict_check_dependencies?(authorizer, request) do
authorizer
|> Authorizer.strict_check_context(authorizer_state(request, authorizer))
|> Enum.filter(fn dependency ->
match?(%UnresolvedField{}, Map.get(request, dependency))
end)
end
defp missing_check_dependencies(authorizer, request) do
authorizer
|> Authorizer.check_context(authorizer_state(request, authorizer))
|> Enum.filter(fn dependency ->
match?(%UnresolvedField{}, Map.get(request, dependency))
end)
end
defp strict_check_authorizer(authorizer, request) do
log(request, "strict checking for #{inspect(authorizer)}")
authorizer_state = authorizer_state(request, authorizer)
keys = Authorizer.strict_check_context(authorizer, authorizer_state)
Authorizer.strict_check(authorizer, authorizer_state, Map.take(request, keys))
end
defp check_authorizer(authorizer, request) do
log(request, "checking for #{inspect(authorizer)}")
authorizer_state = authorizer_state(request, authorizer)
keys = Authorizer.check_context(authorizer, authorizer_state)
Authorizer.check(authorizer, authorizer_state, Map.take(request, keys))
end
defp set_authorizer_state(request, authorizer, authorizer_state) do
%{
request
| authorizer_state: Map.put(request.authorizer_state, authorizer, authorizer_state)
}
end
defp authorizer_state(request, authorizer) do
Map.get(request.authorizer_state, authorizer) || %{}
end
def validate_requests(requests) do
with :ok <- validate_unique_paths(requests),
:ok <- validate_dependencies(requests) do
:ok
else
{:error, {:impossible, path}} ->
{:error, ImpossiblePath.exception(impossible_path: path)}
{:error, paths} ->
{:error, DuplicatedPath.exception(paths: paths)}
end
end
defp validate_unique_paths(requests) do
requests
|> Enum.group_by(& &1.path)
|> Enum.filter(fn {_path, value} ->
Enum.count(value, & &1.write_to_data?) > 1
end)
|> case do
[] ->
:ok
invalid_paths ->
invalid_paths = Enum.map(invalid_paths, &elem(&1, 0))
{:error, invalid_paths}
end
end
defp validate_dependencies(requests) do
result =
Enum.reduce_while(requests, :ok, fn request, :ok ->
case do_build_dependencies(request, requests) do
:ok -> {:cont, :ok}
{:error, error} -> {:halt, {:error, error}}
end
end)
case result do
{:ok, requests} -> {:ok, Enum.reverse(requests)}
other -> other
end
end
defp do_build_dependencies(request, requests, trail \\ []) do
request
|> Map.from_struct()
|> Enum.reduce_while(:ok, fn
{_key, %UnresolvedField{deps: deps}}, :ok ->
case expand_deps(deps, requests, trail) do
{:error, error} ->
{:halt, {:error, error}}
:ok ->
{:cont, :ok}
end
_, :ok ->
{:cont, :ok}
end)
end
defp expand_deps([], _, _), do: :ok
defp expand_deps(deps, requests, trail) do
Enum.reduce_while(deps, :ok, fn dep, :ok ->
case do_expand_dep(dep, requests, trail) do
:ok -> {:cont, :ok}
{:error, error} -> {:halt, {:error, error}}
end
end)
end
defp do_expand_dep(dep, requests, trail) do
if dep in trail do
{:error, {:circular, dep}}
else
request_path = :lists.droplast(dep)
request_key = List.last(dep)
case Enum.find(requests, &(&1.path == request_path)) do
nil ->
{:error, {:impossible, dep}}
%{^request_key => %UnresolvedField{deps: nested_deps}} ->
case expand_deps(nested_deps, requests, [dep | trail]) do
:ok -> :ok
other -> other
end
_ ->
:ok
end
end
end
defp log(request, message, level \\ :debug)
defp log(%{verbose?: true, name: name} = request, message, level) do
if is_list(request.data) do
Logger.log(level, "#{name}: #{Enum.count(request.data)} #{message}")
else
Logger.log(level, "#{name}: #{message}")
end
end
defp log(_, _, _) do
false
end
end