fix: allow argument references in policies

This commit is contained in:
Zach Daniel 2023-02-22 20:12:08 -05:00
parent 007e0fb081
commit 005c1bc6c1
6 changed files with 83 additions and 17 deletions

View file

@ -11,21 +11,23 @@ defmodule Ash.Error.Forbidden.CannotFilterCreates do
def message(_) do
"""
Filter checks cannot be used with create actions.
Cannot use a filter to authorize a create.
If you are using Ash.Policy.Authorizer:
To solve for this, use other checks, or write a custom check.
Many expressions, like those that reference relationships, require using custom checks for create actions.
Expressions that only reference the actor or context, for example `expr(^actor(:is_admin) == true)` will work fine.
Expressions that only reference the actor or context, for example `expr(^actor(:is_admin) == true)` will work
because those are evaluated without needing to reference data.
For create actions, there is no data yet. In the future we may support referencing simple attributes and those
references will be referring to the values of the data about to be created, but at this time we do not.
Given a policy like:
```elixir
policy expr(special == true) do
authorize_if expr(allows_special == true)
authorize_if expr(allows_special == true)
end
```
@ -33,7 +35,7 @@ defmodule Ash.Error.Forbidden.CannotFilterCreates do
```elixir
policy [expr(special == true), action_type([:read, :update, :destroy])] do
authorize_if expr(allows_special == true)
authorize_if expr(allows_special == true)
end
```
@ -41,7 +43,7 @@ defmodule Ash.Error.Forbidden.CannotFilterCreates do
```elixir
policy [changing_attributes(special: [to: true]), action_type(:create)] do
authorize_if changing_attributes(special: [to: true])
authorize_if changing_attributes(special: [to: true])
end
```

View file

@ -4,11 +4,18 @@ defmodule Ash.Policy.Check.Action do
@impl true
def describe(options) do
"action == #{inspect(options[:action])}"
operator =
if is_list(options[:action]) do
"in"
else
"=="
end
"action #{operator} #{inspect(options[:action])}"
end
@impl true
def match?(_actor, %{action: %{name: name}}, options) do
name == options[:action]
name in List.wrap(options[:action])
end
end

View file

@ -64,7 +64,7 @@ defmodule Ash.Policy.FilterCheck do
opts
|> filter()
|> Ash.Filter.build_filter_from_template(actor)
|> Ash.Filter.build_filter_from_template(actor, Ash.Policy.FilterCheck.args(authorizer))
|> try_eval(authorizer)
|> case do
{:ok, false} ->
@ -165,12 +165,22 @@ defmodule Ash.Policy.FilterCheck do
def auto_filter(actor, authorizer, opts) do
opts = Keyword.put_new(opts, :resource, authorizer.resource)
Ash.Filter.build_filter_from_template(filter(opts), actor)
Ash.Filter.build_filter_from_template(
filter(opts),
actor,
Ash.Policy.FilterCheck.args(authorizer)
)
end
def auto_filter_not(actor, authorizer, opts) do
opts = Keyword.put_new(opts, :resource, authorizer.resource)
Ash.Filter.build_filter_from_template(reject(opts), actor)
Ash.Filter.build_filter_from_template(
reject(opts),
actor,
Ash.Policy.FilterCheck.args(authorizer)
)
end
def reject(opts) do
@ -211,4 +221,15 @@ defmodule Ash.Policy.FilterCheck do
def is_filter_check?(module) do
:erlang.function_exported(module, :filter, 1)
end
@doc false
def args(%{changeset: %{arguments: arguments}}) do
arguments
end
def args(%{query: %{arguments: arguments}}) do
arguments
end
def args(_), do: %{}
end

View file

@ -48,7 +48,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
actor
|> filter(authorizer, opts)
|> Ash.Filter.build_filter_from_template(actor)
|> Ash.Filter.build_filter_from_template(actor, Ash.Policy.FilterCheck.args(authorizer))
|> try_eval(authorizer)
|> case do
{:ok, false} ->
@ -149,12 +149,22 @@ defmodule Ash.Policy.FilterCheckWithContext do
def auto_filter(actor, authorizer, opts) do
opts = Keyword.put_new(opts, :resource, authorizer.resource)
Ash.Filter.build_filter_from_template(filter(actor, authorizer, opts), actor)
Ash.Filter.build_filter_from_template(
filter(actor, authorizer, opts),
actor,
Ash.Policy.FilterCheck.args(authorizer)
)
end
def auto_filter_not(actor, authorizer, opts) do
opts = Keyword.put_new(opts, :resource, authorizer.resource)
Ash.Filter.build_filter_from_template(reject(actor, authorizer, opts), actor)
Ash.Filter.build_filter_from_template(
reject(actor, authorizer, opts),
actor,
Ash.Policy.FilterCheck.args(authorizer)
)
end
def reject(actor, authorizer, opts) do

View file

@ -19,6 +19,18 @@ defmodule Ash.Test.Policy.SimpleTest do
assert [] = Api.read!(Tweet, actor: user)
end
test "arguments can be referenced in expression policies", %{admin: admin, user: user} do
Tweet
|> Ash.Changeset.for_create(:create_foo, %{foo: "foo", user_id: admin.id}, actor: user)
|> Api.create!()
assert_raise Ash.Error.Forbidden, fn ->
Tweet
|> Ash.Changeset.for_create(:create_foo, %{foo: "bar", user_id: admin.id}, actor: user)
|> Api.create!()
end
end
test "filter checks work on create/update/destroy actions", %{user: user} do
user2 = Api.create!(Ash.Changeset.new(User))

View file

@ -10,6 +10,10 @@ defmodule Ash.Test.Support.PolicySimple.Tweet do
actions do
defaults [:create, :read, :update, :destroy]
create :create_foo do
argument :foo, :string
end
end
attributes do
@ -21,12 +25,22 @@ defmodule Ash.Test.Support.PolicySimple.Tweet do
authorize_if always()
end
policy always() do
policy action_type([:read, :update, :destroy]) do
authorize_if(expr(user_id == ^actor(:id)))
end
policy action(:create) do
authorize_if relating_to_actor(:user)
end
policy action(:create_foo) do
authorize_if expr(^arg(:foo) == "foo")
end
end
relationships do
belongs_to :user, Ash.Test.Support.PolicySimple.User
belongs_to :user, Ash.Test.Support.PolicySimple.User do
attribute_writable? true
end
end
end