Refactoring CanCan(Can) Abilities

 0
Read this in "about 5 minutes".

If you have a somewhat complicated set of authorization-rules in your system, your CanCan(Can) Abilities can become quite unwieldy. Here are some tips to make them more manageable.

Use Arrays

There is an antipattern when using CanCan::Ability, that iterates over objects or verbs to define a bunch of rules that could be expressed a lot easier. Example:

Bad:

# ability.rb

[:message, :block, :like].each do |verb|
  can verb, UserProfile
end

This defining 3 rules, where only one is needed.

Good:

# ability.rb

can [:message, :block, :like], UserProfile

# Or use a literal

can %i(message block like), UserProfile

# You can use this on the objects as well
can :ignore, [UserProfile, Admin]

Prefer hashes to blocks

Block arguments to rules are slow and cannot be used in db-queries. If you can (and you won’t always be able to) you should instead use hash-arguments:

Bad:

# ability.rb

# Assuming you refer to your logged in user with `user`
can :edit, Post do |post|
  post.author == user
end

Good:

# ability.rb

can :edit, Post, author: user

If you use concerns, use concerns

If you use Concerns you can use that fact to skip tests in your rules. Since CanCan internally uses #kind_of? to test for applicable rules, you can just as well pass modules as objects of your rules.

Bad:

# ability.rb

can :message, [User, Admin] do |user|
  user.respond_to?(:received_messages)
end

Good:

# ability.rb
# Assuming both User and Admin include a messageable concern to send them
# messages.

can :message, Messageable

Use cannot for exceptions instead of blocks

If you have exceptions to a rule, or negative conditions, try to use cannot!

Bad:

can :message, User

# You can block everyone but yourself
can :block, User do |user|
  user != current_user
end

Again, this is slow and not SQL compatible. Instead use something like this:

Good:

can %i(message block), User
cannot :block, User, user_id: current_user.id

Remember the rule-precedence of CanCan: Rules you define later will override the earlier ones.

Use #merge to extract logic from your main ability

If you tried all of the above but somehow still have a file that is way to long, you can try to split it into multiple small abilities (you could call them faculties).

Bad:

# ability.rb

class Ability
  include CanCan::Ability

  def initialize(current_user)
    can :message, User
    can :block, User
    can :edit, Post, author: current_user

    # 300 lines later

    cannot :block, User, user_id: current_user.id
  end
end

Good:

class Ability
  include CanCan::Ability

  def initialize(current_user)
    [ContentFaculty, InteractionFaculty].map do |klass|
      merge klass.new(current_user)
     end
  end
end

class ContentFaculty
  include CanCan::Ability

  def initialize(current_user)
    can :edit, Post, author: current_user
  end
end

class InteractionFaculty
  include CanCan::Ability

  def initialize(current_user)
    can %i(message block), User
    cannot :block, User, user_id: current_user.id
  end
end

This way you can keep the scope and responsibility of your ability small, and your faculties easily testable.

Further Reading

You can always have a look at the official best practices or if you are feeling adventurous you can gem open cancancan and have a look into the source code yourself!


Author

Paul

Independent Webdeveloper based in Kiel, Germany. Come read my stuff!

 Leave a comment on this post