feat: add can?/4 policy utility (#349)

* feat: add can?/4 policy utility

* refactor: change defaults and add can/4

* refactor: add typespecs, rename test
This commit is contained in:
Frank Dugan III 2022-07-14 09:05:51 -05:00 committed by GitHub
parent 4dad1e5d7d
commit 3776af9f85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 85 additions and 0 deletions

View file

@ -133,6 +133,79 @@ defmodule Ash.Policy.Info do
Extension.get_opt(resource, [:policies], :default_access_type, :strict, false)
end
@doc "A utility to determine if an actor is authorized for a given action."
@type can_option? :: {:api, module} | {:maybe_is, boolean()}
@spec can?(Ash.Resource.t(), atom(), map() | nil, list(can_option?())) :: boolean()
def can?(resource, action_or_action_name, actor, opts \\ []) do
opts = Keyword.put(opts, :maybe_is, Keyword.get(opts, :maybe_is, false))
can(resource, action_or_action_name, actor, opts)
end
@doc "A utility to determine if an actor is authorized for a given action."
@type can_option :: {:api, module} | {:maybe_is, boolean() | :maybe}
@spec can(Ash.Resource.t(), atom(), map() | nil, list(can_option())) :: boolean() | :maybe
def can(resource, action_or_action_name, actor, opts \\ []) do
api = Keyword.fetch!(opts, :api)
maybe_is = Keyword.get(opts, :maybe_is, :maybe)
action =
case action_or_action_name do
%Ash.Resource.Actions.Create{} = action -> action
%Ash.Resource.Actions.Read{} = action -> action
%Ash.Resource.Actions.Update{} = action -> action
%Ash.Resource.Actions.Destroy{} = action -> action
name when is_atom(name) -> Ash.Resource.Info.action(resource, name)
end
# Get action type from resource
case action.type do
:update ->
query =
struct(resource)
|> Ash.Changeset.new(%{})
|> Ash.Changeset.for_update(action.name)
run_check(actor, query, api: api, maybe_is: maybe_is)
:create ->
query =
resource
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(action.name)
run_check(actor, query, api: api, maybe_is: maybe_is)
:read ->
query = Ash.Query.for_read(resource, action.name)
run_check(actor, query, api: api, maybe_is: maybe_is)
:destroy ->
query =
struct(resource)
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action.name)
run_check(actor, query, api: api, maybe_is: maybe_is)
action_type ->
raise ArgumentError, message: "Invalid action type \"#{action_type}\""
end
end
defp run_check(actor, query, api: api, maybe_is: maybe_is) do
case Ash.Policy.Info.strict_check(actor, query, api) do
true ->
true
:maybe ->
maybe_is
_ ->
false
end
end
# This should be done at compile time
defp set_access_type(policies, default) when is_list(policies) do
Enum.map(policies, &set_access_type(&1, default))

View file

@ -40,6 +40,18 @@ defmodule Ash.Policy.Test.RbacTest do
end
end
test "if the action can be performed, the can utility should return true", %{
user: user,
org: org
} do
file_with_access = create_file(org, "foo")
give_role(user, org, :viewer, :file, file_with_access.id)
create_file(org, "bar")
create_file(org, "baz")
assert Ash.Policy.Info.can(File, :read, user, api: Api)
end
defp give_role(user, org, role, resource, resource_id) do
Membership
|> Ash.Changeset.new(%{role: role, resource: resource, resource_id: resource_id})