CanCanCan Cheatsheet
A breakdown of how to define and check abilities with CanCanCan, with an opinionated set of best practices based on experience.
Defining abilities
General Best practices
No access by default
Give abilities as you discover you need them
- Don’t define them upfront because you think you’ll need them
- Always use can? to enable a feature - never use cannot? to take it away
Only use the standard index
, show
, create
, update
, destroy
(edit is subset of updated and new is a subset of create)
Your ability file is not somewhere to be DRY; being explicit and clear
Don’t use manage
(its too permissive)
Don’t define aliases - always be explicit and granular:
1 2 3 4 5 6 |
|
Define permissions for one resource at a time.
1 2 3 4 5 6 |
|
Syntax
Hash Conditions
Preferred syntax (used most consistently without hidden side-effects between all 3 methods) * can? & authorize!: Keys match methods on instance, which must return values equal to the hash’s values * accessible_by: Generates a LEFT JOIN to query conditions on associated records (so almost anything you can pass to a hash of conditions in Active Record will work)
1
|
|
Can’t pass model instances into the hash - you must use their id instead:
1 2 3 4 5 |
|
Must use lower-level attributes rather than your existing scopes that wrap them:
1
|
|
Can use a range:
1
|
|
Defining conditions on associations:
* Will issue a query that performs an LEFT JOIN
to query conditions on associated records
1 2 |
|
Blocks
Use only if you need to pass additional context or can’t express the logic using hashes:
1 2 3 4 5 |
|
Only evaluated when an actual instance object is present
1 2 3 |
|
Scopes & Raw SQL
If you need an operator other than the equality one, or for other complex cases, you can provide a scope or a Raw SQL segment.
You must always also define a block, or you get an error:
1 2 3 4 5 6 |
|
When you also define a block:
- Block is used when
can?
andauthorize!
are passed an instance - Scope or Raw SQL segment is use when
accessible_by
is called - Neither are used when when
can?
andauthorize!
are passed a class (only matches on action and class type)
1 2 3 4 5 6 |
|
Using a SQL segment:
1 2 3 |
|
Operators
Operator
|
Actions
|
Conditions
|
||
Hash
|
Block
|
Raw SWL
|
||
AND
|
can [:show,
:index]
|
can :read, Article, author_id: @user.id, is_published: false
WHERE ( `articles`.`author_id` = 97 AND `articles`.`is_published` = 0 )
|
Normal Ruby syntax
|
Normal SQL syntax
|
OR
|
Read released or preview:
can :read, Project, released: true
can :read,
Project, preview: true
|
Not possible
|
||
NOT
|
Not recommended - always give abilities, don’t take them away
cannot
:destroy, Project
|
|||
Precedence
|
An ability rule will override a previous one:
If they were reversed,
cannot :destroy
would be overridden by
can :manage
.
can :manage, Project
cannot :destroy, Project
|
Equal
|
Normal Ruby precedence
|
Depends on your database
|
Defining custom logic
Useful when permissions are defined outside of Ruby such as when defining Abilities in Database
* Block will be triggered for every can?
check, even when only a class is used in the check.
1 2 3 |
|
Merging ability files
1 2 3 4 |
|
Checking abilities
General best practices
Never use load_and_authorize_resource
or load_resource
, instead use the more explicit authorize!
call for readability and flexibiltiy
Use check_authorization
to ensure all controller actions are authorized:
* Adds an after_filter
to ensure authorize!
is called for every controller action
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
CanCanCan methods
There are 3 methods for checking a user’s abilities:
|
can?
|
authorize!
|
accessible_by
|
For
|
Checking user has ability to do an action on a resource
|
Filtering a collection down to those resources a user has the ability to perform an action on
(Default: :index)
|
|
Input
|
(action, class|instance[, additional_block_args])
|
accessible_by(current_ability[, action = :index])
|
|
Output
|
Returns true/false
|
Throws CanCan::AccessDenied
exception
|
Returns scoped collection
|
Application
|
Views (for toggling UI elements)
|
Controller actions (for rescuing exceptions and rendering unauthorized)
|
index action for filtering
resources list
Elsewhere for filtering associations
|
Abilities are matched on up to 3 different attributes:
- An action type
- A target or subject class
- A set of conditions (optional, if defined)
|
Required for Definition
|
Required to match for check to pass
|
|||
Action Type
|
Yes
|
Always
|
|||
Target or Subject Type
|
Yes
|
Always
|
|||
Conditions
|
No
|
Target
|
can?
|
authorize!
|
accessible_by
|
Instance
|
Matches:
Ignores:
|
Not
Applicable
|
|||
Class
|
Ignored. Only ever
matches on action ant target.
(Raw SQL or scope
still throws error if block isn’t also defined)
|
Matches:
Ignores:
|
Usage examples
|
authorize!
|
can?
|
accessible_by
|
index
|
authorize! :index, Post
@posts = Post.accessible_by(current_ability)
|
Call on class (instance doesn’t make sense):
- if can? :index, Post
= link_to ’New post’, new_post_path
|
Always authorize! first
Always use as the first scope you apply
authorize! :index, Post
@posts = Post.accessible_by(current_ability)
|
show
|
@post = Post.find(params[:id])
authorize! :show, @post
|
- @post.each do |post|
- if can? :show, post
|
Can be used for scoping parent resources
@subject = Subject.accessible_by(current_ability,:show)
@article = @subject.articles.find(params[:id])
authorize! :show, @article
|
create
|
Assign attributes before validating (but save after)
@post = Post.new(allowed_create_attributes)
authorize! :create, @post
|
Define new instance with known values:
- if can?, :create, Post.new(subject: @subject, author: current_user)
|
Not applicable
|
update
|
Check requirements for:
* the current values of the resource using the hash
* new values using the context
can :update, Post, user_id: user.id do |post, new_attributes|
# User is now allowed to update their own posts, but not change who the post is
by
post.user_id == user.id && new_attributes[:user_id] == post.user_id
end
@post = Post.find(params[:id])
authorize! :update, @post, allowed_update_params
|
For generic update:
- if can?, :update, @post
= link_to ‘Update’, edit_post_path(@post)
For sub-update:
- if can?, :update, @post, { archived: true }
= link_to ‘Archive’, archive_post_path(@post)
|
Useful for rendering a list of resources that the user can bulk update:
@articles = Article.accessible_by(current_ability, :update)
|
destroy
|
@post = Post.find(params[:id])
authorize! :destroy, @post
|
- if can?, :destroy, @post
= link_to ‘Delete’, post_path(@post), method: :delete
|
Useful for rendering a list of resources that the user can bulk destroy:
@articles = Article.accessible_by(current_ability, :destroy)
|
Handling failures
For unauthorized requests, return 404
rather than 302
to avoid leaking information. However if you do want to render unauthorized:
Custom message to include in the exception thrown:
1
|
|
Raising exception manually:
1
|
|
Rescuing:
1 2 3 4 5 6 7 8 9 10 11 |
|
Troubleshooting
Logging details of AccessDenied exceptions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Order code is executed
- Request is made and assigned to a controller
- Controller instance calls current_ability method
current_ability
instantiates an instance ofCanCan::Ability
class (so yourability.rb
file’s initialize method is called)
Case: For authorize!
or can?
- Controller makes first call to
authorize!
orcan?
(which callscan?
on yourcurrent_ability
instance) - Matching rules are found based on the subject’s class and action
- If the subject is a class, this is where the logic stops (and we only check for a matching action)
- If the subject is an instance
- If a block is defined, its evaluated, and logic ends
- If a hash is defined, its values are matched against the subject’s attributes
Case: For accessible_by
- Controller calls
model_adaptor
on yourcurrent_ability
and other methods to find matching rules based on the subject’s class and action - Returns a
ActiveRecord::Relation
for the subject model class with the raw SQL or scope if one has bee defined, otherwise aLEFT JOIN
with the attributes from the hash
Checking
|
authorize! or can?
|
accessible_by
|
Current user and/or ability.rb logic
|
Place logging statements in your ability.rb
file
|
|
Action and Subject
|
Rescue failed authorize! in
controller:
rescue_from CanCan::AccessDenied do
|exception|
p exception.subject
p exception.action
end
Rails console:
user = User.create!
ability = Ability.new(user)
ability.can?(:destroy,
Project.new(user: user))
See if there is a matching rule for class and action:
current_ability.send(:relevant_rules_for_match, action, class_or_instance)
See if the rule’s conditions match:
current_ability.send(:relevant_rules_for_match, action, class_or_instance).first.matches_conditions?(action, subject, {})
|
Make sure:
current_ability.can?(:index, ModelClass)
|
Raw SQL or scope
|
Not applicable
|
See SQL (works for hashes too):
Project.accessible_by(ability).to_sql
|
Hash
|
See the hash that’s used to match the instance’s values:
current_ability.model_adapter(exception.subject, exception.action).conditions
|
See hash passed to where:
current_ability.model_adapter(TargetClass, :read).conditions
Test query by performing the same LEFT JOIN CanCanCan does:
User.where(id: current_user.id).joins(current_ability.model_adapter(TargetClass,
:read).conditions)
|
Block
|
Place logging or debugging statement in the block and check the result that’s returned
|
Not applicable
|
Common exceptions
1
|
|
Occurs when you’ve defined an association name incorrectly in a condition hash.