Advise on using policies with related resources

I have two resources Transfer and Account. Transfer updates the accounts balance it's related to. Now I have admin which can create/update/read both resources. I have an operator that can only create transfers. The issue is updating the account balance when the operator creates the transfer. Since he does not have access to create/update an acccount. How should I model my policies to handle this kind of situations?
33 Replies
barnabasj
barnabasj•3mo ago
https://hexdocs.pm/ash/Ash.Policy.Check.Builtins.html#accessing_from/2 could maybe work if you use manage_relationship. Otherwise if you use a hook to call the action you can handle auth yourself, or set something in the context you can use to add a policy to your account.
franckstifler
franckstiflerOP•3mo ago
@barnabasj I tried using accessing_from on create action it did not work, but it does on reads So I tried putting some logs in the accessing_from.ex file. I noticed accessing_from in the query.context is nil, in the def match?(_actor, %{query: %Ash.Query{} = query}, options) do function. Don't know if it's a bug or I'm missing something.
defmodule Transfer do
policies do
policy always() do
authorize_if always()
end
end

relationships do
belongs_to :in_account, Account do
source_attribute :to_account
allow_nil? false
end

belongs_to :out_account, Account do
source_attribute :from_account
allow_nil? false
end
end
actions do
create :create do
primary? true
accept [:date, :amount]
argument :from_account, :uuid_v7, allow_nil?: false
argument :to_account, :uuid_v7, allow_nil?: false

change relate_actor(:user)

change manage_relationship(:from_account, :out_account, type: :append)
change manage_relationship(:to_account, :in_account, type: :append)
end
end

defmodule Account do
policies do
policy always() do
authorize_if accessing_from(Transfer, :in_account)
authorize_if accessing_from(Transfer, :out_account)
end
end
end
defmodule Transfer do
policies do
policy always() do
authorize_if always()
end
end

relationships do
belongs_to :in_account, Account do
source_attribute :to_account
allow_nil? false
end

belongs_to :out_account, Account do
source_attribute :from_account
allow_nil? false
end
end
actions do
create :create do
primary? true
accept [:date, :amount]
argument :from_account, :uuid_v7, allow_nil?: false
argument :to_account, :uuid_v7, allow_nil?: false

change relate_actor(:user)

change manage_relationship(:from_account, :out_account, type: :append)
change manage_relationship(:to_account, :in_account, type: :append)
end
end

defmodule Account do
policies do
policy always() do
authorize_if accessing_from(Transfer, :in_account)
authorize_if accessing_from(Transfer, :out_account)
end
end
end
barnabasj
barnabasj•3mo ago
So, what I assume is failing is that the lookup fails. because that one is done without the accessing_from, because at that point there is no relationship yet.
defmodule Account do
policies do
policy always() do
authorize_if action(:read) # <= should make it work.
authorize_if accessing_from(Transfer, :in_account)
authorize_if accessing_from(Transfer, :out_account)
end
end
end
defmodule Account do
policies do
policy always() do
authorize_if action(:read) # <= should make it work.
authorize_if accessing_from(Transfer, :in_account)
authorize_if accessing_from(Transfer, :out_account)
end
end
end
just for testing not sure if you really want to allow all reads like this
franckstifler
franckstiflerOP•3mo ago
Maybe there's no relationship yet, but the accessing_from could/should be set at this point? I find it strange that it's nil tbh. The action(:read) makes it work. But now it opens all the reads as you mentioned too.
barnabasj
barnabasj•3mo ago
there are potential security problems by setting it before the relationship exists because you should only be able to relate to records that you are allowed to read
franckstifler
franckstiflerOP•3mo ago
One big challenge for me is how to debug where the query for account is built, to check how the context is set there. But I really have no clue how to debug this. Any recommendations?
barnabasj
barnabasj•3mo ago
The first time I ran into this, I also just looked for manage_relationship in the code and looked throught the module put some dbg's in. Not sure if there is a better way
franckstifler
franckstiflerOP•3mo ago
Oh yes this makes sense! Currenlty to display the accounts on the UI, I use authorize?: false, for the Operator to see the list of accounts, to which he normally does not have access. I was hoping that accessing_from(Transfer, :in_account) would let me at least create my transfer into the DB, because I am accessing the accounts table from the Transfer resource. Maybe I'm not understanding the meaning of accessing_from?
barnabasj
barnabasj•3mo ago
accessing_from should always be set if something is done through a relationship. it's just that the first read in manage_relationship is not going through the relationship, it's just a read on the resource because at that point the connection between the records does not exist yet. is the operator not allowed to reads accounts at all? like if you do authorize?: false at the moment, replacing that with a more sensible policy would maybe also fix the manage_relationship problem
franckstifler
franckstiflerOP•3mo ago
Yes the operator should not read accounts. But they can do operations Transfers/Deposits that operates on Accounts. Maybe the policy is too strick as you say? What if I need to hide things like balance of the account and others?
franckstifler
franckstiflerOP•3mo ago
Totally agree with you. But atleast we know we are calling the read from the parent resource at that moment in this case which is Transfer. The policy should pass from that particular context, without causing any security concerns. I still think it's maybe the right call. I don't know if I could make my point a little clearer. Thanks this looks interesting too.
barnabasj
barnabasj•3mo ago
you could also turn it around and add create_transfer action on the account that than creates the transfer and only give the operator access to those actions on the account
franckstifler
franckstiflerOP•3mo ago
Yes you are right again. This can be turned in the other direction @Zach Daniel should'nt tag you, but would like to know if this is a bug to report or close this as it is.
ZachDaniel
ZachDaniel•3mo ago
it always runs an action on the target resource, so you can debug by adding preparations/changes on the destination resource. Its a good question if creating new records for a relationship should be considered accessing_from
barnabasj
barnabasj•3mo ago
the create sets accessing_from, it's only the read before hand
ZachDaniel
ZachDaniel•3mo ago
oh then yes that is correct šŸ˜„ We should document this somewhere we can point people at its by design otherwise you'd be able to "steal" records from other users
barnabasj
barnabasj•3mo ago
yeah, and add a flow diagram for how manage_relationship works
ZachDaniel
ZachDaniel•3mo ago
Thanks for volunteering ā¤ļø šŸ˜‚
barnabasj
barnabasj•3mo ago
Let's see what claude can come up with and refine that
franckstifler
franckstiflerOP•3mo ago
Thanks very much for your support. I'll mark this as solved then
barnabasj
barnabasj•3mo ago
I was just spelunking in the code and saw that there is a debug?: true option for manage_relationship, that logs a bunch of stuff https://hexdocs.pm/ash/Ash.Changeset.html#manage_relationship/4
franckstifler
franckstiflerOP•3mo ago
@barnabasj
field_policy :balance do
authorize_if {Mc.Authorize, [name: :view_account_balance]}
end
field_policy :balance do
authorize_if {Mc.Authorize, [name: :view_account_balance]}
end
Actor is nil in def match?(actor, %{resource: Mc.Banking.Account, action: action} = context, opts)
barnabasj
barnabasj•3mo ago
how are you calling the action?
franckstifler
franckstiflerOP•3mo ago
I'm using Cinder to render the table My policy is an Ash.Policy.SimpleCheck. Without the field_policy, it my custom policy works for normal policies. Just the fields one causing bugs.
barnabasj
barnabasj•3mo ago
can you reproduce it when calling the action directly in a test? or does it happen only using cinder? if that's the case, it could be a bug. You should get the same actor you get in the regular policy IIRC
franckstifler
franckstiflerOP•3mo ago
Yup. Bug with Cinder. A regular call works well Can I tag Rebecca Here?
barnabasj
barnabasj•3mo ago
We generally discourage tagging people and let them get to it in their own time, best thing would be a issue on GH with the steps to reproduce or even better a test that reproduces the issue
franckstifler
franckstiflerOP•3mo ago
Turns out problem was me!
barnabasj
barnabasj•3mo ago
can you add the problem/solution here, so it can be indexed for the next person please
franckstifler
franckstiflerOP•3mo ago
I did not pass the actor to my Cinder table.

Did you find this page helpful?