I’ve been working on instance-based access control using rolify and Cancan. It took me a while to work out what’s happening here, so I thought I’d share it.
Rolify is a Gem that provides a roles database table, and also some methods that hook on to your models and users. Cancan is an access control mechanism that hooks into your controllers, and checks whether users have been granted access to the operations they’re attempting to perform.
My first misconception was imagining that Rails goes to the database, gets a list of objects, and then calls through to Cancan to say “can this user access this object.” This mental model caused a lot of misunderstandings for me.
The correct mental model to use is that when an request comes in, Rails goes off to Cancan (and the ability.rb) and asks “what are the attributes of objects that this user can see?” It then goes to the database and selects these objects.
An example may be in order. In my ability.rb I have the following code:
if user.has_role? :system_admin can :manage, Foo else can :manage, Foo, :bar_id=> Bar.with_role(:bar_user, user).map(&:id) can [:show, :index], Foo, :bar_id => Bar.with_role(:bar_user, user).map(&:id) end
This code relates to two objects – a Foo and a Bar. Bar is the parent object, if you have permissions over Bar, you’re allowed to perform actions on Foos that are within that Bar. There are two roles – bar_admin and bar_user. Admins can create, edit and destroy, users can only view.
The key model here is to understand what the line of code
can :manage, Foo, :bar_id => Bar.with_role(:bar_user, user).map(&:id)
is doing. The way I see it, this is doing the following:
- A request comes in from me as a user to perform an action. Rails goes off to Cancan to determine what accesses I have
- I can :manage
- Foos that are owned by a Bar (i.e. where the :bar_id =>)
- Where that Bar has associated with it a role (Bar.with_role)
- And that role is of type :bar_user, and relates to me as a user (:bar_user, user)
- Put the ids of all those bars in an array (.map(&:id))
So, to put it another way, the ability.rb gets a list of all the Bars that I have access to, puts them in an array, and gives that back as a rule that says “can access any Foo where the bar_id is in this list.”
Which is clearly a much more performance enhancing access mechanism than I’d imagined, as this information can then be pushed directly into an SQL statement, and rails can go and get all those Foos using an index.