access-granted
access-granted copied to clipboard
Support for introspection
I propose to add an interface for introspection uses. It can help in different situations. For example, if you need to serialize some object along with current_user's privileges on it now I should carefully copy it from policy one by one. Getting this list dynamically in run-time make things a lot of easier and less error-prone.
Yeah this is an idea I wanted to implement for a longer while now. Some sort of a debug
mode where you can see why any given permission was granted or denied.
Hi @pokonski, first of all, thanks for your work, your gem is a gem! :)
Do you have any update concerning introspection? Having a method where one could clearly see which roles apply to an user would be extremely helpful. I've just spent an afternoon trying to figure out which roles an user had and still couldn't figure it out completely.
There is a method current_policy but it is quite confusing. There's always currently logged-in user listed in each role with granted: true
which is really confusing as many of the roles listed in that output are actually not granted to that user. I tried with a test user with low authorization and even my own admin
role had granted: true
in that hash even though that low-privileged user actually did not get admin
role from the gem. So I do not know how this is constructed and what that granted: true
means for roles which are actually not granted to low-privileged user, but it would be mighty helpful to get some introspection for exactly this reason.
Could you maybe whip up something really simple to at least list the assigned roles in the upcoming months to help us users out? I mean something along the lines of a new method assigned_roles
in AccessGranted::Rails::ControllerMethods
would be really, really great.
Thank you very much for your future contributions to this much-needed gem!
hey @technodrome! There's actually an internal method called applicable_roles
. I haven't had time to work on the debug
mode mentioned earlier but it's still something we could all use.
For now you can access that list of roles by doing something like this:
current_policy.send(:applicable_roles)
I will make it public API soon :)
Thanks, @pokonski for your help. Based on that I created a method I put in application_controller to get list of all available resources and all actions grouped by roles. Someone might find that useful until your solution is available so I am putting it here to make life easier for fellow developers.
# show applicable AccessGranted roles for current user
# a temporary fix until AccessGranted API is available
# https://github.com/chaps-io/access-granted/issues/42
#
# applicable_roles returns an array of roles where each index is one role with nested objects it applies (grants access) to
# and available CRUD actions on these objects
def current_roles
policy_roles = current_policy.send(:applicable_roles)
# new unknown hash keys (:gues, :admin) will be holding nested hashes
roles = Hash.new { |hash, key| hash[key] = {} }
# for rights, new unknown hash keys will be holding arrays of rights (CRUD actions: create, read, ...) by our default set here
rights = Hash.new { |hash, key| hash[key] = [] }
policy_roles.each do |role|
# clear the hash holding rights (CRUD) for the next role (:admin, :guest), otherwise they'd get added to the rights
# of the previously processed role, i.e. we'd have two 'read' for :admin. One for the admin role itself, the other 'read'
# for :guest role if both roles apply to one user. We need 'deep_dup' to preserve 'roles' hash and clear 'rights' hash
# to get only rights (create, read, etc) for the next role
rights.clear
role.permissions.each do |permission|
# rights[:Comment] will contain [:create, :read, ...]
rights[permission.subject.to_s.to_sym] << permission.action
# do a deep dup to preserve roles as we need to clear 'rights' hash for the next role if user has more than one
roles[role.name] = rights.deep_dup
end
end
roles
end
The result is:
{
:editor => {
:Image => [
[0] :read,
[1] :create,
[2] :update,
[3] :destroy
],
:Comment => [
[0] :read,
[1] :create,
[2] :update,
[3] :destroy
]
},
:guest => {
:Image => [
[0] :read
],
:Comment => [
[0] :read
],
}
}
@technodrome but keep in mind you are not actually resolving the permissions so even if :read
is present for Image
it does not mean the user can perform that action. This is because permissions can have blocks of code which need to be executed against a specific target.
You are simply listing all defined permissions. I assume it's what you want but worth noting for any other users.
Thanks for mentioning that, @pokonski. I know, I am also using blocks to check whether a blogpost.author is identical with current_user etc. But I do not know how I could abstract away those checks to make it available as a controller method as blogpost is not defined in Image controller. I am not that well versed in the inner workings of your gem. I just took what your private method applicable_roles gave me and parsed it to offer a high level overview.
I do not know if the gem sets granted = false or something similar in case those procs resolve to false. If you can maybe shed some more light on this, I'd be easy to take that boolean into consideration when building my final hash. Or, just take it for folks who need it and keep that block caveat in mind.
I do not know if the gem sets granted = false or something similar in case those procs resolve to false.
It doesn't. Simply put, by reading role.permissions
you are actually reading just permission definitions, not actual result of can?
method. As you can imagine when you write a permission like this:
can :read, Post do |post|
post.published == true
end
it can only resolve that permission when you supply an actual instance of the Post. You cannot dump a single hash of all permission resolutions without providing subjects. I hope I made it clear with the example :)
But if you don't use blocks to define permissions, you can use a snippet I developed for one of my projects which dumps what user can do for "subject-less" permissions (ones not using any block or hash and which don't rely on actual instances of objects), as a hash of permissions:
class AccessPolicy
include AccessGranted::Policy
def permissions_map
result = {}
applicable_roles.each do |role|
role.permissions.map do |permission|
next if permission.block
result[permission.subject.name] ||= []
if can?(permission.action, permission.subject)
result[permission.subject.name].push(permission.action)
end
end
end
result
end
# (...)
end
As you can see, it iterates over applicable_roles and their permissions but checks if user can perform action using can?
and only for permissions without a block, as you can probably guess why now.
tl;dr introspection in the way you wanted to achieve is not possible
Thanks a bunch for a detailed response, Piotr. I like it when people are detailed with their answers :)
What I basically wanted was to get a list of all applicable roles, not actions in a quick way. That was my main goal and my code luckily achieves that. The task was not to check the actual applicable actions within those roles, it is more of a reminder what a member, admin, guest should be able to do, although I got curious about how you do stuff when I started digging in a bit.
As I see it, specific can? and cannot? are in this case better left for tests. So that was not the goal of the introspection. I was really after something rather simplistic - to answer the question: "which roles apply to me as a user currently viewing this page?" - because in case of my projects, several roles apply to my user simultaneously (guest + editor with editor overrides as seen in my example output). I just wanted that.
Having said that, I understand what you said and we are on the same page. I will take some time to review the code you wrote just to get to know your gem a bit more in my free time. It is like reading a story and yours really is useful.
Thanks for your help, it is much appreciated and all the best to you.
Great, I'm glad I was wrong, makes the solution much easier :D
In that case you can simply extract just the names from applicable_roles :)
The snippet I shared I personally use to extract some top level permissions from API to the frontend client. (For hiding menu options mostly :)). Resource specific actions are checked when it is serialized.
@pokonski A small update to your permissions_map method:
def permissions_map
result = {}
applicable_roles.each do |role|
role.permissions.each do |permission|
if permission.subject
permission_set = result[permission.subject.name] ||= {}
if permission.block
permission_set[permission.action] = 'check'
else
permission_set[permission.action] = can?(permission.action, permission.subject)
end
else
if permission.block
result[permission.action] = 'check'
else
result[permission.action] = can?(permission.action)
end
end
end
end
result
end
This results in something like this:
{
"administrate": false,
"fidget": true,
"wiggle": "check",
"WikiPage": {
"read": true, "create": true, "update": false, "destroy": true, "custom": false
},
"Posts": {
"read": true,
"create": true,
"update": "check",
"destroy": "check"
}
}
If you want to know on the front end if you have permission for something you can 100% trust a boolean to tell you the truth. If it's the string "check"
you'll have to ask the server to verify because it's dependent on which object you're asking about.
It slightly less than ideal since negation is just 'absence' of true
but that works nicely in javascript anyway.