Packages
ash
1.50.9
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
Current section
Files
lib/ash/engine/request.ex
defmodule Ash.Engine.Request do
@moduledoc """
Represents an individual request to be processed by the engine.
See `new/1` for more information
"""
defstruct [
:id,
:async?,
:error?,
:resource,
:changeset,
:path,
:action_type,
:action,
:data,
:name,
:api,
:query,
:data_layer_query,
:authorization_filter,
:write_to_data?,
:strict_check_only?,
:verbose?,
:state,
:actor,
:authorize?,
:engine_pid,
manage_changeset?: false,
notify?: false,
authorized?: false,
authorizer_state: %{},
dependencies_to_send: %{},
dependency_data: %{},
dependencies_requested: []
]
@type t :: %__MODULE__{}
alias Ash.Authorizer
alias Ash.Error.Forbidden.MustPassStrictCheck
alias Ash.Error.Invalid.{DuplicatedPath, ImpossiblePath}
require Ash.Query
require Logger
defmodule UnresolvedField do
@moduledoc """
Represents an unresolved field to be resolved by the engine
"""
defstruct [:resolver, deps: [], data?: false]
@type t :: %__MODULE__{}
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
@doc """
Create an unresolved field.
Can have dependencies, which is a list of atoms. All elements
before the last comprise the path of a request that is also
being processed, like `[:data]`, and the last element is the
key of that request that is required. Make sure to pass a
list of lists of atoms. The second argument is a map, which
contains all the values you requested, at the same path
that they were requested.
For example:
resolve([[:data, :query], [:data, :data]], fn %{data: %{query: query, data: data}} ->
data # This is the data field of the [:data] request
query # This is the query field of the [:data] request
{:ok, result}
# or
{:error, error}
# or
result
end)
"""
def resolve(dependencies \\ [], func) do
UnresolvedField.new(dependencies, func)
end
@doc """
Creates a new request.
The field values may be explicit values, or they may be
instances of `UnresolvedField`.
When other requests depend on a value from this request, they will
not be sent unless this request has completed its authorization (or this
request has been configured not to do authorization). This allows requests
to depend on eachother without those requests happening just before a request
fails with a forbidden error. These fields are `data`, `query`, `changeset`
and `authorized?`.
A field may not be resolved if the data of a request has been resolved and
no other requests depend on that field.
Options:
* query - The query to be used to fetch data. Used to authorize reads.
* data - The ultimate goal of a request is to compute the data
* resource - The primary resource of the request. Used for openeing transactions on creates/updates/destroys
* changeset - Any changes to be made to the resource. Used to authorize writes.
* path - The path of the request. This serves as a unique id, and is the way that other requests can refer to this one
* action_type - The action_type of the request
* action - The action being performed on the data
* async? - Whether or not the request *can* be asynchronous, defaults to `true`.
* api - The api module being called
* name - A human readable name for the request, used when logging/in errors
* strict_check_only? - If true, authorization will not be allowed to proceed to a runtime check (so it cannot run db queries unless authorization is assured)
* actor - The actor performing the action, used for authorization
* authorize? - Wether or not to perform authorization (defaults to true)
* verbose? - print informational logs (warning, this will be a whole lot of logs)
* write_to_data? - If set to false, this value is not returned from the initial call to the engine
"""
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 = Ash.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,
data_layer_query: resolve([], fn _ -> nil end),
manage_changeset?: opts[:manage_changeset?] || false,
api: opts[:api],
name: opts[:name],
strict_check_only?: opts[:strict_check_only?],
state: :strict_check,
actor: opts[:actor],
notify?: opts[:notify?] == true,
authorized?: opts[:authorize?] == false,
verbose?: opts[:verbose?] || false,
authorize?: opts[:authorize?] || true,
write_to_data?: Keyword.get(opts, :write_to_data?, true)
}
end
def resource_notification(request) do
%Ash.Notifier.Notification{
resource: request.resource,
api: request.api,
actor: request.actor,
action: request.action,
data: request.data,
changeset: request.changeset
}
end
def next(request) do
case do_next(request) do
{:complete, new_request, notifications, dependencies} ->
notifications = update_changeset(request, new_request.changeset, notifications)
if request.state != :complete do
{:complete, new_request, notifications, dependencies}
else
{:already_complete, new_request, notifications, dependencies}
end
{:waiting, new_request, notifications, dependencies} ->
notifications = update_changeset(request, new_request.changeset, notifications)
{:wait, new_request, notifications, dependencies}
{:continue, new_request, notifications} ->
notifications = update_changeset(request, new_request.changeset, notifications)
{:continue, new_request, notifications}
{:error, error, request} ->
if request.manage_changeset? && !match?(%UnresolvedField{}, request.changeset) do
new_changeset = Ash.Changeset.add_error(request.changeset, error)
notifications = update_changeset(request, new_changeset, [])
{:error, error, notifications, %{request | changeset: new_changeset}}
else
{:error, error, request}
end
end
end
defp update_changeset(
%{manage_changeset?: true, changeset: changeset},
new_changeset,
notifications
) do
if new_changeset != changeset && not match?(%UnresolvedField{}, new_changeset) do
[{:update_changeset, changeset} | notifications]
else
notifications
end
end
defp update_changeset(_, _, notifications), do: notifications
def do_next(%{state: :strict_check, authorize?: false} = request) do
log(request, fn -> "Skipping strict check due to authorize?: false" end)
{:continue, %{request | state: :fetch_data}, []}
end
def do_next(%{state: :strict_check} = request) do
case Ash.Resource.Info.authorizers(request.resource) do
[] ->
log(request, fn -> "No authorizers found, skipping strict check" end)
{: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, fn -> "Strict check complete" end)
{:continue, %{new_request | state: :fetch_data}, notifications}
{:ok, new_request, notifications, dependencies} ->
log(new_request, fn -> "Strict check incomplete, waiting on dependencies" end)
{:waiting, new_request, notifications, dependencies}
{:error, error} ->
log(request, fn -> "Strict checking failed" end)
{:error, error, request}
end
end
end
def do_next(%{state: :fetch_data} = request) do
key =
case request.changeset do
%UnresolvedField{} ->
:changeset
_ ->
:data
end
case try_resolve_local(request, key, true) do
{:skipped, new_request, notifications, waiting_for} ->
{:waiting, new_request, notifications, waiting_for}
{:ok, request, notifications, []} ->
if key == :changeset do
{:continue, request, notifications}
else
log(request, fn -> "data fetched: #{inspect(notifications)}" end)
{:continue, %{request | state: :check}, notifications}
end
{:ok, new_request, notifications, waiting_for} ->
log(request, fn -> "#{key} waiting on dependencies: #{inspect(waiting_for)}" end)
{:waiting, new_request, notifications, waiting_for}
{:error, error} ->
log(request, fn -> "error fetching data: #{inspect(error)}" end)
{:error, error, request}
end
end
def do_next(%{state: :check, authorize?: false} = request) do
log(request, fn -> "Skipping check due to `authorize?: false`" end)
{:complete, %{request | state: :complete}, [], []}
end
def do_next(%{state: :check} = request) do
case Ash.Resource.Info.authorizers(request.resource) do
[] ->
log(request, fn -> "No authorizers found, skipping check" end)
{:complete, %{request | state: :complete}, [], []}
authorizers ->
case check(authorizers, request) do
{:ok, new_request, notifications, []} ->
log(new_request, fn -> "Check complete" end)
new_request = set_authorized(new_request)
{:complete, %{new_request | state: :complete}, notifications, []}
{:ok, new_request, notifications, waiting} ->
log(request, fn -> "Check incomplete, waiting on dependencies" end)
{:waiting, new_request, notifications, waiting}
{:error, error} ->
log(request, fn -> "Check failed" end)
{: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} ->
new_request = %{new_request | state: :complete}
{:cont,
{:complete, new_request, new_notifications ++ notifications, other_deps ++ deps}}
{:ok, new_request, new_notifications, other_deps} ->
new_request = %{new_request | state: :complete}
{: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, fn -> "Request failed due to failed dependency #{inspect(path ++ [field])}" end)
{:stop, :dependency_failed, request}
end
def send_field(request, receiver_path, field) do
log(request, fn -> "Attempting to provide #{inspect(field)} for #{inspect(receiver_path)}" end)
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, fn -> "Error resolving #{field}: #{inspect(error)}" end)
{:error, error, new_request}
end
end
def receive_field(request, path, field, value) do
log(request, fn -> "Receiving field #{field} from #{inspect(path)}" end)
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.Info.authorizers()
|> Enum.all?(fn authorizer ->
authorizer_state(request, authorizer) == :authorized
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, fn -> "Field #{field} was skipped, no additional dependencies" end)
{:ok, new_request, notifications}
{:skipped, new_request, notifications, waiting} ->
log(request, fn ->
"Field #{field} was skipped, registering dependencies: #{inspect(waiting)}"
end)
{:waiting, new_request, notifications, waiting}
{:ok, new_request, _, _} ->
case Map.get(new_request, field) do
%UnresolvedField{} ->
log(request, fn -> "Field could not be resolved #{field}, registering dependency" end)
{:ok, new_request, []}
value ->
log(request, fn -> "Field #{field}, was resolved and provided" end)
{:value, value, new_request}
end
{:error, error} ->
{:error, error, request}
end
end
defp do_store_dependency(request, field, receiver_path) do
log(request, fn -> "storing dependency on #{field} from #{inspect(receiver_path)}" end)
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, fn -> "strict checking" end)
case do_strict_check(authorizer, request) do
{:ok, new_request} ->
log(new_request, fn -> "strict check succeeded for #{inspect(authorizer)}" end)
{:cont, {:ok, new_request, notifications, waiting_for}}
{:ok, new_request, new_notifications, new_deps} ->
log(new_request, fn -> "strict check succeeded for #{inspect(authorizer)}" end)
{:cont, {:ok, new_request, new_notifications ++ notifications, waiting_for ++ new_deps}}
{:waiting, new_request, new_notifications, new_deps} ->
log(
new_request,
fn -> "waiting on dependencies: #{inspect(new_deps)} for #{inspect(authorizer)}" end
)
{:cont, {:ok, new_request, notifications ++ new_notifications, new_deps ++ waiting_for}}
{:error, error} ->
log(request, fn ->
"strict check failed for #{inspect(authorizer)}: #{inspect(error)}"
end)
{: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), notifications, []}
{:filter, filter} ->
request
|> apply_filter(authorizer, filter, true)
|> case do
{:ok, request} ->
{:ok, request, notifications, []}
{:ok, request, new_notifications, deps} ->
{:ok, request, new_notifications ++ notifications, deps}
other ->
other
end
{:filter_and_continue, _, _} when strict_check_only? ->
{:error, MustPassStrictCheck.exception(resource: request.resource)}
{:filter_and_continue, filter, new_authorizer_state} ->
request
|> set_authorizer_state(authorizer, new_authorizer_state)
|> apply_filter(authorizer, filter)
|> case do
{:ok, request} ->
{:ok, request, notifications, []}
{:ok, request, new_notifications, deps} ->
{:ok, request, new_notifications ++ notifications, deps}
other ->
other
end
{:continue, _} when strict_check_only? ->
{:error, MustPassStrictCheck.exception(resource: request.resource)}
{:continue, authorizer_state} ->
{:ok, set_authorizer_state(request, authorizer, authorizer_state), notifications, []}
{: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 apply_filter(request, authorizer, filter, resolve_data? \\ false)
defp apply_filter(
%{action: %{type: :read}} = request,
authorizer,
filter,
resolve_data?
) do
request =
request
|> Map.update!(:query, &Ash.Query.filter(&1, ^filter))
|> Map.update(
:authorization_filter,
filter,
&add_to_or_parse(&1, filter, request.resource, request.query)
)
|> set_authorizer_state(authorizer, :authorized)
if resolve_data? do
try_resolve(request, [request.path ++ [:query]], false)
else
{:ok, request}
end
end
defp apply_filter(request, authorizer, filter, resolve_data?) do
case do_runtime_filter(request, filter) do
{:ok, request} ->
request = set_authorizer_state(request, authorizer, :authorized)
if resolve_data? do
try_resolve(request, [request.path ++ [:query]], false)
else
{:ok, request}
end
{:error, error} ->
{:error, error}
end
end
defp add_to_or_parse(existing_authorization_filter, filter, resource, query) do
if existing_authorization_filter do
Ash.Filter.add_to_filter(
existing_authorization_filter,
filter,
query.aggregates,
query.calculations,
query.context
)
else
Ash.Filter.parse!(resource, filter, query.aggregates, query.calculations, query.context)
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, fn -> "check succeeded for #{inspect(authorizer)}" end)
{:cont, {:ok, new_request, notifications, waiting_for}}
{:ok, new_request, new_notifications, new_deps} ->
log(request, fn -> "check succeeded for #{inspect(authorizer)}" end)
{:cont, {:ok, new_request, new_notifications ++ notifications, new_deps ++ waiting_for}}
{:waiting, new_request, new_notifications, new_deps} ->
log(
request,
fn -> "waiting on dependencies: #{inspect(new_deps)} for #{inspect(authorizer)}" end
)
{:cont, {:ok, new_request, new_notifications ++ notifications, new_deps ++ waiting_for}}
{:error, error} ->
log(request, fn -> "check failed for #{inspect(authorizer)}: #{inspect(error)}" end)
{: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(%{action: %{type: :read}, data: empty} = request, _filter)
when empty in [nil, []],
do: {:ok, request}
defp do_runtime_filter(%{action: %{type: :read}} = request, filter) do
pkey = Ash.Resource.Info.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)
new_query
|> Ash.Actions.Read.unpaginated_read()
|> case do
{:ok, results} ->
pkey = Ash.Resource.Info.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 do_runtime_filter(%{action: %{type: :create}, changeset: changeset} = request, filter) do
{:ok, fake_result} = Ash.Changeset.apply_attributes(changeset, force?: true)
case Ash.Filter.parse(request.resource, filter) do
{:ok, filter} ->
case Ash.Filter.Runtime.do_match(fake_result, filter) do
{:ok, true} ->
{:ok, request}
{:ok, false} ->
{:error, Ash.Error.Forbidden.exception([])}
:unknown ->
Logger.error("""
Could not apply filter policy because it cannot be checked using Ash.Filter.Runtime: #{inspect(filter)}.
If you are using ash_policy_authorizer policy must include a filter like this, try setting the access_type to `:runtime`"
Otherwise, please report this issue: https://github.com/ash-project/ash_policy_authorizer/issues/new?assignees=&labels=bug%2C+needs+review&template=bug_report.md&title=
""")
{:error, Ash.Error.Forbidden.exception([])}
end
{:error, error} ->
{:error, error}
end
end
defp do_runtime_filter(request, filter) do
pkey = Ash.Resource.Info.primary_key(request.resource)
pkey =
request.changeset.data
|> Map.take(pkey)
|> Map.to_list()
new_query =
request.resource
|> Ash.Query.set_tenant(request.changeset.tenant)
|> Ash.Query.set_context(request.changeset.context)
|> Ash.Query.filter(^pkey)
|> Ash.Query.filter(^filter)
|> Ash.Query.limit(1)
new_query
|> Ash.Actions.Read.unpaginated_read()
|> case do
{:ok, []} ->
{:error, Ash.Error.Forbidden.exception([])}
{:ok, [_]} ->
{:ok, request}
{:error, error} ->
{:error, error}
end
rescue
e ->
Logger.error(
"Exception while running authorization query: #{inspect(Exception.message(e))}"
)
{:error, Ash.Error.Forbidden.exception([])}
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 data until the request is authorized
if field in [
:data,
:query,
:changeset,
:authorized?,
:data_layer_query,
:authorization_filter
] 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
{:ok, request, [], []}
else
{new_request, notifications} = notifications(request, field, value)
{:ok, new_request, notifications, []}
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, fn -> "resolving #{field}" end)
case resolver.(resolver_context) do
{:requests, requests} ->
log(request, fn ->
"#{field} added requests #{inspect(Enum.map(requests, & &1.path))}"
end)
new_deps =
Enum.flat_map(requests, fn
{request, key} ->
[request.path ++ [key]]
_request ->
[]
end)
new_unresolved =
Map.update!(
unresolved,
:deps,
&(&1 ++ new_deps)
)
new_request = Map.put(request, field, new_unresolved)
new_requests =
Enum.map(requests, fn
{request, _} ->
request
request ->
request
end)
{:skipped, new_request,
notifications ++
[{:requests, new_requests}], new_deps}
{:ok, value, instructions} ->
log(request, fn ->
"successfully resolved #{field} with instructions"
end)
set_data_notifications =
Enum.map(Map.get(instructions, :extra_data, %{}), fn {key, value} ->
{:set_extra_data, key, value}
end)
resource_notifications = Map.get(instructions, :notifications, [])
extra_requests = Map.get(instructions, :requests, [])
unless Enum.empty?(extra_requests) do
log(request, fn ->
"added requests #{inspect(Enum.map(extra_requests, & &1.path))}"
end)
end
request_notifications =
case extra_requests do
[] ->
[]
nil ->
[]
requests ->
[{:requests, requests}]
end
handle_successful_resolve(
field,
value,
request,
new_request,
notifications ++
resource_notifications ++
set_data_notifications ++ request_notifications,
internal?
)
{:ok, value} ->
log(request, fn ->
"successfully resolved #{field}"
end)
handle_successful_resolve(
field,
value,
request,
new_request,
notifications,
internal?
)
{:error, error} ->
log(request, fn ->
"error resolving #{field}:\n #{inspect(error)}"
end)
{:error, error}
end
end
end
defp handle_successful_resolve(field, value, request, new_request, notifications, internal?) do
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, Enum.filter(notifications, &match?({:requests, _}, &1))}
end
new_request = Map.put(new_request, field, value)
{:ok, new_request, notifications, []}
end
defp process_resolved_field(:query, %Ash.Query{} = query, request) do
Ash.Query.set_context(query, %{
authorize?: request.authorize?,
actor: request.actor
})
end
defp process_resolved_field(:changeset, %Ash.Changeset{} = changeset, request) do
Ash.Changeset.set_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)
|> Map.put(:verbose?, request.verbose?)
end
defp local_dep?(request, dep) do
:lists.droplast(dep) == request.path
end
def add_initial_authorizer_state(request) do
request.resource
|> Ash.Resource.Info.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))
|> List.wrap()
|> 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, fn -> "strict checking for #{inspect(authorizer)}" end)
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, fn -> "checking for #{inspect(authorizer)}" end)
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
validate_unique_paths!(requests)
validate_dependencies!(requests)
:ok
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))
raise DuplicatedPath, paths: invalid_paths
end
end
defp validate_dependencies!(requests) do
Enum.each(requests, &do_build_dependencies(&1, requests))
:ok
end
defp do_build_dependencies(request, requests, trail \\ []) do
request
|> Map.from_struct()
|> Enum.each(fn
{_key, %UnresolvedField{deps: deps}} ->
expand_deps(deps, requests, trail)
_ ->
:ok
end)
end
defp expand_deps(deps, requests, trail) do
case do_expand_deps(deps, requests, trail) do
:ok ->
:ok
{:error, {:impossible, dep}} ->
raise ImpossiblePath, impossible_path: dep
end
end
defp do_expand_deps([], _, _), do: :ok
defp do_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 do_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, fn ->
message = message.()
"#{name}: #{Enum.count(request.data)} #{message}"
end)
else
Logger.log(level, fn ->
message = message.()
"#{name}: #{message}"
end)
end
end
defp log(_, _, _) do
false
end
end