Here's why:
1. Although scopes are fine for fetching, they pose a problem when authorizing a discrete action.
For example, this declaration in Ability:
can :read, Article, Article.is_published
causes this CanCan::Error
:
The can? and cannot? call cannot be used with a raw sql 'can' definition.
The checking code cannot be determined for :read #<Article ..>.
A better way to define the same is:
can :read, Article, is_published: true
2. Hash conditions are DRYer.
By using hashes instead of blocks for all actions, you won't have to worry about translating blocks used for member controller actions (:create
, :destroy
, :update
) to equivalent blocks for collection actions (:index
, :show
)—which require hashes anyway!
3. Hash conditions are OR'd in SQL, giving you maximum flexibilty.
Every time you define an ability with can
, each can
chains together with OR in the final SQL query for that model.
So if, in addition to the is_published
condition above, we want to allow authors to see their drafts:
can :read, Article, author_id: @user.id, is_published: false
Then the final SQL would be:
SELECT `articles`.*
FROM `articles`
WHERE `articles`.`is_published` = 1
OR ( `articles`.`author_id` = 97 AND `articles`.`is_published` = 0 )
4. For complex object graphs, hash conditions accommodate joins
easily.
As I suggested in this topic on Reddit you should, when possible, give increasing permissions to your users. CanCanCan increases permissions, it starts by giving no permissions to nobody and then increases those permissions depending on the user. A properly written ability.rb looks like that:
class Ability
include CanCan::Ability
def initialize(user)
can :read, Post # start by defining rules for all users, also not logged ones
return unless user.present?
can :manage, Post, user_id: user.id # if the user is logged in can manage it's own posts
can :create, Comment # logged in users can also create comments
return unless user.manager? # if the user is a manager we give additional permissions
can :manage, Comment # like managing all comments in the website
return unless user.admin?
can :manage, :all # finally we give all remaining permissions only to the admins
end
end
following this good practice will help you to keep your permissions clean and more readable.
The risk of giving wrong permissions to the wrong users is also decreased.
Another help, to make CanCanCan work more in a "pundit way" is to define a separate Ability file for each model, or controller, and then use
def current_ability
@current_ability ||= MyAbility.new(current_user)
end
To use a specific ability file: this way you don't have to load the whole ability.rb file on each request.
Abilities files can always be merged together, so if you need two of them in one Controller, you can simply:
def current_ability
@current_ability ||= ReadAbility.new(current_user).merge(WriteAbility.new(current_user))
end
You can read more about splitting the Ability file in this article