improvement: support authorize? as a changeset option

This commit is contained in:
Zach Daniel 2022-08-09 20:25:43 -04:00
parent a4770e35fd
commit b9675295e6
13 changed files with 291 additions and 182 deletions

View file

@ -14,13 +14,14 @@ defmodule Ash do
def get_context_for_transfer do
context = Ash.get_context()
actor = Process.get(:ash_actor)
tenant = Process.get(:tenant)
authorize? = Process.get(:ash_authorize?)
tenant = Process.get(:ash_tenant)
%{context: context, actor: actor, tenant: tenant}
%{context: context, actor: actor, tenant: tenant, authorize?: authorize?}
end
@spec transfer_context(term) :: :ok
def transfer_context(%{context: context, actor: actor, tenant: tenant}) do
def transfer_context(%{context: context, actor: actor, tenant: tenant, authorize?: authorize?}) do
case actor do
{:actor, actor} ->
Ash.set_actor(actor)
@ -37,6 +38,14 @@ defmodule Ash do
:ok
end
case authorize? do
{:authorize?, authorize?} ->
Ash.set_authorize?(authorize?)
_ ->
:ok
end
Ash.set_context(context)
end
@ -60,6 +69,16 @@ defmodule Ash do
:ok
end
@doc """
Sets authorize? into the process dictionary that is used for all changesets and queries.
"""
@spec set_authorize?(map) :: :ok
def set_authorize?(map) do
Process.put(:ash_authorize?, {:authorize?, map})
:ok
end
@doc """
Gets the current actor from the process dictionary
"""
@ -74,6 +93,20 @@ defmodule Ash do
end
end
@doc """
Gets the current authorize? from the process dictionary
"""
@spec get_authorize?() :: term()
def get_authorize? do
case Process.get(:ash_authorize?) do
{:authorize?, value} ->
value
_ ->
nil
end
end
@doc """
Sets tenant into the process dictionary that is used for all changesets and queries.
"""

View file

@ -13,15 +13,6 @@ defmodule Ash.Actions.Create do
def run(api, changeset, action, opts) do
{changeset, opts} = Ash.Actions.Helpers.add_process_context(api, changeset, opts)
opts =
case Map.fetch(changeset.context[:private] || %{}, :actor) do
{:ok, actor} ->
Keyword.put_new(opts, :actor, actor)
_ ->
opts
end
upsert? = opts[:upsert?] || get_in(changeset.context, [:private, :upsert?]) || false
authorize? = authorize?(opts)
upsert_keys = opts[:upsert_keys]
@ -142,7 +133,8 @@ defmodule Ash.Actions.Create do
resource: resource,
error_path: error_path,
changeset:
Request.resolve(changeset_dependencies, fn %{actor: actor} = context ->
Request.resolve(changeset_dependencies, fn %{actor: actor, authorize?: authorize?} =
context ->
input = changeset_input.(context) || %{}
tenant =
@ -163,11 +155,13 @@ defmodule Ash.Actions.Create do
resource
|> Ash.Changeset.for_create(action.name, input,
actor: actor,
authorize?: authorize?,
tenant: tenant,
timeout: timeout
)
|> changeset(api, action,
actor: actor,
authorize?: authorize?,
tenant: tenant,
timeout: timeout
)
@ -175,6 +169,7 @@ defmodule Ash.Actions.Create do
changeset ->
changeset(changeset, api, action,
actor: actor,
authorize?: authorize?,
tenant: tenant,
timeout: timeout
)
@ -263,7 +258,6 @@ defmodule Ash.Actions.Create do
result =
changeset
|> Ash.Changeset.put_context(:private, %{actor: actor})
|> Ash.Changeset.before_action(
&Ash.Actions.ManagedRelationships.setup_managed_belongs_to_relationships(
&1,

View file

@ -27,15 +27,6 @@ defmodule Ash.Actions.Destroy do
def run(api, %{resource: resource} = changeset, action, opts) do
{changeset, opts} = Ash.Actions.Helpers.add_process_context(api, changeset, opts)
opts =
case Map.fetch(changeset.context[:private] || %{}, :actor) do
{:ok, actor} ->
Keyword.put_new(opts, :actor, actor)
_ ->
opts
end
authorize? = authorize?(opts)
actor = opts[:actor]
verbose? = opts[:verbose?]
@ -124,7 +115,8 @@ defmodule Ash.Actions.Destroy do
action: action,
error_path: error_path,
changeset:
Request.resolve(changeset_dependencies, fn %{actor: actor} = context ->
Request.resolve(changeset_dependencies, fn %{actor: actor, authorize?: authorize?} =
context ->
input = changeset_input.(context) || %{}
tenant =
@ -155,11 +147,13 @@ defmodule Ash.Actions.Destroy do
|> Ash.Changeset.for_destroy(action.name, input,
actor: actor,
tenant: tenant,
authorize?: authorize?,
timeout: timeout
)
|> changeset(api, action,
actor: actor,
tenant: tenant,
authorize?: authorize?,
timeout: timeout
)
end
@ -167,6 +161,7 @@ defmodule Ash.Actions.Destroy do
changeset ->
changeset(changeset, api, action,
actor: actor,
authorize?: authorize?,
tenant: tenant,
timeout: timeout
)
@ -223,12 +218,12 @@ defmodule Ash.Actions.Destroy do
data:
Request.resolve(
[path ++ [:data, :data], path ++ [:destroy, :changeset]],
fn %{actor: actor} = context ->
fn %{actor: actor, authorize?: authorize?} = context ->
changeset = get_in(context, path ++ [:destroy, :changeset])
record = changeset.data
changeset
|> Ash.Changeset.put_context(:private, %{actor: actor})
|> Ash.Changeset.put_context(:private, %{actor: actor, authorize?: authorize?})
|> Ash.Changeset.with_hooks(fn changeset ->
if action.manual? do
{:ok, record}

View file

@ -24,25 +24,51 @@ defmodule Ash.Actions.Helpers do
private: %{
actor: actor
}
} ->
}
when not is_nil(actor) ->
Keyword.put_new(opts, :actor, actor)
_ ->
opts
end
{add_context(query_or_changeset), opts |> add_actor(api) |> add_tenant()}
opts =
case query_or_changeset.context do
%{
private: %{
authorize?: authorize?
}
}
when is_boolean(authorize?) ->
Keyword.put_new(opts, :authorize?, authorize?)
_ ->
opts
end
opts = opts |> add_actor(api) |> add_authorize?(api) |> add_tenant()
query_or_changeset = add_context(query_or_changeset, opts)
{query_or_changeset, opts}
end
defp add_context(query_or_changeset) do
defp add_context(query_or_changeset, opts) do
context = Process.get(:ash_context, %{}) || %{}
private_context = Map.new(Keyword.take(opts, [:actor, :authorize?]))
case query_or_changeset do
%Ash.Query{} ->
Ash.Query.set_context(query_or_changeset, context)
query_or_changeset
|> Ash.Query.set_context(context)
|> Ash.Query.set_context(%{private: private_context})
%Ash.Changeset{} ->
Ash.Changeset.set_context(query_or_changeset, context)
query_or_changeset
|> Ash.Changeset.set_context(context)
|> Ash.Changeset.set_context(%{
private: private_context
})
end
end
@ -65,6 +91,29 @@ defmodule Ash.Actions.Helpers do
raise Ash.Error.Forbidden.ApiRequiresActor, api: api
end
opts
else
# The only time api would be nil here is when we call this helper inside of `Changeset.for_*` and `Query.for_read`
# meaning this will be run again later with the api, so we skip the validations on the api
opts
end
end
defp add_authorize?(opts, api) do
opts =
if Keyword.has_key?(opts, :authorize?) do
opts
else
case Process.get(:ash_authorize?) do
{:authorize?, value} when is_boolean(value) ->
Keyword.put(opts, :authorize?, value)
_ ->
opts
end
end
if api do
case Ash.Api.authorize(api) do
:always ->
Keyword.put(opts, :authorize?, true)

View file

@ -203,16 +203,16 @@ defmodule Ash.Actions.ManagedRelationships do
keys ->
relationship.destination
|> Ash.Query.for_read(read, input, actor: actor)
|> Ash.Query.for_read(read, input,
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Query.filter(^keys)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.set_tenant(changeset.tenant)
|> api(changeset, relationship).read_one(
authorize?: opts[:authorize?],
actor: actor
)
|> api(changeset, relationship).read_one()
|> case do
{:ok, nil} ->
create_belongs_to_record(
@ -433,15 +433,12 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.for_create(action_name, input,
require?: false,
actor: actor,
authorize?: opts[:authorize?],
relationships: opts[:relationships] || []
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api(changeset, relationship).create(
actor: actor,
authorize?: opts[:authorize?],
return_notifications?: true
)
|> api(changeset, relationship).create(return_notifications?: true)
|> case do
{:ok, created, notifications} ->
changeset =
@ -834,17 +831,14 @@ defmodule Ash.Actions.ManagedRelationships do
{:ok, input}
else
relationship.destination
|> Ash.Query.for_read(read, input, actor: actor)
|> Ash.Query.for_read(read, input, actor: actor, authorize?: opts[:authorize?])
|> Ash.Query.filter(^keys)
|> Ash.Query.do_filter(relationship.filter)
|> Ash.Query.sort(relationship.sort)
|> Ash.Query.set_context(relationship.context)
|> Ash.Query.set_tenant(changeset.tenant)
|> Ash.Query.limit(1)
|> api(changeset, relationship).read_one(
authorize?: opts[:authorize?],
actor: actor
)
|> api(changeset, relationship).read_one()
end
|> case do
{:ok, found} when not is_nil(found) ->
@ -933,6 +927,7 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(create_or_update, join_input,
actor: actor,
authorize?: opts[:authorize?],
require?: false
)
|> maybe_force_change_attribute(
@ -947,11 +942,7 @@ defmodule Ash.Actions.ManagedRelationships do
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.create(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api.create(return_notifications?: true)
|> case do
{:ok, _created, notifications} ->
case key do
@ -993,7 +984,8 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(create_or_update, input,
relationships: opts[:relationships] || [],
actor: actor
actor: actor,
authorize?: opts[:authorize?]
)
|> maybe_force_change_attribute(
relationship,
@ -1002,11 +994,7 @@ defmodule Ash.Actions.ManagedRelationships do
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api.update(return_notifications?: true)
|> case do
{:ok, updated, notifications} ->
{:ok, [updated | current_value], notifications, [updated]}
@ -1049,6 +1037,7 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.for_create(action_name, input,
require?: false,
actor: actor,
authorize?: opts[:authorize?],
relationships: opts[:relationships]
)
|> maybe_force_change_attribute(
@ -1058,11 +1047,7 @@ defmodule Ash.Actions.ManagedRelationships do
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api(changeset, relationship).create(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api(changeset, relationship).create(return_notifications?: true)
end
case created do
@ -1097,16 +1082,13 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(action_name, regular_params,
require?: false,
authorize?: opts[:authorize?],
relationships: opts[:relationships],
actor: actor
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api(changeset, relationship).create(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api(changeset, relationship).create(return_notifications?: true)
end
case created do
@ -1118,6 +1100,7 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(join_action_name, join_params,
require?: false,
authorize?: opts[:authorize?],
actor: actor
)
|> maybe_force_change_attribute(
@ -1132,11 +1115,7 @@ defmodule Ash.Actions.ManagedRelationships do
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api(changeset, relationship).create(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api(changeset, relationship).create(return_notifications?: true)
|> case do
{:ok, _join_row, notifications} ->
{:ok, [created | current_value], regular_notifications ++ notifications,
@ -1232,11 +1211,12 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(action_name, input,
actor: actor,
authorize?: opts[:authorize?],
relationships: opts[:relationships] || []
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(actor: actor, authorize?: opts[:authorize?], return_notifications?: true)
|> api.update(return_notifications?: true)
|> case do
{:ok, updated, update_notifications} ->
{:ok, [updated | current_value], update_notifications, [match]}
@ -1262,11 +1242,12 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(action_name, regular_params,
actor: actor,
authorize?: opts[:authorize?],
relationships: opts[:relationships]
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(actor: actor, authorize?: opts[:authorize?], return_notifications?: true)
|> api.update(return_notifications?: true)
|> case do
{:ok, updated, update_notifications} ->
destination_value = Map.get(updated, relationship.destination_field)
@ -1299,14 +1280,13 @@ defmodule Ash.Actions.ManagedRelationships do
result
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(join_action_name, join_params, actor: actor)
|> Ash.Changeset.for_update(join_action_name, join_params,
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.update(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api.update(return_notifications?: true)
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
|> case do
{:ok, _updated_join, join_update_notifications} ->
@ -1452,29 +1432,25 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.for_destroy(
join_action_name,
%{},
actor: actor
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api.destroy(return_notifications?: true)
|> case do
{:ok, join_notifications} ->
notifications = join_notifications ++ all_notifications
record
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.for_destroy(action_name, %{},
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api.destroy(return_notifications?: true)
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
|> case do
{:ok, destroy_destination_notifications} ->
@ -1498,14 +1474,13 @@ defmodule Ash.Actions.ManagedRelationships do
{:destroy, action_name} ->
record
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.for_destroy(action_name, %{},
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(changeset.tenant)
|> api.destroy(
authorize?: opts[:authorize?],
actor: actor,
return_notifications?: true
)
|> api.destroy(return_notifications?: true)
|> case do
{:ok, notifications} ->
{:cont, {:ok, current_value, notifications ++ all_notifications}}
@ -1574,14 +1549,10 @@ defmodule Ash.Actions.ManagedRelationships do
{:ok, result} ->
result
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor, authorize?: opts[:authorize?])
|> Ash.Changeset.set_context(join_relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.destroy(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api.destroy(return_notifications?: true)
|> case do
{:ok, notifications} ->
{:ok, notifications}
@ -1613,12 +1584,13 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.new()
|> Ash.Changeset.for_update(action_name, %{},
relationships: opts[:relationships] || [],
authorize?: opts[:authorize?],
actor: actor
)
|> maybe_force_change_attribute(relationship, :destination_field, nil)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.update(return_notifications?: true, actor: actor, authorize?: opts[:authorize?])
|> api.update(return_notifications?: true)
|> case do
{:ok, _unrelated, notifications} ->
{:ok, notifications}
@ -1667,14 +1639,10 @@ defmodule Ash.Actions.ManagedRelationships do
{:ok, result} ->
result
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor)
|> Ash.Changeset.for_destroy(action_name, %{}, actor: actor, authorize?: opts[:authorize?])
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.destroy(
return_notifications?: true,
authorize?: opts[:authorize?],
actor: actor
)
|> api.destroy(return_notifications?: true)
|> case do
{:ok, notifications} ->
{:ok, notifications}
@ -1705,10 +1673,11 @@ defmodule Ash.Actions.ManagedRelationships do
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action_name, %{},
relationships: opts[:relationships] || [],
actor: actor
actor: actor,
authorize?: opts[:authorize?]
)
|> Ash.Changeset.set_context(relationship.context)
|> Ash.Changeset.set_tenant(tenant)
|> api.destroy(return_notifications?: true, actor: actor, authorize?: opts[:authorize?])
|> api.destroy(return_notifications?: true)
end
end

View file

@ -69,6 +69,15 @@ defmodule Ash.Actions.Read do
| {:error, term}
def run(query, action, opts \\ []) do
{query, opts} = Ash.Actions.Helpers.add_process_context(query.api, query, opts)
{query, opts} =
if opts[:unsafe_no_authorize?] do
{Ash.Query.set_context(query, %{private: %{authorize?: false}}),
Keyword.put(opts, :authorize?, false)}
else
{query, opts}
end
authorize? = authorize?(opts)
opts = sanitize_opts(opts, authorize?, query)
query = set_tenant_opt(query, opts)
@ -91,6 +100,7 @@ defmodule Ash.Actions.Read do
query,
action,
actor: engine_opts[:actor],
authorize?: engine_opts[:authorize?],
timeout: opts[:timeout],
tenant: opts[:tenant]
)
@ -209,6 +219,7 @@ defmodule Ash.Actions.Read do
Ash.Query.for_read(resource, action.name, input,
tenant: tenant,
actor: actor,
authorize?: authorize?,
timeout: timeout
)
@ -218,6 +229,7 @@ defmodule Ash.Actions.Read do
action,
actor: actor,
tenant: tenant,
authorize?: authorize?,
timeout: timeout
)
end

View file

@ -14,15 +14,6 @@ defmodule Ash.Actions.Update do
def run(api, changeset, action, opts) do
{changeset, opts} = Ash.Actions.Helpers.add_process_context(api, changeset, opts)
opts =
case Map.fetch(changeset.context[:private] || %{}, :actor) do
{:ok, actor} ->
Keyword.put_new(opts, :actor, actor)
_ ->
opts
end
authorize? = authorize?(opts)
return_notifications? = opts[:return_notifications?]
actor = opts[:actor]
@ -178,7 +169,8 @@ defmodule Ash.Actions.Update do
api: api,
error_path: error_path,
changeset:
Request.resolve(changeset_dependencies, fn %{actor: actor} = context ->
Request.resolve(changeset_dependencies, fn %{actor: actor, authorize?: authorize?} =
context ->
input = changeset_input.(context) || %{}
tenant =
@ -209,11 +201,13 @@ defmodule Ash.Actions.Update do
|> Ash.Changeset.for_update(action.name, input,
actor: actor,
tenant: tenant,
authorize?: authorize?,
timeout: timeout
)
|> changeset(api, action,
actor: actor,
tenant: tenant,
authorize?: authorize?,
timeout: timeout
)
end
@ -222,6 +216,7 @@ defmodule Ash.Actions.Update do
changeset(changeset, api, action,
actor: actor,
tenant: tenant,
authorize?: authorize?,
timeout: timeout
)
end
@ -290,7 +285,6 @@ defmodule Ash.Actions.Update do
else
result =
changeset
|> Ash.Changeset.put_context(:private, %{actor: actor})
|> Ash.Changeset.before_action(
&Ash.Actions.ManagedRelationships.setup_managed_belongs_to_relationships(
&1,

View file

@ -155,16 +155,6 @@ defmodule Ash.Changeset do
require Ash.Query
# Used for eager validating identities
defmodule ShadowApi do
@moduledoc false
use Ash.Api
resources do
allow_unregistered? true
end
end
@doc """
Return a changeset over a resource or a record. `params` can be either attributes, relationship values or arguments.
@ -362,6 +352,11 @@ defmodule Ash.Changeset do
doc:
"set the actor, which can be used in any `Ash.Resource.Change`s configured on the action. (in the `context` argument)"
],
authorize?: [
type: :any,
doc:
"set authorize?, which can be used in any `Ash.Resource.Change`s configured on the action. (in the `context` argument)"
],
tenant: [
type: :any,
doc: "set the tenant on the changeset"
@ -508,12 +503,13 @@ defmodule Ash.Changeset do
changeset
|> handle_errors(action.error_handler)
|> set_actor(opts)
|> set_authorize(opts)
|> set_tenant(opts[:tenant] || changeset.tenant)
|> Map.put(:__validated_for_action__, action.name)
|> Map.put(:action, action)
|> cast_params(action, params)
|> set_argument_defaults(action)
|> run_action_changes(action, opts[:actor])
|> run_action_changes(action, opts[:actor], opts[:authorize?])
|> add_validations()
|> mark_validated(action.name)
|> require_arguments(action)
@ -602,6 +598,7 @@ defmodule Ash.Changeset do
changeset
|> handle_errors(action.error_handler)
|> set_actor(opts)
|> set_authorize(opts)
|> timeout(changeset.timeout || opts[:timeout])
|> set_tenant(opts[:tenant] || changeset.tenant || changeset.data.__metadata__[:tenant])
|> Map.put(:action, action)
@ -610,7 +607,7 @@ defmodule Ash.Changeset do
|> set_argument_defaults(action)
|> validate_attributes_accepted(action)
|> require_values(action.type, false, action.require_attributes)
|> run_action_changes(action, opts[:actor])
|> run_action_changes(action, opts[:actor], opts[:authorize?])
|> set_defaults(changeset.action_type, false)
|> add_validations()
|> mark_validated(action.name)
@ -643,13 +640,13 @@ defmodule Ash.Changeset do
Enum.reduce(identities, changeset, fn identity, changeset ->
changeset =
if identity.eager_check_with do
validate_identity(changeset, identity)
validate_identity(changeset, identity, identity.eager_check_with)
else
changeset
end
if identity.pre_check_with do
before_action(changeset, &validate_identity(&1, identity))
before_action(changeset, &validate_identity(&1, identity, identity.pre_check_with))
else
changeset
end
@ -659,37 +656,41 @@ defmodule Ash.Changeset do
defp validate_identity(
%{context: %{private: %{upsert?: true, upsert_identity: name}}} = changeset,
%{name: name}
%{name: name},
_api
) do
changeset
end
defp validate_identity(
%{action: %{soft?: true}} = changeset,
identity
identity,
api
) do
do_validate_identity(changeset, identity)
do_validate_identity(changeset, identity, api)
end
defp validate_identity(
%{action: %{type: type}} = changeset,
identity
identity,
api
)
when type in [:create, :update] do
do_validate_identity(changeset, identity)
do_validate_identity(changeset, identity, api)
end
defp validate_identity(
%{action: %{type: type}} = changeset,
identity
identity,
api
)
when type in [:create, :update] do
do_validate_identity(changeset, identity)
do_validate_identity(changeset, identity, api)
end
defp validate_identity(changeset, _), do: changeset
defp validate_identity(changeset, _, _), do: changeset
defp do_validate_identity(changeset, identity) do
defp do_validate_identity(changeset, identity, api) do
if Enum.any?(identity.keys, &changing_attribute?(changeset, &1)) do
action = Ash.Resource.Info.primary_action(changeset.resource, :read).name
@ -699,10 +700,14 @@ defmodule Ash.Changeset do
end)
changeset.resource
|> Ash.Query.for_read(action, %{}, tenant: changeset.tenant)
|> Ash.Query.for_read(action, %{},
tenant: changeset.tenant,
actor: changeset.context[:private][:actor],
authorize?: changeset.context[:private][:authorize?]
)
|> Ash.Query.do_filter(values)
|> Ash.Query.limit(1)
|> ShadowApi.read_one()
|> api.read_one()
|> case do
{:ok, nil} ->
changeset
@ -774,6 +779,14 @@ defmodule Ash.Changeset do
end
end
defp set_authorize(changeset, opts) do
if Keyword.has_key?(opts, :authorize?) do
put_context(changeset, :private, %{authorize?: opts[:authorize?]})
else
changeset
end
end
defp raise_no_action(resource, action, type) do
available_actions =
resource
@ -860,7 +873,7 @@ defmodule Ash.Changeset do
end)
end
defp run_action_changes(changeset, %{changes: changes}, actor) do
defp run_action_changes(changeset, %{changes: changes}, actor, authorize?) do
changes = changes ++ Ash.Resource.Info.changes(changeset.resource, changeset.action_type)
Enum.reduce(changes, changeset, fn
@ -872,7 +885,7 @@ defmodule Ash.Changeset do
module.validate(changeset, opts) == :ok
end) do
{:ok, opts} = module.init(opts)
module.change(changeset, opts, %{actor: actor})
module.change(changeset, opts, %{actor: actor, authorize?: authorize?})
else
changeset
end
@ -2039,6 +2052,7 @@ defmodule Ash.Changeset do
relationship.destination
|> Ash.Query.for_read(action, %{},
actor: changeset.context[:private][:actor],
authorize?: changeset.context[:private][:authorize?],
tenant: changeset.tenant
)
|> Ash.Query.limit(Enum.count(input))

View file

@ -191,7 +191,7 @@ defmodule Ash.CodeInterface do
|> Ash.Query.for_read(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
|> Ash.Query.filter(filters)
else
@ -200,13 +200,15 @@ defmodule Ash.CodeInterface do
|> Ash.Query.for_read(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
end
if unquote(interface.get? || action.get?) do
query
|> unquote(api).read_one(Keyword.drop(opts, [:query, :tenant]))
|> unquote(api).read_one(
Keyword.drop(opts, [:query, :tenant, :authorize?, :actor])
)
|> case do
{:ok, nil} ->
{:error, Ash.Error.Query.NotFound.exception(resource: query.resource)}
@ -218,7 +220,10 @@ defmodule Ash.CodeInterface do
{:error, error}
end
else
unquote(api).read(query, Keyword.drop(opts, [:query, :tenant]))
unquote(api).read(
query,
Keyword.drop(opts, [:query, :tenant, :actor, :authorize?])
)
end
end
end
@ -259,7 +264,7 @@ defmodule Ash.CodeInterface do
|> Ash.Query.for_read(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
|> Ash.Query.filter(filters)
else
@ -268,13 +273,15 @@ defmodule Ash.CodeInterface do
|> Ash.Query.for_read(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
end
if unquote(interface.get? || action.get?) do
query
|> unquote(api).read_one!(Keyword.drop(opts, [:query, :tenant]))
|> unquote(api).read_one!(
Keyword.drop(opts, [:query, :tenant, :authorize?, :actor])
)
|> case do
nil ->
raise Ash.Error.Query.NotFound, resource: query.resource
@ -283,7 +290,10 @@ defmodule Ash.CodeInterface do
result
end
else
unquote(api).read!(query, Keyword.drop(opts, [:query, :tenant]))
unquote(api).read!(
query,
Keyword.drop(opts, [:query, :tenant, :actor, :authorize?])
)
end
end
end
@ -316,10 +326,13 @@ defmodule Ash.CodeInterface do
|> Ash.Changeset.for_create(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
unquote(api).create(changeset, Keyword.drop(opts, [:actor, :changeset, :tenant]))
unquote(api).create(
changeset,
Keyword.drop(opts, [:actor, :changeset, :tenant, :authorize?])
)
end
end
@ -350,10 +363,13 @@ defmodule Ash.CodeInterface do
|> Ash.Changeset.for_create(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
unquote(api).create!(changeset, Keyword.drop(opts, [:actor, :changeset]))
unquote(api).create!(
changeset,
Keyword.drop(opts, [:actor, :changeset, :authorize?])
)
end
end
@ -386,10 +402,10 @@ defmodule Ash.CodeInterface do
|> Ash.Changeset.for_update(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
unquote(api).update(changeset, Keyword.drop(opts, [:actor, :tenant]))
unquote(api).update(changeset, Keyword.drop(opts, [:actor, :tenant, :authorize?]))
end
end
@ -422,10 +438,13 @@ defmodule Ash.CodeInterface do
|> Ash.Changeset.for_update(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
unquote(api).update!(changeset, Keyword.drop(opts, [:actor, :tenant]))
unquote(api).update!(
changeset,
Keyword.drop(opts, [:actor, :tenant, :authorize?])
)
end
end
@ -458,10 +477,13 @@ defmodule Ash.CodeInterface do
|> Ash.Changeset.for_destroy(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
unquote(api).destroy(changeset, Keyword.drop(opts, [:actor, :tenant]))
unquote(api).destroy(
changeset,
Keyword.drop(opts, [:actor, :tenant, :authorize?])
)
end
end
@ -494,10 +516,13 @@ defmodule Ash.CodeInterface do
|> Ash.Changeset.for_destroy(
unquote(action.name),
input,
Keyword.take(opts, [:actor, :tenant])
Keyword.take(opts, [:actor, :tenant, :authorize?])
)
unquote(api).destroy!(changeset, Keyword.drop(opts, [:actor, :tenant]))
unquote(api).destroy!(
changeset,
Keyword.drop(opts, [:actor, :tenant, :authorize?])
)
end
end
end

View file

@ -793,7 +793,7 @@ defmodule Ash.Engine.Request do
new_query
|> Map.put(:api, request.api)
|> Ash.Actions.Read.unpaginated_read()
|> Ash.Actions.Read.unpaginated_read(actor: request.actor, unsafe_no_authorize?: false)
|> case do
{:ok, results} ->
pkey = Ash.Resource.Info.primary_key(request.resource)

View file

@ -171,6 +171,16 @@ defmodule Ash.Filter do
end
end
# Used for fetching related data in filters, which will have already had authorization rules applied
defmodule ShadowApi do
@moduledoc false
use Ash.Api
resources do
allow_unregistered? true
end
end
@doc """
Parses a filter statement, accepting only public attributes/relationships
@ -1320,7 +1330,7 @@ defmodule Ash.Filter do
}
relationship.destination
|> Ash.Query.new(api)
|> Ash.Query.new(ShadowApi)
|> Ash.Query.do_filter(filter)
|> Ash.Actions.Read.unpaginated_read()
|> case do

View file

@ -228,6 +228,11 @@ defmodule Ash.Query do
doc:
"set the actor, which can be used in any `Ash.Resource.Change`s configured on the action. (in the `context` argument)"
],
authorize?: [
type: :boolean,
doc:
"set authorize?, which can be used in any `Ash.Resource.Change`s configured on the action. (in the `context` argument)"
],
tenant: [
type: :any,
doc: "set the tenant on the query"
@ -264,11 +269,12 @@ defmodule Ash.Query do
query
|> timeout(query.timeout || opts[:timeout])
|> set_actor(opts)
|> set_authorize?(opts)
|> Ash.Query.set_tenant(opts[:tenant] || query.tenant)
|> Map.put(:action, action)
|> Map.put(:__validated_for_action__, action_name)
|> cast_params(action, args)
|> run_preparations(action, opts[:actor])
|> run_preparations(action, opts[:actor], opts[:authorize?])
|> add_action_filters(action, opts[:actor])
|> require_arguments(action)
else
@ -294,6 +300,14 @@ defmodule Ash.Query do
end
end
defp set_authorize?(query, opts) do
if Keyword.has_key?(opts, :authorize?) do
put_context(query, :private, %{authorize?: opts[:authorize?]})
else
query
end
end
defp require_arguments(query, action) do
query
|> set_argument_defaults(action)
@ -359,14 +373,14 @@ defmodule Ash.Query do
Enum.any?(action.arguments, &(&1.private? == false && to_string(&1.name) == name))
end
defp run_preparations(query, action, actor) do
defp run_preparations(query, action, actor, authorize?) do
query.resource
|> Ash.Resource.Info.preparations()
|> Enum.concat(action.preparations || [])
|> Enum.reduce(query, fn %{preparation: {module, opts}}, query ->
case module.init(opts) do
{:ok, opts} ->
case module.prepare(query, opts, %{actor: actor}) do
case module.prepare(query, opts, %{actor: actor, authorize?: authorize?}) do
%__MODULE__{} = prepared ->
prepared

View file

@ -125,8 +125,8 @@ defmodule Ash.Test.Changeset.AuthorizerTest do
# Filter always fails on creates
assert_raise Ash.Error.Forbidden, fn ->
Post
|> Ash.Changeset.for_create(:create, %{title: "test"})
|> Api.create!(authorize?: true)
|> Ash.Changeset.for_create(:create, %{title: "test"}, authorize?: true)
|> Api.create!()
end
good_post =
@ -142,13 +142,13 @@ defmodule Ash.Test.Changeset.AuthorizerTest do
# Filters apply to the base data
assert_raise Ash.Error.Forbidden, fn ->
bad_post
|> Ash.Changeset.for_update(:update, %{title: "next"})
|> Api.update!(authorize?: true)
|> Ash.Changeset.for_update(:update, %{title: "next"}, authorize?: true)
|> Api.update!()
end
good_post
|> Ash.Changeset.for_update(:update, %{title: "next"})
|> Api.update!(authorize?: true)
|> Ash.Changeset.for_update(:update, %{title: "next"}, authorize?: true)
|> Api.update!()
end
end
end