improvement: allow passing query or changeset in can/can?/4 (#410)

This commit is contained in:
Frank Dugan III 2022-10-10 10:50:13 -05:00 committed by GitHub
parent ae8e0e356b
commit 62805fcce9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 68 additions and 19 deletions

View file

@ -146,15 +146,20 @@ defmodule Ash.Policy.Info do
See the documentation of `can/4` for more.
"""
@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
@spec can?(
Ash.Resource.t(),
atom() | Ash.Resource.Actions.action() | Ash.Query.t() | Ash.Changeset.t(),
map() | nil,
list(can_option?())
) :: boolean()
def can?(resource, action_or_query_or_changeset, actor, opts \\ []) do
opts = Keyword.put(opts, :maybe_is, Keyword.get(opts, :maybe_is, false))
can(resource, action_or_action_name, actor, opts)
can(resource, action_or_query_or_changeset, actor, opts)
end
@doc """
A utility to determine if an actor is or may be authorized for a given action.
A utility to determine if an actor is or may be authorized for a given action/query/changeset.
This only runs the "strict check" portion of policies, meaning that it can return `:maybe` in some cases.
If you have `access_type :runtime` in any of your policies, then you may get `:maybe` from this function.
@ -166,13 +171,20 @@ defmodule Ash.Policy.Info do
returned, and this function would return `true`.
"""
@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
@spec can(
Ash.Resource.t(),
atom() | Ash.Resource.Actions.action() | Ash.Query.t() | Ash.Changeset.t(),
map() | nil,
list(can_option())
) :: boolean() | :maybe
def can(resource, action_or_query_or_changeset, actor, opts \\ []) do
api = Keyword.fetch!(opts, :api)
maybe_is = Keyword.get(opts, :maybe_is, :maybe)
action =
case action_or_action_name do
action_or_query_or_changeset =
case action_or_query_or_changeset do
%Ash.Query{} = query -> query
%Ash.Changeset{} = changeset -> changeset
%Ash.Resource.Actions.Create{} = action -> action
%Ash.Resource.Actions.Read{} = action -> action
%Ash.Resource.Actions.Update{} = action -> action
@ -181,37 +193,44 @@ defmodule Ash.Policy.Info do
end
# Get action type from resource
case action.type do
:update ->
case action_or_query_or_changeset do
%Ash.Query{} = query ->
run_check(actor, query, api: api, maybe_is: maybe_is)
%Ash.Changeset{} = changeset ->
run_check(actor, changeset, api: api, maybe_is: maybe_is)
%{type: :update, name: name} ->
query =
struct(resource)
|> Ash.Changeset.new(%{})
|> Ash.Changeset.for_update(action.name)
|> Ash.Changeset.for_update(name)
run_check(actor, query, api: api, maybe_is: maybe_is)
:create ->
%{type: :create, name: name} ->
query =
resource
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(action.name)
|> Ash.Changeset.for_create(name)
run_check(actor, query, api: api, maybe_is: maybe_is)
:read ->
query = Ash.Query.for_read(resource, action.name)
%{type: :read, name: name} ->
query = Ash.Query.for_read(resource, name)
run_check(actor, query, api: api, maybe_is: maybe_is)
:destroy ->
%{type: :destroy, name: name} ->
query =
struct(resource)
|> Ash.Changeset.new()
|> Ash.Changeset.for_destroy(action.name)
|> Ash.Changeset.for_destroy(name)
run_check(actor, query, api: api, maybe_is: maybe_is)
action_type ->
raise ArgumentError, message: "Invalid action type \"#{action_type}\""
_ ->
raise ArgumentError,
message: "Invalid action/query/changeset \"#{inspect(action_or_query_or_changeset)}\""
end
end

View file

@ -52,6 +52,36 @@ defmodule Ash.Test.Policy.RbacTest do
assert Ash.Policy.Info.can(File, :read, user, api: Api)
end
test "if the query 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")
query = Ash.Query.for_read(File, :read)
assert Ash.Policy.Info.can(File, query, user, api: Api)
end
test "if the changeset 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)
changeset =
File
|> Ash.Changeset.new(%{name: "bar"})
|> Ash.Changeset.for_create(:create)
|> Ash.Changeset.manage_relationship(:organization, org, type: :append_and_remove)
assert Ash.Policy.Info.can(File, changeset, 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})