diff --git a/lib/ash/policy/check.ex b/lib/ash/policy/check.ex index cf2d4dc2..9e3df1fa 100644 --- a/lib/ash/policy/check.ex +++ b/lib/ash/policy/check.ex @@ -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 diff --git a/lib/ash/policy/check/built_in_checks.ex b/lib/ash/policy/check/built_in_checks.ex index 715ccea5..2c5b3392 100644 --- a/lib/ash/policy/check/built_in_checks.ex +++ b/lib/ash/policy/check/built_in_checks.ex @@ -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 diff --git a/lib/ash/policy/check/matches.ex b/lib/ash/policy/check/matches.ex new file mode 100644 index 00000000..8188834a --- /dev/null +++ b/lib/ash/policy/check/matches.ex @@ -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 diff --git a/lib/ash/policy/filter_check_with_context.ex b/lib/ash/policy/filter_check_with_context.ex index c0267b97..09d0b5d3 100644 --- a/lib/ash/policy/filter_check_with_context.ex +++ b/lib/ash/policy/filter_check_with_context.ex @@ -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 } diff --git a/lib/ash/policy/simple_check.ex b/lib/ash/policy/simple_check.ex index 488b1938..86fd4099 100644 --- a/lib/ash/policy/simple_check.ex +++ b/lib/ash/policy/simple_check.ex @@ -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 diff --git a/test/policy/simple_test.exs b/test/policy/simple_test.exs index 3c3f3538..1f43aaac 100644 --- a/test/policy/simple_test.exs +++ b/test/policy/simple_test.exs @@ -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)) diff --git a/test/support/policy_simple/resources/tweet.ex b/test/support/policy_simple/resources/tweet.ex index 4162ebb8..98dc2ad7 100644 --- a/test/support/policy_simple/resources/tweet.ex +++ b/test/support/policy_simple/resources/tweet.ex @@ -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