Idiomatic way to create attribute-specific policies

I'd like to create policies that describe which attributes an actor is allowed to CRUD, based on their attributes and other data in the model. Is there a way to do this without regard to the named action that is performing the operation? For example, I would like the attributes available for an employee record to differ based on whether the actor is themselves, an HR rep, a manager, their manager, etc. and have these policies enforced across all actions that touch those attributes. Ideally, this would also be enforced transparently in the case that the attributes being requested are not explicitly specified. For example, if a user performs a bare read, they should simply get all—and only—the attributes that they have access to without error. If a user specifically requests a set of attributes containing a subset to which they do not have access, this should error out.
8 Replies
ZachDaniel
ZachDaniel3y ago
So there isn't currently a way to do this where the policy itself will limit your access However, there are ways to achieve what you want, generally speaking 🙂 You'd do things like combine a preparation (to alter the query) with a policy (to enforce it) 1. add a policy to enforce this behavior at the policy level (for safety/security)
policy selecting(:admin_only_attribute) do
authorize_if <user_is_admin>
end
policy selecting(:admin_only_attribute) do
authorize_if <user_is_admin>
end
2. add a query preparation to deselect it in appropriate cases
preparations do
prepare fn query, context ->
if is_nil(context[:actor]) || !context.actor.is_admin do
Ash.Query.deselect(query, :admin_only_attribute)
else
query
end
end
end
preparations do
prepare fn query, context ->
if is_nil(context[:actor]) || !context.actor.is_admin do
Ash.Query.deselect(query, :admin_only_attribute)
else
query
end
end
end
\ ឵឵឵
\ ឵឵឵OP3y ago
Ok, if I understand correctly the policy selecting is the solution to the first part, alone satisfying the security requirement of applying across actions, and the prepare gives it the "soft" behavior described in the second. I suppose there must be a policy changing etc. as well?
ZachDaniel
ZachDaniel3y ago
Correct. Although in the example I specified it would actually deselect it even if the user had explicitly selected it. So you could do something like if !Ash.Query.selecting?(query, :attr) do ... deselect(...) And yep! There is a changing check So the way that policy conditions work and checks work is that anything you can use in an individual step you can use as a condition, and vice versa For example:
policy changing(:admin_only_attribute) do
authorize_if actor_attribute_equals(:admin, true)
end
policy changing(:admin_only_attribute) do
authorize_if actor_attribute_equals(:admin, true)
end
and
policy actor_attribute_equals(:admin, true) do
authorize_if changing(:admin_only_attribute)
end
policy actor_attribute_equals(:admin, true) do
authorize_if changing(:admin_only_attribute)
end
Are both valid policies (with different meanings) So you can sort of "phrase" your policies however you like. i.e if you have three specific types of user, you could do
policy <user_type_1> do

end

policy <user_type_2> do

end

policy <user_type_3> do

end
policy <user_type_1> do

end

policy <user_type_2> do

end

policy <user_type_3> do

end
\ ឵឵឵
\ ឵឵឵OP3y ago
I believe the second doesn't quite have the meaning you'd want in this case, but a macro should be able to perform the inversion in the expected way if it's preferable for concision/expressivity.
ZachDaniel
ZachDaniel3y ago
Correct, the second would be a strange policy to write 😆 Just wanted to stress the flexibility of check usage in policies/steps
\ ឵឵឵
\ ឵឵឵OP3y ago
authorize_for actor_attribute_equals(:admin, true) do
policy changing(:admin_only_a)
policy changing(:admin_only_b)
end
authorize_for actor_attribute_equals(:admin, true) do
policy changing(:admin_only_a)
policy changing(:admin_only_b)
end
Expanding to:
policy changing(:admin_only_a) do
authorize_if actor_attribute_equals(:admin, true)
end

policy changing(:admin_only_a) do
authorize_if actor_attribute_equals(:admin, true)
end
policy changing(:admin_only_a) do
authorize_if actor_attribute_equals(:admin, true)
end

policy changing(:admin_only_a) do
authorize_if actor_attribute_equals(:admin, true)
end
ZachDaniel
ZachDaniel3y ago
I think in that case I'd probably do something like authorize_changes actor_attribute_equals(:admin, true), fields: [:admin_only_a, :admin_only_b] but yeah that seems like a useful thing something like that
\ ឵឵឵
\ ឵឵឵OP3y ago
It's certainly a smaller macro (;

Did you find this page helpful?