Rails 3 Authentication and Authorization with Devise and CanCan (Part 2)

by iqbalfarabi on Feb 25, 2011 | 19 Comments
RubyOnRails_2

This article is also published in Iqbal’s blog here.


This is the second part of two posts tutorial on how to use Devise and CanCan with Rails 3. You can find the first part of this tutorial at the original blog here or at this site here.

Where were we?

In the previous part, we have used Devise library to create our User model in CRUD style. With what we have coded in the previous part, you should now be able to manage users and authenticate them. Now it’s time to add the roles and define what features they may access.

1. Adding the Roles

This step is pretty traditional, we can just scaffold all the MVC part for our roles. In this article, I only need one attribute that is the name of the role.

[cc lang="bash"]
rails generate scaffold Role name:string
[/cc]

2. Making HABTM relationship between roles and users

We have to do several tasks in this step. The first thing is to manually create the migration file for the habtm table. We do it by first generating the migration file like this:

[cc lang="bash"]
rails generate migration UsersHaveAndBelongToManyRoles
[/cc]

and then we fill the file with this code:

[cc lang="ruby"]
class UsersHaveAndBelongsToManyRoles < ActiveRecord::Migration
def self.up
create_table :roles_users, :id => false do |t|
t.references :role, :user
end
end

def self.down
drop_table :roles_users
end
end
[/cc]

And do not forget to modify our model files. This is how our role.rb looks like after we modify it:

[cc lang="ruby"]
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
end
[/cc]

And this is how our user.rb file looks like. Please be noted that we also add :role_ids in the attr_accessible and we add a method to identify the role of the respective user.

[cc lang="ruby"]
class User < ActiveRecord::Base
has_and_belongs_to_many :roles

# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable, :lockable and :timeoutable
devise :database_authenticatable,
:recoverable, :rememberable, :trackable, :validatable

# Setup accessible (or protected) attributes for your model
attr_accessible :email, :username, :password, :password_confirmation, :remember_me, :role_ids

def role?(role)
return !!self.roles.find_by_name(role.to_s.camelize)
end
end
[/cc]

In the views part, we have to modify at least the _form.html.erb from our users view so that we can assign the role to the user from web interface. This is how the file users/_form.html.erb looks like:

[cc lang="ruby"]
<%= form_for(@user) do |f| %>
<% if @user.errors.any? %>

<%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

    <% @user.errors.full_messages.each do |msg| %>

  • <%= msg %>
  • <% end %>

<% end %>

<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :username %>
<%= f.text_field :username %>

<% if @current_method == "new" %>

<%= f.label :password %>
<%= f.password_field :password %>
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation %>

<% end %>
<% for role in Role.find(:all) %>

<%= check_box_tag "user[role_ids][]", role.id, @user.roles.include?(role) %>
<%= role.name %>

<% end %>

<%= f.submit %>

<% end %>
[/cc]

Of course you can also modify your index.html.erb and show.html.erb files to show the role of the listed user(s). But I think I leave it to you to do it yourself. After all these tasks, we can run rake task db:migrate.

[cc lang="bash"]
rake db:migrate
[/cc]

In this state, we can now add users and assign them with the roles we desire. But of course each role has not yet identified with any permission by CanCan. That’s what we’re going to do in the next step.

3. Adding ability

To use CanCan, we have to make a class that defines the permissions that are given to each role. In CanCan, this is called as “ability” and to set it up, we have to create the ability.rb file as part of our models.

[cc lang="ruby"]
class Ability
include CanCan::Ability

def initialize(user)
user ||= User.new # guest user

if user.role? :administrator
can :manage, :all
elsif user.role? :operator
can :manage, Post
else
can :read, :all
end
end
end
[/cc]

CanCan use a very pleasant way to describe our roles and it’s corresponding allowed features. In our code above, we use the keyword “can” and then follow it with the level of authorization we want the respective role to have and at last we end it with the name of the model. The level of authorizations that I have tried so far is :create, :read, :update, :delete, and :manage. Maybe there is a way to define another ability, you can find about it in depth in the official Github page of CanCan library here.

4. Adding authorization

Finally, in every controller that needs authorization, we add this line:

[cc lang="ruby"]
load_and_authorize_resource
[/cc]

Final Words

I think that’s it for now. I know I only have covered the basic part of using Devise and CanCan but I hope this can be helpful for anyone that just starts to learn about both libraries. I may add a working source code to this tutorial later. Meanwhile, if you have any trouble following this tutorial, you can ask me in the comment section.

Back to top

19 Responses to “Rails 3 Authentication and Authorization with Devise and CanCan (Part 2)”

  1. Tom Scott says:

    Hi There,
    It was useful looking through this tutorial. I am developing a system at the moment which has 3 main roles.. I am wanting to use one user form for this using devise (already installed). Can I restrict access to certain actions for my page controller?? E.g. prevent access to a certain page if your not a specified role e.g. admin rather than a normal user. Thanks.

  2. admin says:

    Hi, Tom Scott!

    Yes, you can restrict certain actions for your controller like you wish. You just need to add them in your ability.rb file. Let say you have this action called “random_method” in RandomModel, you can just put your ability.rb this way:


    class Ability
    include CanCan::Ability

    def initialize(user)
    user ||= User.new # guest user

    if user.role? :administrator
    can :manage, :all
    can :random_method, Random
    else
    can :read, :all
    end
    end
    end

    Hope you find this helpful. :)

  3. Rajeev says:

    uninitialized constant UsersHaveAndBelongToManyRoles
    getting error please help

    • admin says:

      Hi Rajeev,

      Can you post the complete error message you received? Maybe we can help.

      • matt says:

        There seems to be a spelling mistake…

        you create a migration called : UsersHaveAndBelongToManyRoles

        Then your class is : UsersHaveAndBelongsToManyRoles
        which has an extra “s” on belong

      • junlai says:

        Hi, the reason for this problem is the error migration name “UsersHaveAndBelongsToManyRoles ” in your tutorial. The right name is “UsersHaveAndBelongToManyRoles”.

    • Jeff Boshers says:

      Rajeev,

      Rails is freaking out because you have named the migration ending in a plural by default it’s looking for a singular form model and is not finding it causing the error.

      just do this…

      rails generate migration UsersHaveAndBelongToMany leave off the plural word roles.

    • nilid says:

      rails generate migration UsersHaveAndBelong()ToManyRoles

      class UsersHaveAndBelong(s)ToManyRoles < ActiveRecord::Migration

    • Jay says:

      Hey, the problem comes from
      rails generate migration UsersHaveAndBelongToManyRoles
      and
      class UsersHaveAndBelongsToManyRoles < ActiveRecord::Migration

      see the different ?
      Tips: belong and belongs

    • lanlau says:

      Hi, the problem comes from the migration file name, the “rails generate migration UsersHaveAndBelongToManyRoles” command has to be replaced by
      “rails generate migration UsersHaveAndBelongsToManyRoles”
      (an “s” is missing in Belongs)

  4. sathish says:

    hi i have error in second part of db:migrate like this

    rake db:migrate
    rake aborted!
    An error has occurred, all later migrations canceled:

    uninitialized constant UsersHaveAndBelongToManyRoles

    Tasks: TOP => db:migrate
    (See full trace by running task with –trace)

    what i do help me

  5. David says:

    Hi I have followed this tutorial to what I believe I think is good, as I have been searching everywhere for a tutorial. However at the end of this tutorial I recieved the following error:

    LoadError in UserController#new

    Expected C:/Users/####/Documents/devise/app/controllers/user_controller.rb to define UserController. If you could comment back as soon as possible that would be most grateful. Cheers

  6. Ryan says:

    I followed the tutorial step-by-step and am having problems. No matter what role I check on the user page the user is still just a general user. I don’t think anything is saving in the Roles_Users database. When I add load_and_authorize_resource to the top of my pages no matter if I have administrator checked or nothing it says I do not have authorization.

  7. Jesse says:

    Using rails 3.1
    Everything went fine until I added the first role, “admin”

    then I got the following errors:

    undefined method `roles’ for #

    Rails.root: C:/Ruby192/projects/aaaRailsEssentials/cancan/devise-railscasts-two
    Application Trace | Framework Trace | Full Trace

    app/models/user.rb:13:in `role?’
    app/models/ability.rb:5:in `initialize’

    can’t figure out what Im doing wrong

  8. raghu varma says:

    Hi,
    I have done LDAP authentication with devise successfully. Can you say how to import roles from LDAP and do authorization on them instead of creating new roles.
    Thanks,
    Raghu

  9. ankit says:

    my sign_up is also not working
    ActiveRecord::RecordNotFound in UsersController#show

    Couldn’t find User with id=sign_up

    Rails.root: /home/kipl/railsapp/cancan_app
    Application Trace | Framework Trace | Full Trace

    app/controllers/users_controller.rb:16:in `show’

    Request

    Parameters:

    {“id”=>”sign_up”}

    Show session dump

    Show env dump
    Response

    Headers:

    None

  10. japal says:

    hi,
    is it possible download the project?

    Thank

  11. Fred says:

    attr_accessible is given a reference to roles_ids.
    What is roles_ids? Is this another case of forced pluralization?

Leave a Reply