CanCanCan that scales

How to use CanCanCan when your applications grows

Introduction

One of the drawbacks that people find when using CanCanCan is that it does not scale, because you are forced to define all your abilities in a single ability.rb file. This is obviously cumbersome as soon as your application grows, because this file will grow with it becoming very soon difficult to maintain. The maintainability is not the only issue: when you need to check permissions, the entire ability.rb file is evaluated, which will obviously slow down your app.

As I (quickly) wrote in my previous article, this is not the way to go when your app grows.

I will show you how to split your ability file properly and how this looks like compared to Pundit.

ModelAbility file

When things get complicated, I start refactoring Authorization classes, splitting them “per-model”. Imagine the following scenario:

# app/models/ability.rb
class Ability
include CanCan::Ability
  def initialize(user)
can :edit, User, id: user.id
can :read, Book, published: true
can :edit, Book, user_id: user.id
can :manage, Comment, user_id: user.id
end
end

This is, of course, not too complicated, and in an application I would not split this file, but for didactic reasons I want to split this file “per-model”.

My suggestion is to have an app/abilities folder and create a separate file for each model (exactly as you would do with Pundit).

# app/abilities/user_ability.rb
class UserAbility
include CanCan::Ability
  def initialize(user)
can :edit, User, id: user.id
end
end

# app/abilities/comment_ability.rb
class CommentAbility
include CanCan::Ability
  def initialize(user)
can :manage, Comment, user_id: user.id
end
end
# app/abilities/book_ability.rb
class BookAbility
include CanCan::Ability
  def initialize(user)
can :read, Book, published: true
can :edit, Book, user_id: user.id
end
end

Now you can override the current_ability method in you controller. For example:

# app/controllers/books_controller.rb
class BooksController
  def current_ability
@current_ability ||= BookAbility.new(current_user)
end
end

Advantages

Using this technique you have all the power of CanCanCan ability files, that allows you define your permissions with hash of conditions. This means you can check permissions on a single instance of a model, but also retrieve automatically all the instances where you are authorized to perform a certain action. You can call can? :read, @book but also Book.accessible_by(current_ability, :read that will return all the books you can read.

When your controller is executed, it will read only the ability file that you need, saving time and memory.

You have a much more OO design and you are ready for the future versions of CanCanCan where this technique will be supported natively and you won’t need to override the current_ability method in the controller anymore.