Packages

ash

3.27.4
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 can.ex
Raw

lib/ash/can.ex

# SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs/contributors>
#
# SPDX-License-Identifier: MIT
defmodule Ash.Can do
@moduledoc """
Contains the Ash.can function logic.
"""
require Ash.Query
require Logger
@type subject ::
Ash.Query.t()
| Ash.Changeset.t()
| Ash.ActionInput.t()
| {Ash.Resource.t(), atom | Ash.Resource.Actions.action()}
| {Ash.Resource.t(), atom | Ash.Resource.Actions.action(), input :: map}
| {Ash.Resource.Record.t(), atom | Ash.Resource.Actions.action()}
| {Ash.Resource.Record.t(), atom | Ash.Resource.Actions.action(), input :: map}
@doc """
Returns whether an actor can perform an action, query, or changeset.
You should prefer to use `Ash.can?/3` over this module, directly.
Can raise an exception if return_forbidden_error is truthy in opts or there's an error.
"""
@spec can?(subject(), Ash.Domain.t(), Ash.Resource.Record.t(), Keyword.t()) ::
boolean() | no_return()
def can?(action_or_query_or_changeset, domain, actor, opts \\ []) do
opts =
opts
|> Keyword.put_new(:maybe_is, true)
|> Keyword.put_new(:filter_with, :filter)
case can(action_or_query_or_changeset, domain, actor, opts) do
{:ok, :maybe} ->
opts[:maybe_is]
{:ok, result} ->
result
{:ok, true, _} ->
true
{:ok, false, error} ->
if opts[:return_forbidden_error?] do
raise Ash.Error.to_ash_error(error)
else
false
end
{:error, error} ->
raise Ash.Error.to_ash_error(error)
end
end
@doc """
Returns a an ok tuple if the actor can perform the action, query, or changeset,
an error tuple if an error happens, and a ok tuple with maybe if maybe is set to true
or not set.
You should prefer to use `Ash.can/3` over this module, directly.
Note: `maybe_is` is set to `:maybe`, if not set.
"""
@spec can(subject(), Ash.Domain.t(), Ash.actor() | Ash.Scope.t(), Keyword.t()) ::
{:ok, boolean() | :maybe}
| {:ok, boolean(), term()}
| {:ok, boolean(), Ash.Changeset.t(), Ash.Query.t()}
| {:error, Ash.Error.t()}
def can(action_or_query_or_changeset, domain, actor_or_scope, opts \\ []) do
opts = Keyword.put_new(opts, :maybe_is, :maybe)
opts = Keyword.put_new(opts, :run_queries?, true)
opts = Keyword.put_new(opts, :filter_with, :filter)
pre_flight? = Keyword.get(opts, :pre_flight?, true)
context =
Ash.Helpers.deep_merge_maps(opts[:context] || %{}, %{
private: %{pre_flight_authorization?: pre_flight?}
})
{actor, opts} =
if is_struct(actor_or_scope) and Ash.Scope.ToOpts.impl_for(actor_or_scope) do
opts
|> Keyword.put(:scope, actor_or_scope)
|> Ash.Actions.Helpers.apply_scope_to_opts()
|> Keyword.pop(:actor)
else
{actor_or_scope,
opts |> Ash.Actions.Helpers.apply_scope_to_opts() |> Keyword.delete(:actor)}
end
opts = Keyword.update(opts, :context, context, &Ash.Helpers.deep_merge_maps(&1, context))
{resource, action_or_query_or_changeset, input, opts} =
case resource_subject_input(action_or_query_or_changeset, domain, actor, opts) do
{resource, action_or_query_or_changeset, input, new_opts} ->
{resource, action_or_query_or_changeset, input, Keyword.merge(new_opts, opts)}
{resource, action_or_query_or_changeset, input} ->
{resource, action_or_query_or_changeset, input, opts}
end
subject =
case action_or_query_or_changeset do
%Ash.ActionInput{} = action_input ->
action_input
|> Ash.ActionInput.set_tenant(opts[:tenant] || action_input.tenant)
|> Ash.ActionInput.set_context(%{
private: %{actor: actor, pre_flight_authorization?: pre_flight?}
})
%Ash.Query{} = query ->
query
|> Ash.Query.set_tenant(opts[:tenant] || query.tenant)
|> Ash.Query.set_context(%{
private: %{actor: actor, pre_flight_authorization?: pre_flight?}
})
%Ash.Changeset{} = changeset ->
changeset
|> Ash.Changeset.set_tenant(opts[:tenant] || changeset.tenant)
|> Ash.Changeset.set_context(%{
private: %{actor: actor, pre_flight_authorization?: pre_flight?}
})
%{type: :update, name: name} ->
if opts[:data] do
Ash.Changeset.for_update(opts[:data], name, input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
else
resource
|> struct()
|> Ash.Changeset.for_update(name, input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
end
%{type: :create, name: name} ->
Ash.Changeset.for_create(resource, name, input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
%{type: :read, name: name} ->
Ash.Query.for_read(resource, name, input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
%{type: :destroy, name: name} ->
if opts[:data] do
Ash.Changeset.for_destroy(opts[:data], name, input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
else
resource
|> struct()
|> Ash.Changeset.for_destroy(name, input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
end
%{type: :action, name: name} ->
Ash.ActionInput.for_action(resource, name, input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
_ ->
raise ArgumentError,
message: "Invalid action/query/changeset \"#{inspect(action_or_query_or_changeset)}\""
end
if opts[:validate?] && !subject.valid? do
{:ok, false, Ash.Error.to_error_class(subject.errors)}
else
subject = %{subject | domain: domain}
reuse_values? = Keyword.get(opts, :reuse_values?, false)
opts =
if pre_flight? && !reuse_values? && opts[:data] do
fields = [:__metadata__ | Enum.to_list(Ash.Resource.Info.attribute_names(resource))]
Keyword.update!(opts, :data, fn data ->
data
|> List.wrap()
|> Enum.map(fn record ->
struct(resource, Map.take(record, fields))
end)
end)
else
opts
end
subject =
case subject do
%Ash.Query{} ->
subject
%Ash.Changeset{} = changeset ->
if pre_flight? && !reuse_values? && is_struct(changeset.data, resource) do
fields = [:__metadata__ | Enum.to_list(Ash.Resource.Info.attribute_names(resource))]
%{changeset | data: struct(resource, Map.take(changeset.data, fields))}
else
changeset
end
%Ash.ActionInput{} ->
subject
end
|> Ash.Subject.set_context(%{
private: %{
authorizer_log?: opts[:log?] || false,
log_policy_breakdown?: opts[:log_policy_breakdown?]
}
})
case Ash.Domain.Info.resource(domain, resource) do
{:ok, _} ->
domain
|> run_check(actor, subject, opts)
|> alter_source(domain, actor, subject, opts)
{:error, error} ->
{:error, error}
end
end
end
defp resource_subject_input(action_or_query_or_changeset, domain, actor, opts) do
case action_or_query_or_changeset do
%Ash.Query{} = query ->
{query.resource, query, nil}
%Ash.Changeset{} = changeset ->
{changeset.resource, changeset, nil}
%Ash.ActionInput{} = input ->
{input.resource, input, nil}
{resource, name} when is_atom(name) and is_atom(resource) ->
action =
Ash.Resource.Info.action(resource, name) ||
raise ArgumentError, "No such action #{name} on #{inspect(resource)}"
resource_subject_input(
{resource, action, %{}},
domain,
actor,
opts
)
{resource, name, input} when is_atom(name) and is_atom(resource) and not is_nil(name) ->
action =
Ash.Resource.Info.action(resource, name) ||
raise ArgumentError, "No such action #{name} on #{inspect(resource)}"
resource_subject_input(
{resource, action, input},
domain,
actor,
opts
)
{%Ash.Query{} = query, name} ->
query
|> Ash.Query.for_read(name, %{},
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
|> resource_subject_input(domain, actor, opts)
{%Ash.Changeset{} = changeset, name} ->
changeset
|> Ash.Changeset.for_action(name, %{},
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
|> resource_subject_input(domain, actor, opts)
{%Ash.ActionInput{} = input, name} ->
input
|> Ash.ActionInput.for_action(name, %{},
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
|> resource_subject_input(domain, actor, opts)
{%Ash.Query{} = query, name, input} ->
query
|> Ash.Query.for_read(name, input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
|> resource_subject_input(domain, actor, opts)
{%Ash.Changeset{} = changeset, name, input} ->
changeset
|> Ash.Changeset.for_action(name, input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
|> resource_subject_input(domain, actor, opts)
{%Ash.ActionInput{} = input, name, action_input} ->
input
|> Ash.ActionInput.for_action(name, action_input,
actor: actor,
tenant: opts[:tenant],
context: opts[:context]
)
|> resource_subject_input(domain, actor, opts)
{%resource{} = record, name}
when is_atom(name) and is_atom(resource) and not is_nil(name) ->
action =
Ash.Resource.Info.action(resource, name) ||
raise ArgumentError, "No such action #{name} on #{inspect(resource)}"
resource_subject_input(
{record, action, %{}},
domain,
actor,
opts
)
{%resource{} = record, name, input}
when is_atom(name) and is_atom(resource) and not is_nil(name) ->
action =
Ash.Resource.Info.action(resource, name) ||
raise ArgumentError, "No such action #{name} on #{inspect(resource)}"
resource_subject_input(
{record, action, input},
domain,
actor,
opts
)
{resource, %struct{} = action}
when struct in [
Ash.Resource.Actions.Create,
Ash.Resource.Actions.Read,
Ash.Resource.Actions.Update,
Ash.Resource.Actions.Destroy,
Ash.Resource.Actions.Action
] ->
resource_subject_input({resource, action, %{}}, domain, actor, opts)
{%resource{} = record, %Ash.Resource.Actions.Read{} = action, input} ->
{resource,
Ash.Query.for_read(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
context: opts[:context],
actor: actor
), input, data: [record]}
{%resource{}, %Ash.Resource.Actions.Action{} = action, input} ->
{resource,
Ash.ActionInput.for_action(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
context: opts[:context],
actor: actor
), input}
{%resource{}, %Ash.Resource.Actions.Create{} = action, input} ->
{resource,
Ash.Changeset.for_create(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
context: opts[:context],
actor: actor
), input}
{%resource{} = record, %struct{} = action, input}
when struct in [
Ash.Resource.Actions.Update,
Ash.Resource.Actions.Destroy
] ->
{resource,
Ash.Changeset.for_action(record, action.name, input,
domain: domain,
tenant: opts[:tenant],
context: opts[:context],
actor: actor
), input}
{resource, %Ash.Resource.Actions.Read{} = action, input} ->
{resource,
Ash.Query.for_read(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
context: opts[:context],
actor: actor
), input}
{resource, %Ash.Resource.Actions.Action{} = action, input} ->
{resource,
Ash.ActionInput.for_action(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
context: opts[:context],
actor: actor
), input}
{resource, %Ash.Resource.Actions.Create{} = action, input} ->
{resource,
Ash.Changeset.for_create(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
context: opts[:context],
actor: actor
), input}
{resource, %struct{} = action, input}
when struct in [
Ash.Resource.Actions.Update,
Ash.Resource.Actions.Destroy
] ->
{resource, action, input}
{resource, action} ->
raise ArgumentError, """
If providing an update or destroy action, you must provide a record to update or destroy.
Got: #{inspect({resource, action})}
"""
end
end
defp alter_source({:ok, true, query}, domain, actor, %Ash.Changeset{} = subject, opts) do
pending = get_in(query.context || %{}, [:private, :create_authorize_results_pending])
if pending && subject.action_type == :create do
subject = install_create_authorize_results(subject, pending)
case alter_source({:ok, true}, domain, actor, subject, opts) do
{:ok, true, new_subject} -> {:ok, true, new_subject}
other -> other
end
else
case alter_source(
{:ok, true},
domain,
actor,
subject,
Keyword.put(opts, :base_query, query)
) do
{:ok, true, new_subject} -> {:ok, true, new_subject, query}
other -> other
end
end
end
defp alter_source({:ok, true, query}, domain, actor, _subject, opts) do
alter_source({:ok, true}, domain, actor, query, opts)
end
defp alter_source({:ok, true}, domain, actor, subject, opts) do
if opts[:alter_source?] do
subject.resource
|> Ash.Resource.Info.authorizers()
|> case do
[] ->
{:ok, true, subject}
authorizers ->
authorizers
|> Enum.reduce(
{:ok, true, subject},
fn authorizer, {:ok, true, subject} ->
authorizer_state =
Ash.Authorizer.initial_state(
authorizer,
actor,
subject.resource,
subject.action,
domain
)
context = %{
actor: actor,
tenant: subject.to_tenant,
domain: domain,
resource: subject.resource,
query: nil,
changeset: nil,
action_input: nil,
subject: subject
}
context =
case subject do
%Ash.Query{} -> Map.put(context, :query, subject)
%Ash.Changeset{} -> Map.put(context, :changeset, subject)
%Ash.ActionInput{} -> Map.put(context, :action_input, subject)
end
case subject do
%Ash.Query{} = query ->
alter_query(query, authorizer, authorizer_state, context, opts)
%Ash.Changeset{} = changeset ->
context = Map.put(context, :changeset, changeset)
with {:ok, changeset, authorizer_state} <-
Ash.Authorizer.add_calculations(
authorizer,
changeset,
authorizer_state,
context
) do
if opts[:base_query] do
case alter_query(
opts[:base_query],
authorizer,
authorizer_state,
context,
opts
) do
{:ok, true, query} ->
{:ok, true, changeset, query}
other ->
other
end
else
{:ok, true, changeset}
end
end
%Ash.ActionInput{} = subject ->
{:ok, true, subject}
end
end
)
end
else
{:ok, true}
end
end
defp alter_source(other, _, _, _, _), do: other
defp alter_query(query, authorizer, authorizer_state, context, opts) do
context = Map.put(context, :query, query)
with {:ok, query, _} <-
Ash.Authorizer.add_calculations(
authorizer,
query,
authorizer_state,
context
),
{:ok, new_filter} <-
Ash.Authorizer.alter_filter(
authorizer,
authorizer_state,
query.filter,
context
),
{:ok, hydrated} <-
Ash.Filter.hydrate_refs(new_filter, %{
resource: query.resource,
public?: false
}),
hydrated <- fill_template(hydrated, context, opts),
{:ok, new_sort} <-
Ash.Authorizer.alter_sort(
authorizer,
authorizer_state,
query.sort,
context
) do
{:ok, true, %{query | filter: hydrated, sort: new_sort}}
end
end
defp fill_template(expr, context, opts) do
{:ok, expr} =
Ash.Filter.hydrate_refs(expr, %{
resource: context.resource,
public?: false
})
if opts[:atomic_changeset] do
Ash.Expr.fill_template(
expr,
actor: context.actor,
tenant: opts[:atomic_changeset].to_tenant,
args: opts[:atomic_changeset].arguments,
context: opts[:atomic_changeset].context,
changeset: opts[:atomic_changeset]
)
else
expr
end
end
defp run_check(domain, actor, subject, opts) do
authorizers =
Ash.Resource.Info.authorizers(subject.resource)
|> Enum.map(fn authorizer ->
authorizer_state =
Ash.Authorizer.initial_state(
authorizer,
actor,
subject.resource,
subject.action,
domain
)
context = %{
actor: actor,
tenant: subject.to_tenant,
domain: domain,
resource: subject.resource,
query: nil,
changeset: nil,
action_input: nil,
subject: subject
}
context =
case subject do
%Ash.Query{} -> Map.put(context, :query, subject)
%Ash.Changeset{} -> Map.put(context, :changeset, subject)
%Ash.ActionInput{} -> Map.put(context, :action_input, subject)
end
{authorizer, authorizer_state, context}
end)
base_query =
case subject do
%Ash.Query{} = query ->
opts[:base_query] || query
_ ->
opts[:base_query]
end
case authorizers do
[] ->
if opts[:log?] do
Logger.info("No authorizers present on #{inspect(subject.resource)}")
end
{:ok, true}
authorizers ->
authorizers
|> Enum.reduce_while(
{false, base_query, []},
fn {authorizer, authorizer_state, context}, {_authorized?, query, authorizers} ->
case Ash.Authorizer.strict_check(authorizer, authorizer_state, context) do
{:error, %{class: :forbidden} = e} when is_exception(e) ->
{:halt, {false, e, {authorizer, authorizer_state, context}}}
{:error, error} ->
{:halt, {:error, authorizer, error}}
{:authorized, authorizer_state} ->
{:cont, {true, query, [{authorizer, authorizer_state, context} | authorizers]}}
_ when not is_nil(context.action_input) ->
raise """
Cannot use filter or runtime checks with generic actions
Failed when authorizing #{inspect(subject.resource)}.#{subject.action.name}
"""
{:filter, authorizer_state, filter} ->
filter = fill_template(filter, context, opts)
{:cont,
{true,
apply_filter(
query,
subject,
domain,
filter,
authorizer,
authorizer_state,
context,
opts
), [{authorizer, authorizer_state, context} | authorizers]}}
{:filter, filter} ->
filter = fill_template(filter, context, opts)
{:cont,
{true,
apply_filter(
query,
subject,
domain,
filter,
authorizer,
authorizer_state,
context,
opts
), [{authorizer, authorizer_state, context} | authorizers]}}
{:continue, authorizer_state} ->
cond do
match?(%Ash.Changeset{action_type: :create}, subject) ->
{:cont,
{true,
stash_create_pending(
query,
subject,
domain,
authorizer,
authorizer_state,
context
), [{authorizer, authorizer_state, context} | authorizers]}}
opts[:no_check?] ->
{:halt,
opts[:on_must_pass_strict_check] ||
{:error, {authorizer, authorizer_state, context},
Ash.Authorizer.exception(
authorizer,
:must_pass_strict_check,
authorizer_state
)}}
opts[:alter_source?] || !match?(%Ash.Query{}, subject) ->
query_with_hook =
Ash.Query.authorize_results(
or_query(query, subject.resource, domain, subject),
fn query, results ->
context = Map.merge(context, %{data: results, query: query})
case Ash.Authorizer.check(authorizer, authorizer_state, context) do
:authorized ->
{:ok, results}
{:error, :forbidden, authorizer_state} ->
{:error,
Ash.Authorizer.exception(authorizer, :forbidden, authorizer_state)}
{:error, error} ->
{:error, error}
{:data, data} ->
{:ok, data}
end
end
)
{:cont,
{true, query_with_hook,
[{authorizer, authorizer_state, context} | authorizers]}}
opts[:maybe_is] == false ->
{:halt,
{false, Ash.Authorizer.exception(authorizer, :forbidden, authorizer_state),
{authorizer, authorizer_state, context}}}
true ->
{:halt,
{:maybe, nil, [{authorizer, authorizer_state, context} | authorizers]}}
end
{:filter_and_continue, filter, authorizer_state} ->
filter = fill_template(filter, context, opts)
cond do
match?(%Ash.Changeset{action_type: :create}, subject) ->
{:cont,
{true,
apply_filter(
query,
subject,
domain,
filter,
authorizer,
authorizer_state,
context,
Keyword.put(opts, :create_filter_kind, :filter_and_continue)
), [{authorizer, authorizer_state, context} | authorizers]}}
opts[:no_check?] || !match?(%Ash.Query{}, subject) ->
{:error, {authorizer, authorizer_state, context},
Ash.Authorizer.exception(
authorizer,
:must_pass_strict_check,
authorizer_state
)}
opts[:alter_source?] ->
query_with_hook =
query
|> apply_filter(
subject,
domain,
filter,
authorizer,
authorizer_state,
context,
opts
)
|> Ash.Query.authorize_results(fn query, results ->
context = Map.merge(context, %{data: results, query: query})
case Ash.Authorizer.check(authorizer, authorizer_state, context) do
:authorized ->
{:ok, results}
{:error, :forbidden, authorizer_state} ->
{:error,
Ash.Authorizer.exception(authorizer, :forbidden, authorizer_state)}
{:error, error} ->
{:error, error}
{:data, data} ->
{:ok, data}
end
end)
{:cont,
{true, query_with_hook,
[{authorizer, authorizer_state, context} | authorizers]}}
opts[:maybe_is] == false ->
{:halt,
{false, Ash.Authorizer.exception(authorizer, :forbidden, authorizer_state),
authorizer}}
true ->
{:halt,
{:maybe, nil, [{authorizer, authorizer_state, context} | authorizers]}}
end
end
end
)
|> case do
{:error, _authorizer, error} ->
{:error, error}
{true, nil, _} ->
{:ok, true}
{true, query, authorizers} when not is_nil(query) ->
if opts[:run_queries?] do
run_queries(subject, actor, opts, authorizers, query)
else
if opts[:alter_source?] do
{:ok, true, query}
else
{:ok, :maybe}
end
end
{false, error, authorizer} ->
if opts[:return_forbidden_error?] do
{:ok, false, error || authorizer_exception([authorizer])}
else
{:ok, false}
end
{:maybe, _v, authorizers} ->
if opts[:maybe_is] == false && opts[:return_forbidden_error?] do
{:ok, false, authorizer_exception(authorizers)}
else
{:ok, opts[:maybe_is]}
end
end
end
end
defp apply_filter(query, subject, domain, filter, authorizer, authorizer_state, context, opts) do
resource = subject.resource
filter = Ash.Filter.parse!(resource, filter).expression
case subject do
%Ash.Changeset{action_type: :create} ->
kind = opts[:create_filter_kind] || :filter
stash_create_pending(
query,
subject,
domain,
authorizer,
authorizer_state,
context,
{kind, filter}
)
_ ->
case opts[:filter_with] || :filter do
:filter ->
Ash.Query.filter(or_query(query, resource, domain, subject), ^filter)
:error ->
Ash.Query.filter(
or_query(query, resource, domain, subject),
if ^filter do
true
else
error(Ash.Error.Forbidden.Placeholder, %{
authorizer: ^inspect(authorizer)
})
end
)
|> Ash.Query.set_context(%{
private: %{authorizer_state: %{authorizer => authorizer_state}}
})
end
end
end
defp stash_create_pending(
query,
subject,
domain,
authorizer,
authorizer_state,
context,
disposition \\ {:continue, nil}
) do
query = or_query(query, subject.resource, domain, subject)
existing = get_in(query.context || %{}, [:private, :create_authorize_results_pending]) || []
Ash.Query.set_context(query, %{
private: %{
create_authorize_results_pending: [
{authorizer, authorizer_state, context, disposition} | existing
]
}
})
end
defp run_queries(subject, actor, opts, authorizers, query) do
case subject do
%Ash.Query{tenant: tenant} ->
if opts[:data] do
data = List.wrap(opts[:data])
pkey = Ash.Resource.Info.primary_key(query.resource)
if Enum.any?(data, fn record ->
pkey |> Enum.map(&Map.get(record, &1)) |> Enum.any?(&is_nil/1)
end) do
{:ok, :maybe}
else
query
|> Ash.Query.do_filter(Ash.pkey_filter(data, pkey))
|> Ash.Query.select([])
|> Ash.Query.set_tenant(tenant)
|> Ash.Actions.Read.add_calc_context_to_query(
actor,
true,
query.tenant,
opts[:tracer],
query.domain,
expand?: false,
parent_stack: Ash.Actions.Read.parent_stack_from_context(subject.context),
source_context: subject.context
)
|> Ash.Query.data_layer_query()
|> case do
{:ok, data_layer_query} ->
data_layer_query
|> Ash.DataLayer.run_query(query.resource)
|> Ash.Actions.Helpers.rollback_if_in_transaction(query.resource, query)
|> case do
{:ok, results} ->
case Ash.Actions.Read.run_authorize_results(query, results) do
{:ok, results} ->
if Enum.count(results) == Enum.count(data) do
{:ok, true}
else
if opts[:return_forbidden_error?] do
{:ok, false, authorizer_exception(authorizers)}
else
{:ok, false}
end
end
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
end
else
{:ok, true}
end
%Ash.Changeset{data: data, action_type: type, resource: resource, tenant: tenant} =
changeset
when type in [:update, :destroy] ->
pkey = Ash.Resource.Info.primary_key(resource)
pkey_value = Map.take(data, pkey)
query =
Map.update!(query, :filter, fn filter ->
Ash.Expr.fill_template(
filter,
actor: actor,
actor: changeset.to_tenant,
args: changeset.arguments,
context: changeset.context,
changeset: changeset
)
end)
if pkey_value |> Map.values() |> Enum.any?(&is_nil/1) do
{:ok, :maybe}
else
query
|> Ash.Query.do_filter(pkey_value)
|> Ash.Query.set_tenant(tenant)
|> Ash.Query.select([])
|> Ash.Actions.Read.add_calc_context_to_query(
actor,
true,
query.tenant,
opts[:tracer],
query.domain,
expand?: false,
parent_stack: Ash.Actions.Read.parent_stack_from_context(subject.context),
source_context: subject.context
)
|> Ash.Query.data_layer_query()
|> case do
{:ok, data_layer_query} ->
data_layer_query
|> Ash.DataLayer.run_query(resource)
|> Ash.Actions.Helpers.rollback_if_in_transaction(query.resource, query)
|> case do
{:ok, results} ->
case Ash.Actions.Read.run_authorize_results(query, results) do
{:ok, []} ->
if opts[:return_forbidden_error?] do
{:ok, false, authorizer_exception(authorizers)}
else
{:ok, false}
end
{:ok, [_]} ->
{:ok, true}
{:error, error} ->
if opts[:return_forbidden_error?] do
{:ok, false, error}
else
{:ok, false}
end
end
{:error, error} ->
{:error, error}
_ ->
if opts[:return_forbidden_error?] do
{:ok, false, authorizer_exception(authorizers)}
else
{:ok, false}
end
end
{:error, error} ->
{:error, error}
end
end
%Ash.Changeset{action_type: :create} = changeset ->
case get_in(query.context, [:private, :create_authorize_results_pending]) do
nil ->
raise Ash.Error.Forbidden.CannotFilterCreates,
filter: query.filter,
resource: changeset.resource,
action: changeset.action && changeset.action.name
_pending ->
{:ok, true, query}
end
%Ash.Changeset{} = changeset ->
raise Ash.Error.Forbidden.CannotFilterCreates,
filter: query.filter,
resource: changeset.resource,
action: changeset.action && changeset.action.name
end
end
defp install_create_authorize_results(changeset, pending) do
cond do
!Ash.DataLayer.data_layer_can?(changeset.resource, :transact) ->
raise Ash.Error.Forbidden.CannotFilterCreates,
filter: nil,
resource: changeset.resource,
action: changeset.action && changeset.action.name
changeset.action && changeset.action.transaction? == false ->
raise Ash.Error.Forbidden.CannotFilterCreates,
filter: nil,
resource: changeset.resource,
action: changeset.action && changeset.action.name
(changeset.before_transaction != [] or changeset.around_transaction != []) and
not (changeset.action && changeset.action.allow_post_action_authorization?) ->
raise Ash.Error.Forbidden.CannotFilterCreates,
filter: nil,
resource: changeset.resource,
action: changeset.action && changeset.action.name,
reason: :non_transactional_hooks
true ->
:ok
end
Enum.reduce(pending, changeset, fn entry, cs ->
Ash.Changeset.authorize_results(cs, fn cs, [record] ->
# Runtime guard: even though the install-time guard verified the data
# layer supports transactions and the action's `transaction?` is true,
# bulk creates accept a separate `transaction: false` option that can
# bypass that wrapping. If we're not actually inside a transaction
# when the hook fires, the post-insert authorization can't roll back
# the row — fail loudly rather than silently authorize.
if Ash.DataLayer.in_transaction?(cs.resource) do
evaluate_create_pending(entry, cs, record)
else
raise Ash.Error.Forbidden.CannotFilterCreates,
filter: nil,
resource: cs.resource,
action: cs.action && cs.action.name
end
end)
end)
end
defp evaluate_create_pending(
{authorizer, authorizer_state, context, {kind, filter}},
changeset,
record
) do
case kind do
:filter ->
# All scenarios were filterable — the combined filter is both
# necessary and sufficient. One SELECT covers every filter check.
case filter_matches?(changeset, record, filter) do
{:ok, true} -> {:ok, [record]}
{:ok, false} -> forbidden(authorizer, authorizer_state)
{:error, error} -> {:error, error}
end
:filter_and_continue ->
# The filter is globally required but not sufficient on its own.
# One SELECT short-circuits the forbidden case; if the row matches,
# fall through to check/3 to evaluate the remaining scenarios.
case filter_matches?(changeset, record, filter) do
{:ok, false} ->
forbidden(authorizer, authorizer_state)
{:ok, true} ->
delegate_to_authorizer_check(authorizer, authorizer_state, context, changeset, record)
{:error, error} ->
{:error, error}
end
:continue ->
# No pre-flight filter — every check is :unknown / runtime. Delegate
# straight to the authorizer's check/3 callback, which will run
# check_fact for each unresolved scenario.
delegate_to_authorizer_check(authorizer, authorizer_state, context, changeset, record)
end
end
defp filter_matches?(_changeset, _record, nil), do: {:ok, true}
defp filter_matches?(_changeset, _record, true), do: {:ok, true}
defp filter_matches?(changeset, record, filter) do
resource = changeset.resource
pkey = Ash.Resource.Info.primary_key(resource)
pkey_filter = record |> Map.take(pkey) |> Map.to_list()
resource
|> Ash.Query.do_filter(pkey_filter)
|> Ash.Query.filter(^filter)
|> Ash.Query.set_tenant(changeset.tenant)
|> Ash.Query.set_context(%{private: %{internal?: true}})
|> Ash.Query.data_layer_query()
|> case do
{:ok, data_layer_query} ->
case Ash.DataLayer.run_query(data_layer_query, resource) do
{:ok, [_ | _]} -> {:ok, true}
{:ok, []} -> {:ok, false}
{:error, error} -> {:error, error}
end
{:error, error} ->
{:error, error}
end
end
defp delegate_to_authorizer_check(authorizer, authorizer_state, context, changeset, record) do
context = Map.merge(context, %{data: [record], changeset: changeset})
case Ash.Authorizer.check(authorizer, authorizer_state, context) do
:authorized ->
{:ok, [record]}
{:data, [_ | _]} ->
{:ok, [record]}
{:data, []} ->
forbidden(authorizer, authorizer_state)
{:error, :forbidden, state} ->
{:error, Ash.Authorizer.exception(authorizer, :forbidden, state)}
{:error, error} ->
{:error, error}
end
end
defp forbidden(authorizer, authorizer_state) do
{:error, Ash.Authorizer.exception(authorizer, :forbidden, authorizer_state)}
end
defp or_query(query, resource, domain, subject) do
query || Ash.Query.set_context(Ash.Query.new(resource, domain: domain), subject.context)
end
defp authorizer_exception([{authorizer, authorizer_state, _context}]) do
Ash.Authorizer.exception(authorizer, :forbidden, authorizer_state)
end
defp authorizer_exception(authorizers) do
authorizers
|> Enum.map(&authorizer_exception([&1]))
|> Ash.Error.to_error_class()
end
end