Get a string that represents a user's CanCan abilities
Asked Answered
R

2

10

I want to cache a Post view, but the view depends on the permissions of the current user (e.g., I only show the "edit" link if current_user.can?(:edit, @post))

So I'd like my cache key to include a representation of the current user's CanCan abilities, so that I can invalidate the cache when the user's abilities change

SO: how can I get a string that represents the current user's abilities such that 2 different users with the same abilities will generate the same "ability string"?

I've tried user.ability.inspect, but this doesn't produce the same string for different users who have the same abilities

Renaud answered 21/3, 2012 at 16:52 Comment(0)
A
15

EDIT: revised for CanCanCan

As of version 1.12 of CanCanCan (the community continuation of CanCan), Ability.new(user).permissions returns a hash with all permissions for the given user.

Previous answer (CanCan):

This might be a little complex...but here it goes..

If you pass the specified User into the Ability model required by CanCan, you can access the definition of that users role using instance_variable_get, and then break it down into whatever string values you want from there..

>> u=User.new(:role=>"admin")
>> a=Ability.new(u)
>> a.instance_variable_get("@rules").collect{ 
      |rule| rule.instance_variable_get("@actions").to_s
   }
=> ["read", "manage", "update"]

if you want to know the models in which those rules are inflicted upon, you can access the @subjects instance variable to get its name..

here is the model layout for Ability from which I worked with (pp)

Ability:0x5b41dba @rules=[
  #<CanCan::Rule:0xc114739 
    @actions=[:read], 
    @base_behavior=true, 
    @conditions={}, 
    @match_all=false, 
    @block=nil, 
    @subjects=[
      User(role: string)]>, 
  #<CanCan::Rule:0x7ec40b92 
    @actions=[:manage], 
    @base_behavior=true, 
    @conditions={}, 
    @match_all=false, 
    @block=nil, 
    @subjects=[
      Encounter(id: integer)]>, 
  #<CanCan::Rule:0x55bf110c 
    @actions=[:update], 
    @base_behavior=true, 
    @conditions={:id=>4}, 
    @match_all=false, 
    @block=nil, 
    @subjects=[
      User(role: string)]>
]
Ab answered 13/4, 2012 at 19:20 Comment(7)
I guess to sum it up, you would have to build up the array with the content of your choice, save it, and then convert its contents into the string using to_s again! :)Ab
This is a hack that relies on implementation details.Pathfinder
The only thing this relies on is that the user is using the CanCan gem, which is what the OP is using. The Ability and User classes are inherently available when using CanCanAb
Its instance variables are undocumented and could change in a newer version. It also glosses over potentially important details (like the condition of the last rule in your output). This is why you should stick to public APIs or find another way!Pathfinder
Ours was to maintain an external list of possible actions returned by the API for every resource, then test and send all of them with the objects themselves, not a general list for all objects.Pathfinder
this answer is 3 years old, if you want to update it you are welcome to.Ab
As of version 1.12 of CanCanCan (the community continuation of CanCan), Ability.new(user).permissions returns a hash with all permissions for the given user.Plainclothesman
F
7

I wanted to send my abilities to JS, and to follow up on this post, here's my helper method that you can use to convert your user abilities to an array in your controller. I then call .to_json on the array an pass it to javascript.

def ability_to_array(a)
  a.instance_variable_get("@rules").collect{ |rule| 
  { 
    :subject => rule.instance_variable_get("@subjects").map { |s| s.name }, 
    :actions => rule.instance_variable_get("@actions").map { |a| a.to_s }
  }
}
end

And here's my Backbone.js model that implements the can() method:

var Abilities = Backbone.Model.extend({
  can : function(action, subject)
  {
    return _.some(this.get("abilities"), function(a) {
      if(_.contains(a["actions"], "manage") && _.contains(a["subject"], "all")) return true;
      return _.contains(a["actions"], action) && _.contains(a["subject"], subject);
    });
   }
});
Fortification answered 29/1, 2013 at 18:44 Comment(1)
You can also just use :subject => rule.instance_variable_get("@subjects").map { |s| s.to_s}, as if you use s.name and your subject contains symbols it will errorManno

© 2022 - 2024 — McMap. All rights reserved.