Skip to content

Commit

Permalink
improvement: add authorize_with fallback option to bulk actions
Browse files Browse the repository at this point in the history
improvement: store non-expr atomic changes in attributes for simplicity
fix: make `relating_to_actor` built-in-check aware of atomics

closes #1327
  • Loading branch information
zachdaniel committed Jul 22, 2024
1 parent b8029e3 commit bad62c1
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 79 deletions.
2 changes: 2 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ spark_locals_without_parens = [
belongs_to: 2,
belongs_to: 3,
broadcast_type: 1,
bypass: 0,
bypass: 1,
bypass: 2,
calculate: 2,
Expand Down Expand Up @@ -152,6 +153,7 @@ spark_locals_without_parens = [
pagination: 1,
parse_attribute: 1,
plural_name: 1,
policy: 0,
policy: 1,
policy: 2,
pre_check?: 1,
Expand Down
8 changes: 4 additions & 4 deletions documentation/dsls/DSL:-Ash.Policy.Authorizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ end

## policies.policy
```elixir
policy condition
policy condition \\ nil
```


Expand Down Expand Up @@ -314,7 +314,7 @@ Target: `Ash.Policy.Policy`

## policies.bypass
```elixir
bypass condition
bypass condition \\ nil
```


Expand Down Expand Up @@ -594,7 +594,7 @@ end

## field_policies.field_policy_bypass
```elixir
field_policy_bypass fields, condition \\ {Ash.Policy.Check.Static, [result: true]}
field_policy_bypass fields, condition \\ nil
```


Expand Down Expand Up @@ -791,7 +791,7 @@ Target: `Ash.Policy.FieldPolicy`

## field_policies.field_policy
```elixir
field_policy fields, condition \\ {Ash.Policy.Check.Static, [result: true]}
field_policy fields, condition \\ nil
```


Expand Down
10 changes: 7 additions & 3 deletions lib/ash.ex
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,19 @@ defmodule Ash do
],
authorize_query_with: [
type: {:one_of, [:filter, :error]},
default: :filter,
doc:
"If set to `:error`, instead of filtering unauthorized query results, unauthorized query results will raise an appropriate forbidden error"
"If set to `:error`, instead of filtering unauthorized query results, unauthorized query results will raise an appropriate forbidden error. Uses `authorize_with` if not set."
],
authorize_changeset_with: [
type: {:one_of, [:filter, :error]},
doc:
"If set to `:error`, instead of filtering unauthorized changes, unauthorized changes will raise an appropriate forbidden error. Uses `authorize_with` if not set."
],
authorize_with: [
type: {:one_of, [:filter, :error]},
default: :filter,
doc:
"If set to `:error`, instead of filtering unauthorized changes, unauthorized changes will raise an appropriate forbidden error"
"If set to `:error`, instead of filtering unauthorized query results, unauthorized query results will raise an appropriate forbidden error."
],
context: [
type: :map,
Expand Down
2 changes: 1 addition & 1 deletion lib/ash/actions/destroy/bulk.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ defmodule Ash.Actions.Destroy.Bulk do
return_forbidden_error?: true,
pre_flight?: false,
atomic_changeset: atomic_changeset,
filter_with: opts[:authorize_query_with] || :filter,
filter_with: opts[:authorize_query_with] || opts[:authorize_with] || :filter,
run_queries?: false,
maybe_is: false,
alter_source?: true
Expand Down
5 changes: 3 additions & 2 deletions lib/ash/actions/update/bulk.ex
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ defmodule Ash.Actions.Update.Bulk do
%Ash.Changeset{valid?: true} = atomic_changeset <-
Ash.Changeset.handle_allow_nil_atomics(atomic_changeset, opts[:actor]),
atomic_changeset <- sort_atomic_changes(atomic_changeset),
atomic_changeset <- Ash.Changeset.move_attributes_to_atomics(atomic_changeset),
{:ok, data_layer_query} <-
Ash.Query.data_layer_query(query) do
case Ash.DataLayer.update_query(
Expand Down Expand Up @@ -1240,7 +1241,7 @@ defmodule Ash.Actions.Update.Bulk do
maybe_is: false,
pre_flight?: false,
atomic_changeset: atomic_changeset,
filter_with: opts[:authorize_query_with] || :filter,
filter_with: opts[:authorize_query_with] || opts[:authorize_with] || :filter,
run_queries?: false,
alter_source?: true,
no_check?: true
Expand Down Expand Up @@ -1275,7 +1276,7 @@ defmodule Ash.Actions.Update.Bulk do
on_must_pass_strict_check:
{:error,
%Ash.Error.Forbidden.InitialDataRequired{source: "must pass strict check"}},
filter_with: opts[:authorize_changeset_with] || :filter,
filter_with: opts[:authorize_changeset_with] || opts[:authorize_with] || :filter,
alter_source?: true,
run_queries?: false,
base_query: query
Expand Down
108 changes: 71 additions & 37 deletions lib/ash/changeset/changeset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -947,16 +947,21 @@ defmodule Ash.Changeset do
{:cont, %{changeset | arguments: Map.put(changeset.arguments, key, value)}}

attribute = Ash.Resource.Info.attribute(changeset.resource, key) ->
case Ash.Type.cast_atomic(attribute.type, value, attribute.constraints) do
{:atomic, atomic} ->
atomic = set_error_field(atomic, attribute.name)
{:cont, atomic_update(changeset, attribute.name, {:atomic, atomic})}
if Ash.Expr.expr?(value) do
case Ash.Type.cast_atomic(attribute.type, value, attribute.constraints) do
{:atomic, atomic} ->
atomic = set_error_field(atomic, attribute.name)
{:cont, atomic_update(changeset, attribute.name, {:atomic, atomic})}

{:error, error} ->
{:cont, add_invalid_errors(value, :attribute, changeset, attribute, error)}
{:error, error} ->
{:cont, add_invalid_errors(value, :attribute, changeset, attribute, error)}

{:not_atomic, reason} ->
{:halt, {:not_atomic, reason}}
{:not_atomic, reason} ->
{:halt, {:not_atomic, reason}}
end
else
{:cont,
%{changeset | attributes: Map.put(changeset.attributes, attribute.name, value)}}
end

match?("_" <> _, key) ->
Expand Down Expand Up @@ -987,15 +992,19 @@ defmodule Ash.Changeset do
attribute = Ash.Resource.Info.attribute(changeset.resource, key) ->
cond do
attribute.name in action.accept ->
case Ash.Type.cast_atomic(attribute.type, value, attribute.constraints) do
{:atomic, atomic} ->
{:cont, atomic_update(changeset, attribute.name, {:atomic, atomic})}
if Ash.Expr.expr?(value) do
case Ash.Type.cast_atomic(attribute.type, value, attribute.constraints) do
{:atomic, atomic} ->
{:cont, atomic_update(changeset, attribute.name, {:atomic, atomic})}

{:error, error} ->
{:cont, add_invalid_errors(value, :attribute, changeset, attribute, error)}
{:error, error} ->
{:cont, add_invalid_errors(value, :attribute, changeset, attribute, error)}

{:not_atomic, reason} ->
{:halt, {:not_atomic, reason}}
{:not_atomic, reason} ->
{:halt, {:not_atomic, reason}}
end
else
{:cont, force_change_attribute(changeset, attribute.name, value)}
end

key in List.wrap(opts[:skip_unknown_inputs]) ->
Expand Down Expand Up @@ -1457,7 +1466,7 @@ defmodule Ash.Changeset do
set_error_field(value, attribute.name)
end

%{changeset | atomics: Keyword.put(changeset.atomics, key, value)}
%{changeset | atomics: Keyword.put(changeset.atomics, attribute.name, value)}
|> record_atomic_update_for_atomic_upgrade(attribute.name, value)

{:not_atomic, message} ->
Expand All @@ -1468,6 +1477,13 @@ defmodule Ash.Changeset do
end
end

@doc false
def move_attributes_to_atomics(changeset) do
Enum.reduce(changeset.attributes, changeset, fn {key, value}, changeset ->
%{changeset | atomics: Keyword.put_new(changeset.atomics, key, value)}
end)
end

@doc false
def handle_allow_nil_atomics(changeset, actor) do
changeset.atomics
Expand Down Expand Up @@ -2256,26 +2272,29 @@ defmodule Ash.Changeset do
end

defp do_hydrate_atomic_refs(changeset, actor) do
Enum.reduce_while(changeset.atomics, {:ok, %{changeset | atomics: []}}, fn {key, expr},
{:ok, changeset} ->
expr =
Ash.Expr.fill_template(
expr,
actor,
changeset.arguments,
changeset.context,
changeset
)
Enum.reduce_while(
changeset.atomics,
{:ok, %{changeset | atomics: []}},
fn {key, expr}, {:ok, changeset} ->
expr =
Ash.Expr.fill_template(
expr,
actor,
changeset.arguments,
changeset.context,
changeset
)

case Ash.Filter.hydrate_refs(expr, %{resource: changeset.resource, public?: false}) do
{:ok, expr} ->
{:cont, {:ok, %{changeset | atomics: Keyword.put(changeset.atomics, key, expr)}}}
case Ash.Filter.hydrate_refs(expr, %{resource: changeset.resource, public?: false}) do
{:ok, expr} ->
{:cont, {:ok, %{changeset | atomics: Keyword.put(changeset.atomics, key, expr)}}}

{:error, error} ->
{:halt,
{:not_atomic, "Failed to validate expression #{inspect(expr)}: #{inspect(error)}"}}
{:error, error} ->
{:halt,
{:not_atomic, "Failed to validate expression #{inspect(expr)}: #{inspect(error)}"}}
end
end
end)
)
end

@doc false
Expand Down Expand Up @@ -4582,9 +4601,9 @@ defmodule Ash.Changeset do
"""
@spec update_change(t(), atom, (any -> any)) :: t()
def update_change(changeset, attribute, fun) do
case Ash.Changeset.fetch_change(changeset, attribute) do
case fetch_change(changeset, attribute) do
{:ok, change} ->
Ash.Changeset.force_change_attribute(changeset, attribute, fun.(change))
force_change_attribute(changeset, attribute, fun.(change))

:error ->
changeset
Expand Down Expand Up @@ -4773,7 +4792,15 @@ defmodule Ash.Changeset do
), casted},
{{:ok, casted}, _} <-
{Ash.Type.apply_constraints(attribute.type, casted, constraints), casted} do
data_value = Map.get(changeset.data, attribute.name)
data_value =
case changeset.data do
%Ash.Changeset.OriginalDataNotAvailable{} ->
nil

data ->
Map.get(data, attribute.name)
end

changeset = remove_default(changeset, attribute.name)

cond do
Expand Down Expand Up @@ -4895,7 +4922,14 @@ defmodule Ash.Changeset do
{:ok, casted} <- handle_change(changeset, attribute, casted, constraints),
{:ok, casted} <-
Ash.Type.apply_constraints(attribute.type, casted, constraints) do
data_value = Map.get(changeset.data, attribute.name)
data_value =
case changeset.data do
%Ash.Changeset.OriginalDataNotAvailable{} ->
nil

data ->
Map.get(data, attribute.name)
end

changeset = remove_default(changeset, attribute.name)

Expand Down
12 changes: 4 additions & 8 deletions lib/ash/code_interface.ex
Original file line number Diff line number Diff line change
Expand Up @@ -832,8 +832,7 @@ defmodule Ash.CodeInterface do
bulk_opts
|> Keyword.put(:return_records?, true)
|> Keyword.put(:return_errors?, true)
|> Keyword.put_new(:authorize_query_with, :error)
|> Keyword.put_new(:authorize_changeset_with, :error)
|> Keyword.put_new(:authorize_with, :error)
|> Keyword.put(:notify?, true)
else
bulk_opts
Expand Down Expand Up @@ -903,8 +902,7 @@ defmodule Ash.CodeInterface do
bulk_opts
|> Keyword.put(:return_records?, true)
|> Keyword.put(:return_errors?, true)
|> Keyword.put_new(:authorize_query_with, :error)
|> Keyword.put_new(:authorize_changeset_with, :error)
|> Keyword.put_new(:authorize_with, :error)
|> Keyword.put(:notify?, true)
else
bulk_opts
Expand Down Expand Up @@ -1051,8 +1049,7 @@ defmodule Ash.CodeInterface do
bulk_opts
|> Keyword.put(:return_records?, opts[:return_destroyed?])
|> Keyword.put(:return_errors?, true)
|> Keyword.put_new(:authorize_query_with, :error)
|> Keyword.put_new(:authorize_changeset_with, :error)
|> Keyword.put_new(:authorize_with, :error)
|> Keyword.put(:notify?, true)
else
Keyword.put(bulk_opts, :return_records?, opts[:return_destroyed?])
Expand Down Expand Up @@ -1139,8 +1136,7 @@ defmodule Ash.CodeInterface do
bulk_opts
|> Keyword.put(:return_records?, opts[:return_destroyed?])
|> Keyword.put(:return_errors?, true)
|> Keyword.put_new(:authorize_query_with, :error)
|> Keyword.put_new(:authorize_changeset_with, :error)
|> Keyword.put_new(:authorize_with, :error)
|> Keyword.put(:notify?, true)
else
Keyword.put(bulk_opts, :return_records?, opts[:return_destroyed?])
Expand Down
Loading

0 comments on commit bad62c1

Please sign in to comment.