Using args in policies

I'm trying to figure out how to use args in my policy as I've seen in the documentation but all I ever get back is nil. Any help would be appreciated. Here is my code. How does one get the attributes to use in your policies? I see in the docs something like ^arg(:attribute_name) but doing so is not working for me. This is my resource.
defmodule OpenBudget.Budgets.Transaction do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer]

def what_is_it(id) do
dbg(id)
false
end

relationships do
belongs_to :bank_account, OpenBudget.Budgets.BankAccount do
primary_key? true
end
end

policies do
policy action_type(:create) do
authorize_if actor_present()
end

policy action_type([:read, :update, :destroy]) do
authorize_if expr(what_is_it(^arg(:title)))
authorize_if expr(^arg(:bank_account_id) == ^actor(:id))
end
end

attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false
attribute :amount, :decimal, allow_nil?: false, default: 0
attribute :bank_account_id, :uuid, allow_nil?: false
end

actions do
defaults [:update, :destroy]

read :read do
get? true
end

create :create_transaction do
accept [:title, :amount]

argument :bank_account_id, :uuid do
allow_nil? false
end

change manage_relationship(:bank_account_id, :bank_account, type: :append_and_remove)
end

update :update_title do
require_attributes [:title]
end
end

postgres do
table "transactions"
repo OpenBudget.Repo
end
end
defmodule OpenBudget.Budgets.Transaction do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer]

def what_is_it(id) do
dbg(id)
false
end

relationships do
belongs_to :bank_account, OpenBudget.Budgets.BankAccount do
primary_key? true
end
end

policies do
policy action_type(:create) do
authorize_if actor_present()
end

policy action_type([:read, :update, :destroy]) do
authorize_if expr(what_is_it(^arg(:title)))
authorize_if expr(^arg(:bank_account_id) == ^actor(:id))
end
end

attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false
attribute :amount, :decimal, allow_nil?: false, default: 0
attribute :bank_account_id, :uuid, allow_nil?: false
end

actions do
defaults [:update, :destroy]

read :read do
get? true
end

create :create_transaction do
accept [:title, :amount]

argument :bank_account_id, :uuid do
allow_nil? false
end

change manage_relationship(:bank_account_id, :bank_account, type: :append_and_remove)
end

update :update_title do
require_attributes [:title]
end
end

postgres do
table "transactions"
repo OpenBudget.Repo
end
end
No description
14 Replies
lifeofdan
lifeofdanOP3y ago
Here is a picture of the relevant section I have a feeling I'm not importing something I need to be.
ZachDaniel
ZachDaniel3y ago
arg is only used for accessing input arguments (i.e argument :amount, :decimal) In your case, you can just do expr(bank_account_id == ^actor(:id)) for example
lifeofdan
lifeofdanOP3y ago
Okay, that seems to work in the same manner as using authorize_if relates_to_actor_via(:bank_account) in that, when doing a query to read a transaction, if an actor is passed at all, even an incorrect one, it will return {:ok, []} . So it does not return anything, but it does say :ok. Is this the expected behaviour? I was expecting it to return :error
ZachDaniel
ZachDaniel3y ago
What its actually doing is restricting the query to only the results that would pass your policies So its attaching a bank_account_id == ^actor(:id) filter to the query itself If you want to produce a forbidden on read actions, it tends to get a bit more manual, because introspecting filters is not typically a good idea. So what you'd do is something like this:
read :read do
argument :bank_account_id, :....
filter expr(....) # attach a filter using that
end

# since you are matching arguments, best to have a policy per action
policy action(:read) do
authorize_if CustomCheckName # maybe AccessingAccountId
end

defmodule CustomCheckName do
use Ash.Policy.SimpleCheck

def describe(_), do: "a description"

def match?(nil, _, _), do: false
def match?(%{account_id: account_id}, %{changeset: %Ash.Changeset{} = changeset}, _) do
account_id == Ash.Changeset.get_argument(changeset, :account_id)
end
end
read :read do
argument :bank_account_id, :....
filter expr(....) # attach a filter using that
end

# since you are matching arguments, best to have a policy per action
policy action(:read) do
authorize_if CustomCheckName # maybe AccessingAccountId
end

defmodule CustomCheckName do
use Ash.Policy.SimpleCheck

def describe(_), do: "a description"

def match?(nil, _, _), do: false
def match?(%{account_id: account_id}, %{changeset: %Ash.Changeset{} = changeset}, _) do
account_id == Ash.Changeset.get_argument(changeset, :account_id)
end
end
By not using filter checks, and instead using simple checks, you can produce forbidden results. But using expr/1 will filter the data. Its a bit magical, I know, but its a really smart solver under the hood.
lifeofdan
lifeofdanOP3y ago
Right. That is interesting. I think the "authorize" wording confused me. In essence the expr() is saying, this person is authorized to access data but only this particular data so only return that. When I think of "authorize" I think "can they do this at all". I suppose the authorize_if works similar to graphql, which also posts the data then has to check if it can return. forbid_if seems to work differently?
ZachDaniel
ZachDaniel3y ago
Well, its sort of a natural extension to "can the user see this at all", i.e "well just show me the ones they can see". So the tooling provides a way to express essentially both at the same time. There are security implications to that also which is part of the reason we do it i.e if you say ?account_id=<some_account_id> and the system says Forbidden! but then you use a different account_id and it says Not Found!, you can start to enumerate/discover the data under the hood. So using filter checks can prevent that sort of thing from being a problem by doing both jobs at once. All of the checks work effectively the same, it just causes us to remix the expressions differently
forbid_if expr(name == "fred")
authorize_if expr(name == "jack")
forbid_if expr(name == "fred")
authorize_if expr(name == "jack")
not(name == "fred") and name == "jack"
not(name == "fred") and name == "jack"
lifeofdan
lifeofdanOP3y ago
So would you say returning an :ok with no data is more secure than returning forbidden?
ZachDaniel
ZachDaniel3y ago
generally speaking, yes.
lifeofdan
lifeofdanOP3y ago
interesting...
ZachDaniel
ZachDaniel3y ago
Not an absolute thing though, and you are more than welcome to write explicit checks to say "can you do this action at all" its just not what expr/1 is for is all
lifeofdan
lifeofdanOP3y ago
So why does, this not work. forbid_unless expr(bank_account_id != ^actor(:id))
ZachDaniel
ZachDaniel3y ago
A policy must be explicitly allowed so if you only have a forbid_unless there is no way to actually authorize the policy i.e if you go from top to bottom and follow each step, nothing says "authorized"
forbid_unless <your_expr>
authorize_if always()
forbid_unless <your_expr>
authorize_if always()
is a way to do a "white-list" style policy You can mix and match simple checks and filter checks, also:
policy action(:read) do
forbid_unless UsingCorrectAccountId
authorize_if actor_attribute_equals(:admin)
authorize_if expr(public == true)
end
policy action(:read) do
forbid_unless UsingCorrectAccountId
authorize_if actor_attribute_equals(:admin)
authorize_if expr(public == true)
end
That would require that a user uses their own account id always, but filter public == true if the user is not an admin
lifeofdan
lifeofdanOP3y ago
Right, okay, this is all starting to make sense now. Thank you for taking so much time and really explaining this. You answered my first questions ages ago, but now I am really getting a whole picture.
ZachDaniel
ZachDaniel3y ago
Glad to help!

Did you find this page helpful?