improvement: add matches built-in policy check (#937)

This commit is contained in:
Dmitry Maganov 2024-03-21 14:39:21 +02:00 committed by GitHub
parent c04b638136
commit ed4655cd83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 65 additions and 6 deletions

View file

@ -9,6 +9,7 @@ defmodule Ash.Policy.Check do
for an easy way to write that check.
"""
@type actor :: any
@type options :: Keyword.t()
@type authorizer :: Ash.Policy.Authorizer.t()
@type check_type :: :simple | :filter | :manual
@ -29,24 +30,24 @@ defmodule Ash.Policy.Check do
It should return `{:ok, true}` if it can tell that the request is authorized, and `{:ok, false}` if
it can tell that it is not. If unsure, it should return `{:ok, :unknown}`
"""
@callback strict_check(struct(), authorizer(), options) :: {:ok, boolean | :unknown}
@callback strict_check(actor(), authorizer(), options) :: {:ok, boolean | :unknown}
@doc """
An optional callback, that allows the check to work with policies set to `access_type :filter`
Return a keyword list filter that will be applied to the query being made, and will scope the results to match the rule
"""
@callback auto_filter(struct(), authorizer(), options()) :: Keyword.t() | Ash.Expr.t()
@callback auto_filter(actor(), authorizer(), options()) :: Keyword.t() | Ash.Expr.t()
@doc """
An optional callback, hat allows the check to work with policies set to `access_type :runtime`
Takes a list of records, and returns the subset of authorized records.
"""
@callback check(struct(), list(Ash.Resource.record()), map, options) ::
@callback check(actor(), list(Ash.Resource.record()), map, options) ::
list(Ash.Resource.record())
@doc "Describe the check in human readable format, given the options"
@callback describe(options()) :: String.t()
@callback requires_original_data?(struct(), options()) :: boolean()
@callback requires_original_data?(actor(), options()) :: boolean()
@doc """
The type of the check

View file

@ -268,4 +268,15 @@ defmodule Ash.Policy.Check.Builtins do
def changing_relationships(relationships) do
{Ash.Policy.Check.ChangingRelationships, relationships: relationships}
end
@doc "This check is true when the specified function returns true"
defmacro matches(description, func) do
{value, function} = Spark.CodeHelpers.lift_functions(func, :matches_policy_check, __CALLER__)
quote generated: true do
unquote(function)
{Ash.Policy.Check.Matches, description: unquote(description), func: unquote(value)}
end
end
end

View file

@ -0,0 +1,14 @@
defmodule Ash.Policy.Check.Matches do
@moduledoc "This check is true when the specified function returns true"
use Ash.Policy.SimpleCheck
@impl true
def describe(options) do
options[:description]
end
@impl true
def match?(actor, request, options) do
options[:func].(actor, request)
end
end

View file

@ -9,7 +9,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
required(:resource) => Ash.Resource.t(),
required(:api) => Ash.Api.t(),
optional(:query) => Ash.Query.t(),
optional(:changeset) => Ash.Query.t(),
optional(:changeset) => Ash.Changeset.t(),
optional(any) => any
}

View file

@ -4,6 +4,7 @@ defmodule Ash.Policy.SimpleCheck do
Define `c:match?/3`, which gets the actor, request context, and opts, and returns true or false
"""
@type actor :: Ash.Policy.Check.actor()
@type context :: %{
required(:action) => Ash.Resource.Actions.action(),
required(:resource) => Ash.Resource.t(),
@ -15,7 +16,7 @@ defmodule Ash.Policy.SimpleCheck do
@type options :: Keyword.t()
@doc "Whether or not the request matches the check"
@callback match?(actor :: struct(), context(), options) :: boolean
@callback match?(actor(), context(), options()) :: boolean
defmacro __using__(_) do
quote do

View file

@ -31,6 +31,22 @@ defmodule Ash.Test.Policy.SimpleTest do
end
end
test "functions can be used as checks through `matches`", %{user: user} do
Tweet
|> Ash.Changeset.for_create(:create_bar, %{bar: 2}, actor: user)
|> Api.create!()
Tweet
|> Ash.Changeset.for_create(:create_bar, %{bar: 9}, actor: user)
|> Api.create!()
assert_raise Ash.Error.Forbidden, fn ->
Tweet
|> Ash.Changeset.for_create(:create_bar, %{bar: 1}, 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

@ -14,6 +14,10 @@ defmodule Ash.Test.Support.PolicySimple.Tweet do
create :create_foo do
argument :foo, :string
end
create :create_bar do
argument :bar, :integer, allow_nil?: false
end
end
attributes do
@ -36,6 +40,14 @@ defmodule Ash.Test.Support.PolicySimple.Tweet do
policy action(:create_foo) do
authorize_if expr(is_foo(foo: arg(:foo)))
end
policy action(:create_bar) do
authorize_if matches("bar is big", &check_bar_is_big/2)
authorize_if matches("bar is even", fn _actor, context ->
rem(context.changeset.arguments.bar, 2) == 0
end)
end
end
calculations do
@ -49,4 +61,8 @@ defmodule Ash.Test.Support.PolicySimple.Tweet do
attribute_writable? true
end
end
defp check_bar_is_big(_actor, context) do
context.changeset.arguments.bar > 5
end
end