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.
The authorize! method in the controller will raise an exception if the user is not able to perform the given action.
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
The
You can pass
Common actions are
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.
Important notice about :manage. As you read above it represents ANY action on the object. So if you have something like:
and if you take a test of last
If you want only CRUD actions on object, you should create custom action that called
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.
The above will issue a query that performs an
An array or range can be passed to match multiple values. Here the user can only read projects of priority 1 through 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.
If you have a complex case which cannot be done through a hash of conditions, see Defining Abilities with Blocks or MetaWhere.
The
The order of these calls is important. See Ability Precedence for more details
The
Also see Authorizing Controller Actions and Custom Actions.
Important: If a block or hash of conditions exist they will be ignored when checking on a class, and it will return
It is impossible to answer this
Think of it as asking "can the current user read a project?". The user can read a project, so this returns
The reason for this behavior is because of the controller
That is why passing a class to
The biggest change is that basic controller action authorization will be handled by default. This means everything will be locked down with one simple
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.
To install CanCan, add it to your Gemfile and run the
Next generate an Ability class, this is where your permissions will be defined.
Then enable authorization in your ApplicationController or any controller you want authorization to happen in.
This will add an authorization check locking down every controller action. If you try visiting a page a
or you can rescue from the exception itself.
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.
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
Here the user will be able to create and update both posts and comments. You don't need to mention the
You can check permissions in any controller or view using the
Here the link will only show up if one can create comments.
The specific rule in the end will override a previous generic rule.
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
However this can get tedious. Instead CanCan provides a
The
You can check permissions on instances using the
Here it will only show the edit link if the
If you use this in combination with
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.
You can check permissions on specific attributes to determine what to show in the form.
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:abilityIn 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 endSee 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 endSetting 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 endSee 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 endSee 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 endThis 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
TheAbility
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 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
: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
: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]
can :manage, User
can :invite, User
: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
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 }
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
can :read, Project, :priority => 1..3
can :manage, Project, :group => { :id => user.group_ids }
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
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
Checking Abilities
After abilities are defined, you can use thecan?
method in the controller or view to check the user's permission for a given action and object.can? :destroy, @project
cannot?
method is for convenience and performs the opposite check of can?
cannot? :destroy, @project
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 %>
true
. For example:can :read, Project, :priority => 3
can? :read, Project # returns true
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
However that can be tedious to apply to each action. Instead you can use the
This is the same as calling
As of CanCan 1.5 you can use the
Also see Controller Authorization Example, Ensure Authorization and Non RESTful Controllers.
You can specify which actions to affect using the
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.
If you want custom find options such as includes or pagination, you can build on this further since it is a scope.
The
Then the product will be built with that attribute in the controller.
This way it will pass authorization when the user accesses the
The attributes are then overridden by whatever is passed by the user in
If the model class is namespaced differently than the controller you will need to specify the
It is important that any custom loading behavior happens before the call to
Also see Nested Resources and Non RESTful Controllers.
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
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
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
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
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 adiscontinue
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
: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
load_resource
index action
As of 1.4 the index action will load the collection resource usingaccessible_by
.def index
# @products automatically set to Product.accessible_by(current_ability)
end
def index
@products = @products.includes(:category).page(params[:page])
end
@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 thiscan
definition.can :manage, Product, :discontinued => false
@product = Product.new(:discontinued => false)
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
: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 thanid
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 separatebefore_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
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
Addingauthorize_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 theload_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 anycan?
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
This exception can also be raised manually if you want more custom behavior.
The message can also be customized through internationalization.
Notice
You can catch the exception and modify its behavior in the
The action and subject can be retrieved through the exception to customize the behavior further.
The default error message can also be customized through the exception. This will be used if no message was provided.
If you prefer to return the 403 Forbidden HTTP code, create a
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.
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."
raise CanCan::AccessDenied.new("Not authorized!", :read, Article)
# 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."
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
exception.action # => :read
exception.subject # => Article
exception.default_message = "Default error message"
exception.message # => "Default error message"
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
This will add an
Here's another example where authorization is only ensured for the admin subdomain.
Note: The
check_authorization
to your ApplicationController
.class ApplicationController < ActionController::Base
check_authorization
end
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, thecheck_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
class ApplicationController < ActionController::Base
check_authorization :if => :admin_subdomain?
private
def admin_subdomain?
request.subdomain == "admin"
end
end
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.
The
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.
If your method that returns the currently logged in user just has another name than
That's it! See Accessing Request Data for a more complex example of what you can do here.
- You have an
Ability
class which defines the permissions. - You have a
current_user
method in the controller which returns the current user model.
current_ability
method in your ApplicationController
. The current method looks like this.def current_ability
@current_ability ||= Ability.new(current_user)
end
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
# 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
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
Translating your app
To use translations in your app define some yaml like this:
Enjoy!
# 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 useaction
(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}!"
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 acurrent_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
rails g cancan:ability
class ApplicationController < ActionController::Base
enable_authorization
end
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
enable_authorization
rescue_from CanCan::Unauthorized do |exception|
redirect_to root_url, :alert => exception.message
end
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 theAbility
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
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]
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 %>
Ability Precedence
In CanCan 2.0, there's a slight change in the way in which can rules are evaluated. A more specificcan
rule below a general can
rule will overwrite it. can :read, :projects
can :read, :projects, :title => 'Sir'
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 tocan
. 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
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
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
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 %>
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 tocan
.can :update, :projects, [:name, :priority]
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
<%= f.text_field :name if can? :update, @project, :name %>
Comentários
Postar um comentário