diff --git a/lib/ash.ex b/lib/ash.ex index a3e7cd63..e9801f93 100644 --- a/lib/ash.ex +++ b/lib/ash.ex @@ -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. """ diff --git a/lib/ash/actions/create.ex b/lib/ash/actions/create.ex index 292637c8..87ed53b2 100644 --- a/lib/ash/actions/create.ex +++ b/lib/ash/actions/create.ex @@ -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, diff --git a/lib/ash/actions/destroy.ex b/lib/ash/actions/destroy.ex index e65cf262..067c8605 100644 --- a/lib/ash/actions/destroy.ex +++ b/lib/ash/actions/destroy.ex @@ -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} diff --git a/lib/ash/actions/helpers.ex b/lib/ash/actions/helpers.ex index fda826f4..f559d11e 100644 --- a/lib/ash/actions/helpers.ex +++ b/lib/ash/actions/helpers.ex @@ -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) diff --git a/lib/ash/actions/managed_relationships.ex b/lib/ash/actions/managed_relationships.ex index 5a69c7f5..a1c0a376 100644 --- a/lib/ash/actions/managed_relationships.ex +++ b/lib/ash/actions/managed_relationships.ex @@ -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 diff --git a/lib/ash/actions/read.ex b/lib/ash/actions/read.ex index 08935e4d..c66ddb85 100644 --- a/lib/ash/actions/read.ex +++ b/lib/ash/actions/read.ex @@ -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 diff --git a/lib/ash/actions/update.ex b/lib/ash/actions/update.ex index ab0a75f1..1b7698df 100644 --- a/lib/ash/actions/update.ex +++ b/lib/ash/actions/update.ex @@ -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, diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index 157f0790..33dc6629 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -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)) diff --git a/lib/ash/code_interface.ex b/lib/ash/code_interface.ex index 277711a1..cdf13fda 100644 --- a/lib/ash/code_interface.ex +++ b/lib/ash/code_interface.ex @@ -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 diff --git a/lib/ash/engine/request.ex b/lib/ash/engine/request.ex index 27f35d89..326d1790 100644 --- a/lib/ash/engine/request.ex +++ b/lib/ash/engine/request.ex @@ -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) diff --git a/lib/ash/filter/filter.ex b/lib/ash/filter/filter.ex index 30cf31fe..7a39067f 100644 --- a/lib/ash/filter/filter.ex +++ b/lib/ash/filter/filter.ex @@ -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 diff --git a/lib/ash/query/query.ex b/lib/ash/query/query.ex index 0a4a2727..38c200a0 100644 --- a/lib/ash/query/query.ex +++ b/lib/ash/query/query.ex @@ -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 diff --git a/test/authorizer/authorizer_test.exs b/test/authorizer/authorizer_test.exs index 3b97154c..f4e7da36 100644 --- a/test/authorizer/authorizer_test.exs +++ b/test/authorizer/authorizer_test.exs @@ -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