CanCanCan Cheatsheet

written

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
# Bad
alias_action :create, :read, :update, :destroy, to: :crud
can :crud, User

# Good
can [:create, :read, :update, :destroy], User

Define permissions for one resource at a time.

1
2
3
4
5
6
# Bad: Update or destroy both articles and comments
can [:update, :destroy], [Article, Comment]

# Good
can [:update, :destroy], Article
can [:update, :destroy], Comment

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 :read, Project, active: true, user_id: user.id

Can’t pass model instances into the hash - you must use their id instead:

1
2
3
4
5
# Won’t work
can :manage, Project, group: user.groups

# Will work
can :manage, Project, group: { id: user.group_ids }

Must use lower-level attributes rather than your existing scopes that wrap them:

1
can :read, Article, is_published: true

Can use a range:

1
can :read, Project, priority: 1..3

Defining conditions on associations: * Will issue a query that performs an LEFT JOIN to query conditions on associated records

1
2
can :read, Project, category: { visible: true }
can :manage, Part, service: { account: { user: { id: user.id } } }

Blocks

Use only if you need to pass additional context or can’t express the logic using hashes:

1
2
3
4
5
can :create, Project do |project, remote_ip|
  # ...
end

can? :create, Project, request.remote_ip

Only evaluated when an actual instance object is present

1
2
3
can :update, Project do |project|
  project.priority < 3
end

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
can :read, Article, Article.published_recently

# Raises exception:
# The can? and cannot? call cannot be used with a raw sql 'can' definition.
# The checking code cannot be determined for :read #<Article ..>.
can? :read, Article

When you also define a block:

  • Block is used when can? and authorize! are passed an instance
  • Scope or Raw SQL segment is use when accessible_by is called
  • Neither are used when when can? and authorize! are passed a class (only matches on action and class type)
1
2
3
4
5
6
can :read, Article, Article.published_recently do |article|
  article.created_at > DateTime.now - 3.days
end

# Works fine
can? :read, Article

Using a SQL segment:

1
2
3
can :update, Project, ["priority < ?", 3] do |project|
  project.priority < 3
end

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
can do |action, subject_class, subject|
  # ...
end

Merging ability files

1
2
3
4
# ApplicationController.rb
def current_ability
  @current_ability ||= ReadAbility.new(current_user).merge(WriteAbility.new(current_user))
end

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
class ApplicationController < ActionController::Base
  check_authorization

  skip_authorization_check :only => [:new, :create]

  check_authorization unless: :admin_subdomain? # Only skips checking authorization - still does the authorization

  private

  def admin_subdomain?
    request.subdomain == "admin"
  end
end

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:
  • Block if defined (evaluate to true/false)
  • Hash (if no block defined) Compares values of condition hash against those returned by calling the methods on the target with the same name as the corresponding condition hash key

Ignores: 
  • Raw SQL or Scope (Throws error if block isn’t also defined)
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:
  • Raw SQL or Scope: Overrides other scopes you may have applied before calling  accessible_by - must use as first scope
  • Hash (if Raw SQL or Scope not defined) Constructs a LEFT JOIN with them

Ignores:
  • Block

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
authorize! :read, Article, :message => "Unable to read this article."

Raising exception manually:

1
raise CanCan::AccessDenied.new("Not authorized!", :read, Article)

Rescuing:

1
2
3
4
5
6
7
8
9
10
11
class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do |exception|
    respond_to do |format|
      format.json { head :forbidden }
      format.html { redirect_to main_app.root_url, :alert => exception.message }
    end
  end
end

exception.action # => :read
exception.subject # => Article

Troubleshooting

Logging details of AccessDenied exceptions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# in ApplicationController
rescue_from CanCan::AccessDenied do |exception|
  if Rails.env.development?
    matching_rules = current_ability.send(:relevant_rules_for_match, exception.action, exception.subject)

    if matching_rules.any?
      describe_rules = matching_rules.map {|rule| { conditions: rule.conditions, block: rule.instance_variable_get(:@block) } }

      Rails.logger.debug "CanCanCan::AccessDenied: User has ability to #{exception.action} " \
                         "#{exception.subject.inspect}, but failed to satisfy conditions: " \
                         "#{describe_rules.to_sentence(two_words_connector: ' or ', last_word_connector: ' or ')}"
    else
      Rails.logger.debug "CanCanCan::AccessDenied: User does not have ability to #{exception.action} " \
                         "#{exception.subject.inspect}"
    end
  end
end

Order code is executed

  • Request is made and assigned to a controller
  • Controller instance calls current_ability method
  • current_ability instantiates an instance of CanCan::Ability class (so your ability.rb file’s initialize method is called)

Case: For authorize! or can?

  • Controller makes first call to authorize! or can? (which calls can? on your current_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 your current_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 a LEFT 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:
  • If not, use techniques below depending on how you’re defining your matching rule

current_ability.send(:relevant_rules_for_match, action, class_or_instance).first.matches_conditions?(action, subject, {})

Make sure:
  • Check class you’re calling accessible_by on and the arguments you’re passing it

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
undefined method `klass' for nil:NilClass

Occurs when you’ve defined an association name incorrectly in a condition hash.


Comments