Packages

ash

3.7.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.

Retired package: Security issue - CVE-2025-48044
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(), atom | Ash.Resource.Actions.action()}
| {Ash.Resource.record(), 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(), 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: `is_maybe` is set to `true`, 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)
{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}
end
{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}})
%Ash.Query{} = query ->
query
|> Ash.Query.set_tenant(opts[:tenant] || query.tenant)
|> Ash.Query.set_context(%{private: %{actor: actor}})
%Ash.Changeset{} = changeset ->
changeset
|> Ash.Changeset.set_tenant(opts[:tenant] || changeset.tenant)
|> Ash.Changeset.set_context(%{private: %{actor: actor}})
%{type: :update, name: name} ->
if opts[:data] do
Ash.Changeset.for_update(opts[:data], name, input,
actor: actor,
tenant: opts[:tenant]
)
else
resource
|> struct()
|> Ash.Changeset.for_update(name, input, actor: actor, tenant: opts[:tenant])
end
%{type: :create, name: name} ->
Ash.Changeset.for_create(resource, name, input, actor: actor, tenant: opts[:tenant])
%{type: :read, name: name} ->
Ash.Query.for_read(resource, name, input, actor: actor, tenant: opts[:tenant])
%{type: :destroy, name: name} ->
if opts[:data] do
Ash.Changeset.for_destroy(opts[:data], name, input,
actor: actor,
tenant: opts[:tenant]
)
else
resource
|> struct()
|> Ash.Changeset.for_destroy(name, input, actor: actor, tenant: opts[:tenant])
end
%{type: :action, name: name} ->
Ash.ActionInput.for_action(resource, name, input, actor: actor, tenant: opts[:tenant])
_ ->
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}
pre_flight? = Keyword.get(opts, :pre_flight?, true)
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{} ->
Ash.Query.set_context(subject, %{private: %{pre_flight_authorization?: pre_flight?}})
%Ash.Changeset{} ->
changeset =
Ash.Changeset.set_context(subject, %{
private: %{pre_flight_authorization?: pre_flight?}
})
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{} ->
Ash.ActionInput.set_context(subject, %{
private: %{pre_flight_authorization?: pre_flight?}
})
end
|> Ash.Subject.set_context(%{
private: %{authorizer_log?: opts[:log?] || false}
})
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, %{})
|> resource_subject_input(domain, actor, opts)
{%Ash.Changeset{} = changeset, name} ->
changeset
|> Ash.Changeset.for_action(name, %{})
|> resource_subject_input(domain, actor, opts)
{%Ash.ActionInput{} = input, name} ->
input
|> Ash.ActionInput.for_action(name, %{})
|> resource_subject_input(domain, actor, opts)
{%Ash.Query{} = query, name, input} ->
query
|> Ash.Query.for_read(name, input)
|> resource_subject_input(domain, actor, opts)
{%Ash.Changeset{} = changeset, name, input} ->
changeset
|> Ash.Changeset.for_action(name, input)
|> resource_subject_input(domain, actor, opts)
{%Ash.ActionInput{} = input, name, action_input} ->
input
|> Ash.ActionInput.for_action(name, action_input)
|> 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],
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],
actor: actor
), input}
{%resource{}, %Ash.Resource.Actions.Create{} = action, input} ->
{resource,
Ash.Changeset.for_create(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
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],
actor: actor
), input}
{resource, %Ash.Resource.Actions.Read{} = action, input} ->
{resource,
Ash.Query.for_read(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
actor: actor
), input}
{resource, %Ash.Resource.Actions.Action{} = action, input} ->
{resource,
Ash.ActionInput.for_action(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
actor: actor
), input}
{resource, %Ash.Resource.Actions.Create{} = action, input} ->
{resource,
Ash.Changeset.for_create(resource, action.name, input,
domain: domain,
tenant: opts[:tenant],
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
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
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 =
authorizer.initial_state(
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 =
authorizer.initial_state(
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 authorizer.strict_check(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]}}
:forbidden ->
{:halt,
{false, Ash.Authorizer.exception(authorizer, :forbidden, authorizer_state),
{authorizer, authorizer_state, context}}}
_ 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,
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,
opts
), [{authorizer, authorizer_state, context} | authorizers]}}
{:continue, authorizer_state} ->
if opts[:no_check?] do
{:halt,
opts[:on_must_pass_strict_check] ||
{:error, {authorizer, authorizer_state, context},
Ash.Authorizer.exception(
authorizer,
:must_pass_strict_check,
authorizer_state
)}}
else
if opts[:alter_source?] || !match?(%Ash.Query{}, subject) do
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 authorizer.check(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]}}
else
if opts[:maybe_is] == false do
{:halt,
{false, Ash.Authorizer.exception(authorizer, :forbidden, authorizer_state),
{authorizer, authorizer_state, context}}}
else
{:halt,
{:maybe, nil, [{authorizer, authorizer_state, context} | authorizers]}}
end
end
end
{:filter_and_continue, filter, authorizer_state} ->
filter = fill_template(filter, context, opts)
if opts[:no_check?] || !match?(%Ash.Query{}, subject) do
{:error, {authorizer, authorizer_state, context},
Ash.Authorizer.exception(
authorizer,
:must_pass_strict_check,
authorizer_state
)}
else
if opts[:alter_source?] do
query_with_hook =
query
|> apply_filter(
subject,
domain,
filter,
authorizer,
authorizer_state,
opts
)
|> Ash.Query.authorize_results(fn query, results ->
context = Map.merge(context, %{data: results, query: query})
case authorizer.check(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]}}
else
if opts[:maybe_is] == false do
{:halt,
{false, Ash.Authorizer.exception(authorizer, :forbidden, authorizer_state),
authorizer}}
else
{:halt,
{:maybe, nil, [{authorizer, authorizer_state, context} | authorizers]}}
end
end
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, opts) do
resource = subject.resource
filter = Ash.Filter.parse!(resource, filter).expression
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
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)
pkey_values = Enum.map(data, &Map.take(&1, pkey))
if Enum.any?(pkey_values, fn pkey_value ->
pkey_value |> Map.values() |> Enum.any?(&is_nil/1)
end) do
{:ok, :maybe}
else
query
|> Ash.Query.do_filter(or: pkey_values)
|> 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{} ->
raise Ash.Error.Forbidden.CannotFilterCreates, filter: query.filter
end
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