Rails CanCan

CanCan is an authorization library for Ruby on Rails which restricts what resources a given user is allowed to access. All permissions are defined in a single location (the Ability class) and not duplicated across controllers, views, and database queries.

Installation

In Rails 3, add this to your Gemfile and run the bundle command.
gem "cancan"
In Rails 2, add this to your environment.rb file.
config.gem "cancan"
Alternatively, you can install it as a plugin.
rails plugin install git://github.com/ryanb/cancan.git

Getting Started

CanCan expects a current_user method to exist in the controller. First, set up some authentication (such as Authlogic or Devise). See Changing Defaults if you need different behavior.

1. Define Abilities

User permissions are defined in an Ability class. CanCan 1.5 includes a Rails 3 generator for creating this class.
rails g cancan:ability
In Rails 2.3, just add a new class in `app/models/ability.rb` with the following contents:
class Ability
  include CanCan::Ability

  def initialize(user)
  end
end
See Defining Abilities for details.

2. Check Abilities & Authorization

The current user’s permissions can then be checked using the can? and cannot? methods in the view and controller.
<% if can? :update, @article %>
  <%= link_to "Edit", edit_article_path(@article) %>
<% end %>
See Checking Abilities for more information
The authorize! method in the controller will raise an exception if the user is not able to perform the given action.
def show
  @article = Article.find(params[:id])
  authorize! :read, @article
end
Setting this for every action can be tedious, therefore the load_and_authorize_resource method is provided to automatically authorize all actions in a RESTful style resource controller. It will use a before filter to load the resource into an instance variable and authorize it for every action.
class ArticlesController < ApplicationController
  load_and_authorize_resource

  def show
    # @article is already loaded and authorized
  end
end
See Authorizing Controller Actions for more information.

3. Handle Unauthorized Access

If the user authorization fails, a CanCan::AccessDenied exception will be raised. You can catch this and modify its behavior in the ApplicationController.
class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do |exception|
    redirect_to root_url, :alert => exception.message
  end
end
See Exception Handling for more information.

4. Lock It Down

If you want to ensure authorization happens on every action in your application, add check_authorization to your ApplicationController.
class ApplicationController < ActionController::Base
  check_authorization
end
This will raise an exception if authorization is not performed in an action. If you want to skip this add skip_authorization_check to a controller subclass. See Ensure Authorization for more information.





Defining Abilities

The Ability class is where all user permissions are defined. An example class looks like this.
class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end
The current user model is passed into the initialize method, so the permissions can be modified based on any user attributes. CanCan makes no assumption about how roles are handled in your application. See Role Based Authorization for an example.

The can Method

The can method is used to define permissions and requires two arguments. The first one is the action you're setting the permission for, the second one is the class of object you're setting it on.
can :update, Article
You can pass :manage to represent any action and :all to represent any object.
can :manage, Article  # user can perform any action on the article
can :read, :all       # user can read any object
can :manage, :all     # user can perform any action on any object
Common actions are :read, :create, :update and :destroy but it can be anything. See Action Aliases and Custom Actions for more information on actions.
You can pass an array for either of these parameters to match any one. For example, here the user will have the ability to update or destroy both articles and comments.
can [:update, :destroy], [Article, Comment]
Important notice about :manage. As you read above it represents ANY action on the object. So if you have something like:
can :manage, User
can :invite, User
and if you take a test of last :invite rule you always get true. Why? That's because :manage represents ANY action on object and :manage is not just :create, :read, :update, :destroy on object.
If you want only CRUD actions on object, you should create custom action that called :crud for example, and use it instead of :manage:
def initialize(user)
  user ||= User.new

  alias_action :create, :read, :update, :destroy, :to => :crud

  can :crud, User
  can :invite, User
end

Hash of Conditions

A hash of conditions can be passed to further restrict which records this permission applies to. Here the user will only have permission to read active projects which he owns.
can :read, Project, :active => true, :user_id => user.id
It is important to only use database columns for these conditions so it can be used for Fetching Records.
You can use nested hashes to define conditions on associations. Here the project can only be read if the category it belongs to is visible.
can :read, Project, :category => { :visible => true }
The above will issue a query that performs an INNER JOIN to query conditions on associated records. If you require the associations to be queried with a LEFT OUTER JOIN then you can pass in a scope. The example below will use a scope that returns all Photos that do not belong to a group.
class Photo
  has_and_belongs_to_many :groups
  scope :unowned, includes(:groups).where(:groups => {:id => nil})
end

class Group
  has_and_belongs_to_many :photos
end

class Ability
  def initialize(user)
    user ||= User.new # guest user (not logged in)
    can :read, Photo, Photo.unowned do |photo|
      photo.groups.empty?
    end
  end
end
An array or range can be passed to match multiple values. Here the user can only read projects of priority 1 through 3.
can :read, Project, :priority => 1..3
Anything that you can pass to a hash of conditions in Active Record will work here. The only exception is working with model ids. You can't pass in the model objects directly, you must pass in the ids.
can :manage, Project, :group => { :id => user.group_ids }
If you have a complex case which cannot be done through a hash of conditions, see Defining Abilities with Blocks or MetaWhere.

Combining Abilities

It is possible to define multiple abilities for the same resource. Here the user will be able to read projects which are released OR available for preview.
can :read, Project, :released => true
can :read, Project, :preview => true
The cannot method takes the same arguments as can and defines which actions the user is unable to perform. This is normally done after a more generic can call.
can :manage, Project
cannot :destroy, Project
The order of these calls is important. See Ability Precedence for more details

Checking Abilities

After abilities are defined, you can use the can? method in the controller or view to check the user's permission for a given action and object.
can? :destroy, @project
The cannot? method is for convenience and performs the opposite check of can?
cannot? :destroy, @project
Also see Authorizing Controller Actions and Custom Actions.

Checking with Class

You can also pass the class instead of an instance (if you don't have one handy).
<% if can? :create, Project %>
  <%= link_to "New Project", new_project_path %>
<% end %>
Important: If a block or hash of conditions exist they will be ignored when checking on a class, and it will return true. For example:
can :read, Project, :priority => 3
can? :read, Project # returns true
It is impossible to answer this can? question completely because not enough detail is given. Here the class does not have a priority attribute to check on.
Think of it as asking "can the current user read a project?". The user can read a project, so this returns true. However it depends on which specific project you're talking about. If you are doing a class check, it is important you do another check once an instance becomes available so the hash of conditions can be used.
The reason for this behavior is because of the controller index action. Since the authorize_resource before filter has no instance to check on, it will use the Project class. If the authorization failed at that point then it would be impossible to filter the results later when Fetching Records.
That is why passing a class to can? will return true.


Authorizing controller actions

You can use the authorize! method to manually handle authorization in a controller action. This will raise a CanCan::AccessDenied exception when the user does not have permission. See Exception Handling for how to react to this.
def show
  @project = Project.find(params[:project])
  authorize! :show, @project
end
However that can be tedious to apply to each action. Instead you can use the load_and_authorize_resource method in your controller to load the resource into an instance variable and authorize it automatically for every action in that controller.
class ProductsController < ActionController::Base
  load_and_authorize_resource
end
This is the same as calling load_resource and authorize_resource because they are two separate steps and you can choose to use one or the other.
class ProductsController < ActionController::Base
  load_resource
  authorize_resource
end
As of CanCan 1.5 you can use the skip_load_and_authorize_resource, skip_load_resource or skip_authorize_resource methods to skip any of the applied behavior and specify specific actions like in a before filter. For example.
class ProductsController < ActionController::Base
  load_and_authorize_resource
  skip_authorize_resource :only => :new
end
Also see Controller Authorization Example, Ensure Authorization and Non RESTful Controllers.

Choosing Actions

By default this will apply to every action in the controller even if it is not one of the 7 RESTful actions. The action name will be passed in when authorizing. For example, if we have a discontinue action on ProductsController it will have this behavior.
class ProductsController < ActionController::Base
  load_and_authorize_resource
  def discontinue
    # Automatically does the following:
    # @product = Product.find(params[:id])
    # authorize! :discontinue, @product
  end
end
You can specify which actions to affect using the :except and :only options, just like a before_filter.
load_and_authorize_resource :only => [:index, :show]

Choosing actions on nested resources

For this you can pass a name to skip_authorize_resource. For example:
class CommentsController < ApplicationController
  load_and_authorize_resource :post
  load_and_authorize_resource :through => :post

  skip_authorize_resource :only => :show  
  skip_authorize_resource :post, :only => :show
end
The first skip_authorize_resource skips authorization check for comment and the second for post. Both are needed if you want to skip all authorization checks for an action.

load_resource

index action

As of 1.4 the index action will load the collection resource using accessible_by.
def index
  # @products automatically set to Product.accessible_by(current_ability)
end
If you want custom find options such as includes or pagination, you can build on this further since it is a scope.
def index
  @products = @products.includes(:category).page(params[:page])
end
The @products variable will not be set initially if Product does not respond to accessible_by (such as if you aren't using a supported ORM). It will also not be set if you are only using a block in the can definitions because there is no way to determine which records to fetch from the database.

show, edit, update and destroy actions

These member actions simply fetch the record directly.
def show
  # @product automatically set to Product.find(params[:id])
end

new and create actions

As of 1.4 these builder actions will initialize the resource with the attributes in the hash conditions. For example, if we have this can definition.
can :manage, Product, :discontinued => false
Then the product will be built with that attribute in the controller.
@product = Product.new(:discontinued => false)
This way it will pass authorization when the user accesses the new action.
The attributes are then overridden by whatever is passed by the user in params[:product].

Custom class

If the model is named differently than the controller, then you may explicitly name the model that should be loaded; however, you must specify that it is not a parent in a nested routing situation, ie:
class ArticlesController < ApplicationController
  load_and_authorize_resource :post, :parent => false
end
If the model class is namespaced differently than the controller you will need to specify the :class option.
class ProductsController < ApplicationController
  load_and_authorize_resource :class => "Store::Product"
end

Custom find

If you want to fetch a resource by something other than id it can be done so using the find_by option.
load_resource :find_by => :permalink # will use find_by_permalink!(params[:id])
authorize_resource

Override loading

The resource will only be loaded into an instance variable if it hasn't been already. This allows you to easily override how the loading happens in a separate before_filter.
class BooksController < ApplicationController
  before_filter :find_published_book, :only => :show
  load_and_authorize_resource

  private

  def find_published_book
    @book = Book.released.find(params[:id])
  end
end
It is important that any custom loading behavior happens before the call to load_and_authorize_resource. If you have authorize_resource in your ApplicationController then you need to use prepend_before_filter to do the loading in the controller subclasses so it happens before authorization.

authorize_resource

Adding authorize_resource will make a before filter which calls authorize!, passing the resource instance variable if it exists. If the instance variable isn't set (such as in the index action) it will pass in the class name. For example, if we have a ProductsController it will do this before each action.
authorize!(params[:action], @product || Product)

More info

For additional information see the load_resource and authorize_resource methods in the RDoc.
Also see Nested Resources and Non RESTful Controllers.

Resetting Current Ability

If you ever update a User record which may be the current user, it will make the current ability for that request stale. This means any can? checks will use the user record before it was updated. You will need to reset the current_ability instance so it will be reloaded. Do the same for the current_user if you are caching that too.
if @user.update_attributes(params[:user])
  @current_ability = nil
  @current_user = nil
  # ...
end




Exception Handling

The CanCan::AccessDenied exception is raised when calling authorize! in the controller and the user is not able to perform the given action. A message can optionally be provided.
authorize! :read, Article, :message => "Unable to read this article."
This exception can also be raised manually if you want more custom behavior.
raise CanCan::AccessDenied.new("Not authorized!", :read, Article)
The message can also be customized through internationalization.
# in config/locales/en.yml
en:
  unauthorized:
    manage:
      all: "Not authorized to %{action} %{subject}."
      user: "Not allowed to manage other user accounts."
    update:
      project: "Not allowed to update this project."
Notice manage and all can be used to generalize the subject and actions. Also %{action} and %{subject} can be used as variables in the message.
You can catch the exception and modify its behavior in the ApplicationController. For example here we set the error message to a flash and redirect to the home page.
class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do |exception|
    redirect_to main_app.root_url, :alert => exception.message
  end
end
The action and subject can be retrieved through the exception to customize the behavior further.
exception.action # => :read
exception.subject # => Article
The default error message can also be customized through the exception. This will be used if no message was provided.
exception.default_message = "Default error message"
exception.message # => "Default error message"
If you prefer to return the 403 Forbidden HTTP code, create a public/403.html file and write a rescue_from statement like this example in ApplicationController:
class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do |exception|
    render :file => "#{Rails.root}/public/403.html", :status => 403, :layout => false
    ## to avoid deprecation warnings with Rails 3.2.x (and incidentally using Ruby 1.9.3 hash syntax)
    ## this render call should be:
    # render file: "#{Rails.root}/public/403", formats: [:html], status: 403, layout: false
  end
end 
403.html must be pure HTML, CSS, and JavaScript--not a template. The fields of the exception are not available to it.
If you are getting unexpected behavior when rescuing from the exception it is best to add some logging . See Debugging Abilities for details.
See Authorization in Web Services for rescuing exceptions for XML responses.

Ensure Authorization

If you want to be certain authorization is not forgotten in some controller action, add check_authorization to your ApplicationController.
class ApplicationController < ActionController::Base
  check_authorization
end
This will add an after_filter to ensure authorization takes place in every inherited controller action. If no authorization happens it will raise a CanCan::AuthorizationNotPerformed exception. You can skip this check by adding skip_authorization_check to that controller. Both of these methods take the same arguments as before_filter so you can exclude certain actions with :only and :except.
class UsersController < ApplicationController
  skip_authorization_check :only => [:new, :create]
  # ...
end

Conditionally Check Authorization

As of CanCan 1.6, the check_authorization method supports :if and :unless options. Either one takes a method name as a symbol. This method will be called to determine if the authorization check will be performed. This makes it very easy to skip this check on all Devise controllers since they provide a devise_controller? method.
class ApplicationController < ActionController::Base
  check_authorization :unless => :devise_controller?
end
Here's another example where authorization is only ensured for the admin subdomain.
class ApplicationController < ActionController::Base
  check_authorization :if => :admin_subdomain?
  private
  def admin_subdomain?
    request.subdomain == "admin"
  end
end
Note: The check_authorization only ensures that authorization is performed. If you have authorize_resource the authorization will still be performed no matter what is returned here.


Changing Defaults

CanCan makes two assumptions about your application.
  • You have an Ability class which defines the permissions.
  • You have a current_user method in the controller which returns the current user model.
You can override both of these by defining the current_ability method in your ApplicationController. The current method looks like this.
def current_ability
  @current_ability ||= Ability.new(current_user)
end
The Ability class and current_user method can easily be changed to something else.
# in ApplicationController
def current_ability
  @current_ability ||= AccountAbility.new(current_account)
end
Sometimes you might have a gem in your project which provides its own Rails engine which also uses CanCan such as LocomotiveCMS. In this case the current_ability override in the ApplicationController can also be useful.
# in ApplicationController
def current_ability
  if request.fullpath =~ /\/locomotive/
    @current_ability ||= Locomotive::Ability.new(current_user)
  else
    @current_ability ||= Ability.new(current_user)
  end
end
If your method that returns the currently logged in user just has another name than current_user, it may be the easiest solution to simply alias the method in your ApplicationController like this:
class ApplicationController < ActionController::Base
  alias_method :current_user, :name_of_your_method # Could be :current_member or :logged_in_user
end
That's it! See Accessing Request Data for a more complex example of what you can do here.




Translating your app

To use translations in your app define some yaml like this:
# en.yml
en:
  unauthorized:
    manage:
      all: "You have no access!"

Translation for individual abilities

If you want to customize messages for some model or even for some ability define translation like this:
# models/ability.rb
...
can :create, Article
...
# en.yml
en:
  unauthorized:
    create:
      article: "Only admin may do this!"

Translating custom abilities

Also translations is available for your custom abilities:
# models/ability.rb
...
can :vote, Article
...
# en.yml
en:
  unauthorized:
    vote:
      article: "Only users which have one or more article may do this!"

Variables for translations

Finally you may use action(which contain ability like 'create') and subject(for example 'article') variables in your translation:
# en.yml
en:
  unauthorized:
    manage:
      all: "You do not have access to %{action} %{subject}!"
Enjoy!
 
 
 
 
 

CanCan 2.0

My ultimate goal in CanCan 2.0 is to make the behavior more intuitive. I've seen times in the issue tracker where it is used in a way it wasn't intended, or cases it authorized access when they didn't think it should. I'm taking all of this into account.
The biggest change is that basic controller action authorization will be handled by default. This means everything will be locked down with one simple enable_authorization call. The load_and_authorize_resource behavior is still there, but now only necessary if needing to check permission on resource instances.
This means abilities will be more focused on controller actions than resource classes, but this doesn't mean resources are being left in the dust. Another big feature is that you can add fine-grain permissions on specific resource attributes. See the Resource Attributes section below.

README

CanCan 2.0 is still in very early development. Currently little of what I'm describing here works, but I am writing the readme first. See the 2.0 branch for progress on the implementation and the issue tracker for feedback.

Setup

First add authentication if you haven't already. This can be done through Devise, Authlogic, etc. The only thing CanCan requires by default is a current_user method in the controller.
To install CanCan, add it to your Gemfile and run the bundle command.
gem "cancan", "2.0" # not yet available
Next generate an Ability class, this is where your permissions will be defined.
rails g cancan:ability
Then enable authorization in your ApplicationController or any controller you want authorization to happen in.
class ApplicationController < ActionController::Base
  enable_authorization
end
This will add an authorization check locking down every controller action. If you try visiting a page a CanCan::Unauthorized exception will be raised since you have not granted the user ability to access it. You can customize the behavior of this exception by passing a block.
enable_authorization do |exception|
  redirect_to root_url, :alert => exception.message
end
or you can rescue from the exception itself.
  enable_authorization
  rescue_from CanCan::Unauthorized do |exception|
    redirect_to root_url, :alert => exception.message
  end
Here it will redirect the user to the home page with an alert message when unauthorized. If you do this, make sure the user has permission to access the home page.
If you're using devise and only want to enable authorization for non-devise controllers, then you can achieve it like the code below.
enable_authorization do |exception|
  redirect_to root_url, :alert => exception.message
end unless :devise_controller?

Defining Abilities

You grant access to controller actions through the Ability class. The current_user is passed into the initialize method allowing you to define permissions based on user attributes.
def initialize(user)
  if user
    can :access, :all
  else
    can :access, :home
  end
end
As you can see here, if a logged in user exists he can access the entire application, but guest users can only access the HomeController.
The first argument to can is the name of the controller action. Using :access here will allow access to all actions on that controller. The second argument is the name of the controller. Using :all here will represent all controllers. Either one can be an array to represent multiple actions and controllers.
can [:create, :update], [:posts, :comments]
Here the user will be able to create and update both posts and comments. You don't need to mention the new and edit actions because CanCan includes some default aliases. See the Aliases page for details.
You can check permissions in any controller or view using the can? method.
<% if can? :create, :comments %>
  <%= link_to "New Comment", new_comment_path %>
<% end %>
Here the link will only show up if one can create comments.

Ability Precedence

In CanCan 2.0, there's a slight change in the way in which can rules are evaluated. A more specific can rule below a general can rule will overwrite it.
can :read, :projects
can :read, :projects, :title => 'Sir'
The specific rule in the end will override a previous generic rule.

Resources

What if you need to change authorization based on a model's attributes? You can do so by passing a hash of conditions as the last argument to can. For example, if you want to only allow one to access projects which he owns you can set the :user_id option.
can :access, :projects, :user_id => user.id
A block can also be used for complex condition checks just like in CanCan 1, but here it is not necessary.
If you try visiting any of the project pages at this point you will see a CanCan::InsufficientCheck exception is raised. This is because the default authorization has no way to check permissions on the @project instance. You can check permissions on an object manually using the authorize! method.
def edit
  @project = Project.find(params[:id])
  authorize! :edit, @project
end
However this can get tedious. Instead CanCan provides a load_and_authorize_resource method to load the @project instance in every controller action and authorize it.
class ProjectsController < ApplicationController
  load_and_authorize_resource
  def edit
    # @project already loaded here and authorized
  end
end
The index (and other collection actions) will load the @projects instance which automatically limits the projects the user is allowed to access. This is a scope so you can make further calls to where to limit what is returned from the database.
You can check permissions on instances using the can? method as well.
<%= link_to "Edit Project", edit_project_path if can? :update, @project %>
Here it will only show the edit link if the user_id matches.

Resource Attributes

It is possible to define permissions on specific resource attributes. For example, if you want to allow a user to only update the name and priority of a project, pass that as the third argument to can.
can :update, :projects, [:name, :priority]
If you use this in combination with load_and_authorize_resource it will ensure that only those two attributes exist in params[:project] when updating the project. If you do this everywhere it will not be necessary to use attr_accessible in your models.
You can combine this with a hash of conditions. For example, here the user can only update the price if the product isn't discontinued.
can :update, :products, :price, :discontinued => false
You can check permissions on specific attributes to determine what to show in the form.
<%= f.text_field :name if can? :update, @project, :name %>

What do you think?

 

Comentários

Postagens mais visitadas deste blog

Meus insights mais valiosos sobre criptomoedas para 2018 e além

O Melhor de 2012 para Designers