homeASCIIcasts

192: Authorization with CanCan 

(view original Railscast)

Other translations: Es It Pt

A few episodes ago we covered Declarative Authorization. While it is an excellent authorization plugin for Rails it can be a little heavy for simpler sites. After writing the Railscast on Declarative Authorization Ryan Bates looked for an alternative solution and, failing to find one that suited his needs, decided to write his own, CanCan. CanCan is a simple authorization plugin and Ryan has tried to keep everything as simple as he could when writing it. Let’s dive in and take a look at it.

In this episode we’ll be working with the same basic blog application that we used in the Declarative Authorization episode. The application has a number of articles, each of which belongs to a user and each article can have many comments associated with it.

The article page in our application.

Notice in the screenshot above that although there is no logged-in user the links to edit and destroy articles and comments are visible. We want to use authorization to restrict access to what each user can do. We already have authentication in our application so that users can sign up and log in and we’ve used Authlogic to do this, although any authentication solution will work.

On the sign-in page we have a number of checkboxes to allow a user to select the roles they want to be a member of. An admin will be allowed to do everything; a moderator can edit anyone’s comments and an author can create articles and update the articles they have written. Users who don’t belong to any role can create comments and update those comments.

The signup page showing the roles checkboxes.

Installing CanCan

CanCan is supplied as a gem. To add it to our application we need to add the following line in the Rails::Initializer.run block of our /config/environment.rb file.

config.gem "cancan"

We can then make sure that the gem is installed by running

sudo rake gems:install

If you installed an earlier version of CanCan you’ll need to make sure that you’ve upgraded to the latest version as there are some features that are only available since version 1.0.0.

Using CanCan

To use CanCan we need to create a new class called Ability, which we’ll place in our /app/models directory. The class needs to include the CanCan::Ability module and also an initialize method that takes a user object as a parameter and it’s in this method that we’ll define the abilities for each type of user.

class Ability
  include CanCan::Ability
  def initialize(user)
  end
end

The abilities are defined with the three-letter method can which is at the heart of CanCan. This method takes two parameters: the first is the action that we want to perform and the second is the model class that the action applies to. Alternatively, to apply an action to all models we can pass :all. If we want all users to just be able to read all models we can do this:

class Ability
  include CanCan::Ability
  def initialize(user)
    can :read, :all
  end
end

As our authorization stands no one can edit or delete articles or comments but there are still links to the edit and destroy actions on each article’s page. In the view code for the article’s show action we can use can? (note the question mark) to determine whether the current user is authorized to perform the action that each link links to. While the can method defines the abilities, can? is a boolean method that determines whether the current user has that ability. Like can, can? takes two parameters, an action and a model, in this case an Article. We’ll use can? in the show view so that the edit and destroy links are hidden unless the current user has the appropriate ability. To do this we’ll wrap the articles links in if statements so that they’re only shown if the current user can perform the appropriate action on an article.

<p>
  <% if can? :update, @article %>
    <%= link_to "Edit", edit_article_path(@article) %> |
  <% end %>
  <% if can? :destroy, @article %>
    <%= link_to "Destroy", @article, :method => :delete, :confirm => "Are you sure?" %> |
  <% end %>
  <%= link_to "Back to Articles", articles_path %>
</p>

Lower down in the same view code we’ll make a similar change to each comment link.

<p>
  <% if can? :update, comment %>
    <%= link_to "Edit", edit_comment_path(comment) %> |
  <% end %>
  <% if can? :destroy, comment %>
    <%= link_to "Destroy", comment, :method => :delete, :confirm => "Are you sure?" %>
  <% end %>
</p>

We can load the page for an article now and see that the links to edit or delete articles and comments have gone as there are no users with the ability to edit or alter them.

The links are now hidden.

Protecting the Controllers

Although we’ve removed the links to edit articles and comments the actions themselves are still available and if we directly visit the edit action for an article we can still update it. So as well as making changes to the view layer we’ll need to modify our controllers so that users can only access the actions they are authorized to. There are two ways to do this with CanCan. The first works at the level of an action and we’ll use the ArticleController’s edit action as an example.

def edit
  @article = Article.find(params[:id])
  unauthorized! if cannot? :edit, @article
end

To stop the action being executed we call unauthorized! which will raise an exception. Obviously we only want this exception raised if the user does not have the appropriate authorization. To check this we can use can? as we did in the view or, as we have here, cannot? to check the authorization.

If we try to access the edit action directly now we’ll be stopped from doing so and an error will be raised.

An exception will be raised if an attempt to access an unauthorized action is made.

We could repeat this across every action in our controllers, but as we’re using the RESTful convention there’s an easier way to do this. At the top of the controller we can call load_and_authorize_resource which will load and authorize the appropriate resource in a before filter. As this method loads the necessary resource for us based on the action we can remove the lines of code that set the instance variable in each action (in this case @article) making our ArticlesController code look like this:

class ArticlesController < ApplicationController
  load_and_authorize_resource
  def index
    @articles = Article.all
  end
  def show
    @comment = Comment.new(:article => @article)
  end
  def new
  end
  def create
    @article.user = current_user
    if @article.save
      flash[:notice] = "Successfully created article."
      redirect_to @article
    else
      render :action => 'new'
    end
  end
  def edit
  end
  def update
    if @article.update_attributes(params[:article])
      flash[:notice] = "Successfully updated article."
      redirect_to @article
    else
      render :action => 'edit'
    end
  end
  def destroy
    @article.destroy
    flash[:notice] = "Successfully destroyed article."
    redirect_to articles_url
  end
end

With this in place users won’t be able to create edit or delete any articles.

We’ll need to make a similar change to the CommentsController to restrict access to its actions too. Once again we’ll use load_and_authorize_resources to load the Comment resource and check the authorization for it. If Comment was a nested resource under Article in the routes we could use :nested with load_and_authorize_resources to load the comments through the Article resource.

load_and_authorize_resource :nested => :article

We’re not using nesting here, however, so we don’t need to do this.

Adding Abilities

Now that our application is secure we can start to define the abilities that each role will have. This is done back in the Ability class we created earlier. The abilities we define in the initialize method will be reflected through the rest of our application.

class Ability
  include CanCan::Ability
  def initialize(user)
    can :read, :all
  end
end

We pass in the current user to initialize so we can change the abilities according to the currently logged-in user. We’ll start with the users in the administrator role who should be able to manage everything.

The user passed to initialize can be an object of any type which means that the authentication is completely decoupled from the authorization. What defines a user as, say, an administrator entirely depends on the authentication system used. We might, for example, have an admin? boolean field in our User model. In our application a user can have many roles and we’ll have a role? method to tell us if a user is a member of a role. We’ll use that method to set the abilities.

class Ability
  include CanCan::Ability
  def initialize(user)
    if user.role? :admin
      can :manage, :all
    else
      can :read, :all
    end
  end
end

Our code now checks to see if the current user is an admin and if so allows them to manage all models. Passing :manage as an action means that the user can perform all actions on a model.

We still need to define the role? method in the User model. We’ve set up in the roles here in the same way we did in Episode 189 [watch, read] but it doesn’t matter how you set up your roles as long as you can determine which role or roles a user belongs to.

class User < ActiveRecord::Base
  acts_as_authentic
  has_many :articles
  has_many :comments
  named_scope :with_role, lambda { |role| {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0 "} }
  ROLES = %w[admin moderator author editor]
  def roles=(roles)
    self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum
  end
  def roles
    ROLES.reject { |r| ((roles_mask || 0) & 2**ROLES.index(r)).zero? }
  end
  def role?(role)
    roles.include? role.to_s
  end
end

The role? method we’ve added here checks that the role passed is included in the user’s roles.

Back in the Ability class we need to make one more change. In initialize we’re checking to see if the user belongs to a role but for guest users, i.e. users who have not yet logged in, user will be nil. We could check for nil before trying to check the user’s role but instead we’ll create a guest user if the user passed in is nil. This way we can still call methods like role? for users who have not yet set up an account.

class Ability
  include CanCan::Ability
  def initialize(user)
    user ||= User.new # Guest user
    if user.role? :admin
      can :manage, :all
    else
      can :read, :all
    end
  end
end

If we go back to our application again we still won’t be able to edit or destroy comments, but if we log in as a user in the admin role then the links will be shown.

The links are visible again for administrators.

Having got authorization working for administrators we now need to set up the abilities for the other roles. We’ll start with guest users, those who have no roles assigned to them. These should be able to create comments and update comments that they have written. To do this we’ll modify our Ability class thus:

class Ability
  include CanCan::Ability
  def initialize(user)
    user ||= User.new
    if user.role? :admin
      can :manage, :all
    else
      can :read, :all
      can :create, Comment
      can :update, Comment do |comment|
        comment.try(:user) == user
      end
    end
  end
end

Writing the code to enable guest users to create comments is straightforward but the update code is a little trickier as users should only be able to update comments they have written. To do this we pass can a block which will pass in the instance of the model we’re checking. The block should return true or false depending on whether the action should be allowed so in the block we’ll check that the comment’s user is the current user. There’s a possibility that the comment might be nil so we’ll use Rails’ try method to read the user attribute so that nil is returned if the comment is nil instead of an exception being raised.

If we log in as a user who has no roles now we can add a comment and update it but not the comments made by anyone else.

Guest users can edit only their own comments.

Next we’ll modify the code to add the abilities for moderators. Moderators should be able to modify any comment so we’ll update the update comment ability to allow this.

can :update, Comment do |comment|
  comment.try(:user) == user || user.role?(:moderator)
end

We have one more role left to cover, :author. Authors should be able to create articles and modify any articles that they have written. To add these abilities we just need to add the following code to the Ability class.

class Ability
  include CanCan::Ability
  def initialize(user)
    user ||= User.new
    if user.role? :admin
      can :manage, :all
    else
      can :read, :all
      can :create, Comment
      can :update, Comment do |comment|
        comment.try(:user) == user || user.role?(:moderator)
      end
      if user.role?(:author)
        can :create, Article
        can :update, Article do |article|
          article.try(:user) == user
        end
      end
    end
  end
end

As we did with guest users and comments we pass the current article to a block in the update article ability and check that the article’s user is the current user.

Now we have all of our abilities defined for each user role. The nice thing about CanCan is that it allows us to define all of the abilities in one location and the rest of the application will reflect these changes.

A Prettier Error Page

If a user calls an action that they don’t have access to they will see a rather ugly error page showing an AccessDenied exception. We can change this so that they see a better-looking custom error page instead.

Rails provides a method called rescue_from that we can place in our ApplicationController. We pass it an exception and pass it either a method or a block. We’ll pass a block and inside it make the application show a flash error message and redirect to the home page.

rescue_from CanCan::AccessDenied do |exception|
  flash[:error] = "Access denied!"
  redirect_to root_url
end

If a user without roles now tries to edit an article by typing the URL in directly they’ll be redirected to the home page and told that they can’t do that.

An flash message is shown if an attempt to access an unauthorized action is made.

That’s it for this episode. For more details or to report an issue visit Ryan’s GitHub page for the project.