Row Level Permissions

Hi, I’m trying to return certain attributes only for some user roles aka implementing row level permissions. Currently I’m doing that with an prepare/after_action block and while that works, it doesn’t feel natural. Filters and checks don’t help and removing attributes with the after_action make them still appear in the output of AshJsonApi and I would prefer them to not appear at all. Are there other approaches I could take?
13 Replies
frankdugan3
frankdugan33y ago
Have you looked into Ash's Policies? The default mode of operation is to filter, and it allows for some pretty complex expressions to determine authorization. https://ash-hq.org/docs/guides/ash/latest/topics/policies
Ash HQ
Guide: Policies
Read the "Policies" guide on Ash HQ
frankdugan3
frankdugan33y ago
My personal favorite way to handle row-level authorization is to give users a set of roles attribute :roles, {:array, UserRole}, allow_nil?: false and then create a custom policy checker so I can create simple policies, like:
policy action_type(:read) do
description "Employees should always be able to view their own records, but only ACTIVE users with appropriate roles can see the records of other employees."

authorize_if relates_to_actor_via([:employee, :user])
authorize_if relates_to_actor_via([:employee])

forbid_unless actor_is_active()

authorize_if actor_roles_has_any([
UserRole._Executive(),
UserRole._Human_Resources()
])
end
policy action_type(:read) do
description "Employees should always be able to view their own records, but only ACTIVE users with appropriate roles can see the records of other employees."

authorize_if relates_to_actor_via([:employee, :user])
authorize_if relates_to_actor_via([:employee])

forbid_unless actor_is_active()

authorize_if actor_roles_has_any([
UserRole._Executive(),
UserRole._Human_Resources()
])
end
Jan Ulbrich
Jan UlbrichOP3y ago
I’m using policies (not that complex ones, though), but they only allow me to filter which rows to return and not the columns, right? My case is more "yes, that user is allowed to get this record, but only certain attributes"… 😇
frankdugan3
frankdugan33y ago
Ahh, so column-level policies. Missed that.
Jan Ulbrich
Jan UlbrichOP3y ago
Yes, sorry for the confusion
frankdugan3
frankdugan33y ago
Well... You can do some stuff w/ policies to block a row if certain attributes are selected, but that doesn't sound like what you want. IMO, probably the easiest thing to do is create different resources that don't have the restricted attributes at all. Ash is going to return a struct, so you will always have the nild out columns even if you come up w/ a good way of ensuring the unauthorized columns are scrubbed. Dynamic columns is not really part of the way Ash functions. Zach may have better suggestions, that's just my 2c.
Jan Ulbrich
Jan UlbrichOP3y ago
Thanks, I like the idea of an alternative struct with less fields. Looking into that!
frankdugan3
frankdugan33y ago
The way I've handled something similar is to break out User Employee EmployeePrivate into separate resources (and tables). They are related, and I just decide w/ some UI logic whether or not to load the more restricted relationships if the actor has certain roles. In a simpler case, I have another app where users can write public product endorsements. I didn't want to risk leaking any info, so I made a separate resource that only has public attributes and filters the users to just the ones that wrote endorsements. But both User and Endorsement share the same table, so it's nice and neat.
Jan Ulbrich
Jan UlbrichOP3y ago
That sounds like a clean approach: Will do! Thanks! 🙂
frankdugan3
frankdugan33y ago
If you're happy w/ the answer, would you mind closing the topic by marking it Solved? 🙂
ZachDaniel
ZachDaniel3y ago
(You actually have to do both, mark it as solved and close it) I don’t think there is automation to close things with a given tag, although I could add that to our not 🙂 **bot
Jan Ulbrich
Jan UlbrichOP3y ago
I’m still new to Discord and grooving in… 😄
ZachDaniel
ZachDaniel3y ago
Not a problem at all. It’s a brand new process so we’re still figuring it out 😅

Did you find this page helpful?