Rails Authentication with Devise and CanCan part 2 – Restful Resources for Administrators

About two months ago I wrote an article on getting started with Devise and CanCan. Since then, I’ve implemented the Devise + CanCan combo on three projects and wrote a couple specs for Ryan Bates to help improve CanCan functionality. This article will focus more on Devise with some CanCan sprinkled in there.

If you read my first article on Devise + CanCan, you have some questions on your mind. The most common question was how to create a RESTful interface for a super admin to CRUD users. This is a common requirement for managing users and/or keeping registration private. This is really easy.

I’m going to show you how to keep the awesome functionality provided by Devise while adding your custom stuff with no major time cost!

Adding custom functionality to a Devise enabled model is easy

Step 1 – Configure Routes
Since you probably want to keep your public interface for logging in and password recovery, leave your devise routes and add RESTful routes for users:

devise_for :users
resources :users

Step 2 – The User Controller
Next you need to set up your CRUD actions in the controller. Most of the actions are typical:

class UsersController < ApplicationController
  before_filter :get_user, :only => [:index,:new,:edit]
  before_filter :accessible_roles, :only => [:new, :edit, :show, :update, :create]
  load_and_authorize_resource :only => [:show,:new,:destroy,:edit,:update]
 
  # GET /users
  # GET /users.xml                                                
  # GET /users.json                                       HTML and AJAX
  #-----------------------------------------------------------------------
  def index
    @users = User.accessible_by(current_ability, :index).limit(20)
    respond_to do |format|
      format.json { render :json => @users }
      format.xml  { render :xml => @users }
      format.html
    end
  end
 
  # GET /users/new
  # GET /users/new.xml                                            
  # GET /users/new.json                                    HTML AND AJAX
  #-------------------------------------------------------------------
  def new
    respond_to do |format|
      format.json { render :json => @user }   
      format.xml  { render :xml => @user }
      format.html
    end
  end
 
  # GET /users/1
  # GET /users/1.xml                                                       
  # GET /users/1.json                                     HTML AND AJAX
  #-------------------------------------------------------------------
  def show
    respond_to do |format|
      format.json { render :json => @user }
      format.xml  { render :xml => @user }
      format.html      
    end
 
  rescue ActiveRecord::RecordNotFound
    respond_to_not_found(:json, :xml, :html)
  end
 
  # GET /users/1/edit                                                      
  # GET /users/1/edit.xml                                                      
  # GET /users/1/edit.json                                HTML AND AJAX
  #-------------------------------------------------------------------
  def edit
    respond_to do |format|
      format.json { render :json => @user }   
      format.xml  { render :xml => @user }
      format.html
    end
 
  rescue ActiveRecord::RecordNotFound
    respond_to_not_found(:json, :xml, :html)
  end
 
  # DELETE /users/1     
  # DELETE /users/1.xml
  # DELETE /users/1.json                                  HTML AND AJAX
  #-------------------------------------------------------------------
  def destroy
    @user.destroy!
 
    respond_to do |format|
      format.json { respond_to_destroy(:ajax) }
      format.xml  { head :ok }
      format.html { respond_to_destroy(:html) }      
    end
 
  rescue ActiveRecord::RecordNotFound
    respond_to_not_found(:json, :xml, :html)
  end
 
  # POST /users
  # POST /users.xml         
  # POST /users.json                                      HTML AND AJAX
  #-----------------------------------------------------------------
  def create
    @user = User.new(params[:user])
 
    if @user.save
      respond_to do |format|
        format.json { render :json => @user.to_json, :status => 200 }
        format.xml  { head :ok }
        format.html { redirect_to :action => :index }
      end
    else
      respond_to do |format|
        format.json { render :text => "Could not create user", :status => :unprocessable_entity } # placeholder
        format.xml  { head :ok }
        format.html { render :action => :new, :status => :unprocessable_entity }
      end
    end
  end
  ...
end

CanCan provides the class level accessible_by method that I am using to retrieve all users that can be viewed by the current user. The load_and_authorize_resource filter provided by CanCan actually does this accessibility filtering for you to keep things DRY and it recognizes collections as of this issue fix. I support sorting in my actual code so I didn’t want to use the CanCan filter to grab my user objects in the index action.

The other two filters – get_user and accessible_roles – are pretty basic:

  # Get roles accessible by the current user
  #----------------------------------------------------
  def accessible_roles
    @accessible_roles = Role.accessible_by(current_ability,:read)
  end
 
  # Make the current user object available to views
  #----------------------------------------
  def get_user
    @current_user = current_user
  end

Finally, respond_to_not_found is an application wide helper I use to respond when a requested object is not found. I stole it from the Fat Free CRM source code.

Next we’ll look at the Update action which has some non standard code.

  # PUT /users/1
  # PUT /users/1.xml
  # PUT /users/1.json                                            HTML AND AJAX
  #----------------------------------------------------------------------------
  def update
    if params[:user][:password].blank?
      [:password,:password_confirmation,:current_password].collect{|p| params[:user].delete(p) }
    else
      @user.errors[:base] << "The password you entered is incorrect" unless @user.valid_password?(params[:user][:current_password])
    end
 
    respond_to do |format|
      if @user.errors[:base].empty? and @user.update_attributes(params[:user])
        flash[:notice] = "Your account has been updated"
        format.json { render :json => @user.to_json, :status => 200 }
        format.xml  { head :ok }
        format.html { render :action => :edit }
      else
        format.json { render :text => "Could not update user", :status => :unprocessable_entity } #placeholder
        format.xml  { render :xml => @user.errors, :status => :unprocessable_entity }
        format.html { render :action => :edit, :status => :unprocessable_entity }
      end
    end
 
  rescue ActiveRecord::RecordNotFound
    respond_to_not_found(:js, :xml, :html)
  end

That was easy!

The only unusual code we added is to clean up the password request params if the user’s password field is blank, and validate the password with Devise’s valid_password? method if it’s not blank. That allows us to provide a UI that looks like this:
User interface for editing a user with RESTful additions to the Rails Devise authentication gem
Step 3 – Add you views
It may seem a bit redundant to show all of my view code, but I’m going to do it anyway so you can see how I’m using CanCan everywhere.

index.html.erb

<!-- table header stuff here -->
<% @users.each do |u| %>
  <tr>
    <td><%= link_to_if(can?(:read, User), "#{u.name}", user_path(u.id)) {} %></td>
    <td><%= "#{u.email}" %></td>
    <td><%= "#{u.roles.collect{|r| r.name}}" %></td>
    <td><%= link_to_if(can?(:edit, User), image_tag("/images/edit_icon.gif"), edit_user_path(u.id)) {} %></td>
    <td><%= link_to_if(can?(:delete, u), image_tag("/images/delete_icon.gif"), u, :confirm => "Are you sure?", :method => :delete) {} %></td>
  </tr>
<% end %>

Note the heavy use of link_to_if(can?(:method, object), innerHTML, path, options) . It’s nice to only display links to those who can actually use them.

User registration – new.html.erb

<h2>Register User</h2>
 
<%= form_for(@user) do |f| %>
  <%= error_messages(@user,"Could not register user") %>
 
  <%= render :partial => 'user_fields', :locals => { :f => f } %>
 
  <p><%= f.label :password %></p>
  <p><%= f.password_field :password %></p>
 
  <p><%= f.label :password_confirmation %></p>
  <p><%= f.password_field :password_confirmation %></p>
 
  <p><%= f.submit "Register" %></p>
<% end %>

Where the partial – _user_fields.html.erb is the following:

<p><%= f.label :first_name %></p>
<p><%= f.text_field :first_name %></p>
 
<p><%= f.label :last_name %></p>
<p><%= f.text_field :last_name %></p>
 
<p><%= f.label :email %></p>
<p><%= f.text_field :email %></p>
 
<% if can? :read, Role %>
	<p><%= f.label :role %></p>
	<ul class="no-pad no-bullets">
		<%= habtm_checkboxes(@user, :role_ids, @accessible_roles, :name) %>
	</ul>
<% end %>

This is quite similar to the Devise generated views except we are not using a general “resource” object, we are specifying @user. Owning our views gives us the ability to easily add custom fields.

edit.html.erb

<h3><%= @user == @current_user ? "Your Account Settings" : "Edit User" %></h3>
 
<%= form_for(@user, :html => { :method => :put }) do |f| %>
	<%= error_messages(@user,"Could not update user") %>
	<%= render :partial => 'user_fields', :locals => { :f => f } %>
 
	<p><%= f.label :password %> <i>(leave blank if you don't want to change it)</i></p>
	<p><%= f.password_field :password %></p>
 
	<p><%= f.label :password_confirmation %></p>
	<p><%= f.password_field :password_confirmation %></p>
 
	<p><%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i></p>
	<p><%= f.password_field :current_password %></p>
 
  <p><%= f.submit "Update" %></p>
<% end %>
<%= link_to "Back", :back %>

show.html.erb

<h3><%= @user.name %></h3>
 
<%= link_to_if(can?(:update,@user), "Edit", edit_user_path(@user)) %> |
<%= link_to_if(can?(:delete, @user), "Delete", user_path(@user), :confirm => "Are you sure?", :method => :delete) {} %>
 
<table class="one-column-emphasis">
	<tbody>
		<tr>
			<td class="oce-first">Email:</td>
			<td><%= @user.email %></td>
		</tr>
		<tr>
			<td class="oce-first">Role:</td>
			<td><%= @user.roles.first.name %></td>
		</tr>
	<% if can?(:see_timestamps,User) %>
		<tr>
			<td class="oce-first">Created at:</td>
			<td><%= @user.created_at %></td>
		</tr>
		<tr>
			<td class="oce-first">Last Sign In:</td>
			<td><%= @user.last_sign_in_at %></td>
		</tr>
		<tr>
			<td class="oce-first">Sign In Count:</td>
			<td><%= @user.sign_in_count %></td>
		</tr>
	<% end %>
	</tbody>
</table>

Wait…WTF is “see_timestamps” ???

This is one thing I love about CanCan – it’s easy to add arbitrary permissions. In my CanCan ability class I can have:

#------------------------------------------------
def initialize(user)
    user ||= User.new # guest user
 
    if user.role? :admin
      can :see_timestamps, User
    elsif user.role? :normal
      can :see_timestamps, User, :id => user.id
    end
end

I’ll admit that in my actual code I’m showing the timestamps to users that can? :manage, :all but you get the idea.

Between this post and my first post on Devise and CanCan, you should be rocking out.
Rockin out with Devise and CanCan ...or a fake guitar

No TweetBacks yet. (Be the first to Tweet this post)
Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google
  • MySpace
  • Slashdot
  • StumbleUpon
  • Technorati
  • TwitThis

If you enjoyed this post, make sure you subscribe to my RSS feed!

This entry was posted in Software and tagged , , , , , , , , , . Bookmark the permalink. Both comments and trackbacks are currently closed.

66 Comments

  1. Hans
    Posted April 13, 2011 at 5:56 am | Permalink

    This works great. Can you demonstrate how to create a multi-select for the user’s user_role

  2. deco81
    Posted May 12, 2011 at 5:12 pm | Permalink

    Good stuff!
    I would like to see how you implemented unit and functional tests :)

  3. Posted July 2, 2011 at 12:14 pm | Permalink

    Hi, I was wondering what your habtm_checkboxes helper looks like? Thanks!

  4. Cynthia
    Posted December 25, 2011 at 1:53 pm | Permalink
  5. Kris Utter
    Posted January 5, 2012 at 10:56 pm | Permalink

    Nice tutorial. I keep getting one error thorugh, undefined method `error_messages’. Is there a method missing?

  6. @Kris Utter
    Posted January 25, 2012 at 9:34 am | Permalink

    to resolve the error_messages issue.in rails 3.1.3, I copied a file named error_messages_helper.rb in the /app/helpers/. Putting a copy of the content of that file here as well.

    module ErrorMessagesHelper
    # Render error messages for the given objects. The :message and :header_message options are allowed.
    def error_messages_for(*objects)
    options = objects.extract_options!
    options[:header_message] ||= I18n.t(:”activerecord.errors.header”, :default => “Invalid Fields”)
    options[:message] ||= I18n.t(:”activerecord.errors.message”, :default => “Correct the following errors and try again.”)
    messages = objects.compact.map { |o| o.errors.full_messages }.flatten
    unless messages.empty?
    content_tag(:div, :class => “error_messages”) do
    list_items = messages.map { |msg| content_tag(:li, msg) }
    content_tag(:h2, options[:header_message]) + content_tag(:p, options[:message]) + content_tag(:ul, list_items.join.html_safe)
    end
    end
    end

    module FormBuilderAdditions
    def error_messages(options = {})
    @template.error_messages_for(@object, options)
    end
    end
    end

    ActionView::Helpers::FormBuilder.send(:include, ErrorMessagesHelper::FormBuilderAdditions)

  7. George Wu
    Posted March 9, 2012 at 10:03 pm | Permalink

    @Tony, I followed both #1 and #2 tutorial as one whole complete app. all is well except I ran into a problem towards the end.

    The problem is that when a super_admin user try to create a new user. I got error:

    Started POST "/users" for 127.0.0.1 at 2012-03-09 23:37:51 -0500
      Processing by RegistrationsController#create as HTML
      Parameters: {"utf8"=>"✓", "authenticity_token"=>"c8v6fmCFSlJV2v9qClxD46c1wcBU7n78Mk9xWsJm/Ls=", "user"=>{"email"=>"test@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "customer_attributes"=>{"first_name"=>"test", "last_name"=>"doe"}, "role_ids"=>["", "3"]}, "commit"=>"Sign up"}
    Completed   in 9ms
    
    NameError (uninitialized constant Registration):
    

    What I can figured out is the route conflict. tutorial #1, use devise custom registrations controller for registering new user. tut#2, added another way to create a new user by super_admin. Below is route listing.

    cancel_user_registration GET    /users/cancel(.:format)             {:action=>"cancel", :controller=>"registrations"}
           user_registration POST   /users(.:format)                    {:action=>"create", :controller=>"registrations"}
       new_user_registration GET    /users/register(.:format)           {:action=>"new", :controller=>"registrations"}
      edit_user_registration GET    /users/edit(.:format)               {:action=>"edit", :controller=>"registrations"}
                             PUT    /users(.:format)                    {:action=>"update", :controller=>"registrations"}
                             DELETE /users(.:format)                    {:action=>"destroy", :controller=>"registrations"}
           user_confirmation POST   /users/confirmation(.:format)       {:action=>"create", :controller=>"devise/confirmations"}
       new_user_confirmation GET    /users/confirmation/new(.:format)   {:action=>"new", :controller=>"devise/confirmations"}
                             GET    /users/confirmation(.:format)       {:action=>"show", :controller=>"devise/confirmations"}
                       users GET    /users(.:format)                    {:action=>"index", :controller=>"users"}
                             POST   /users(.:format)                    {:action=>"create", :controller=>"users"}
                    new_user GET    /users/new(.:format)                {:action=>"new", :controller=>"users"}
                   edit_user GET    /users/:id/edit(.:format)           {:action=>"edit", :controller=>"users"}
                        user GET    /users/:id(.:format)                {:action=>"show", :controller=>"users"}
                             PUT    /users/:id(.:format)                {:action=>"update", :controller=>"users"}
                             DELETE /users/:id(.:format)                {:action=>"destroy", :controller=>"users"}
    

    note matching http verb Post /users and user_registrations path was matched first.

                             POST   /users(.:format)                    {:action=>"create", :controller=>"users"}
    
           user_registration POST   /users(.:format)                    {:action=>"create", :controller=>"registrations"}
    

    But don’t know what is the best way to avoid this conflict. Can you shed some light on this? I could not figure out from your tutorial.

    Thanks in advance,

    George

  8. George Wu
    Posted March 9, 2012 at 10:17 pm | Permalink
  9. Laetitia Duby
    Posted March 16, 2012 at 6:01 am | Permalink

    @Tony : Thank you so much for those 2 tutorials. It was a great help for me to implement the base app of a future quality management tool. I needed a database RBAC and the Devise/Cancan combo works perfectly for me. Without your detailed instructions, I think I would still be trying to make it all work together.

    Many thanks again.
    Laetitia

  10. Posted April 19, 2012 at 7:45 pm | Permalink

    Where is the code for this method error_messages() in the edit.html.erb view?

  11. Posted April 21, 2012 at 7:09 am | Permalink

    Regarding the use of error_messages() handler in these views, I see that in Rails 3+, this been removed as a base Rails helper, and they just wany you to handle it right in the view with your own html output.

    Here is a link with the reasons it was removed, and sample code you can use in yours views instead: http://www.suffix.be/blog/error-messages-for-rails3

    Also, here is a link where someone has written a replaced helper so you can still make a help call in your views: http://www.emersonlackey.com/article/rails3-error-messages-for-replacement

  12. Posted April 21, 2012 at 7:13 am | Permalink

    Regarding the habtm_checkboxes() helper in the partial view _user_fields.html.erb, here is the code you can use to create that helper method:

    https://github.com/jtrupiano/habtm_checkboxes/blob/master/lib/habtm_checkboxes.rb

    Just add this to you addp/helper directory as a separate file, or add it to the existing /app/helpers/application_helper.rb file.

  13. Posted May 2, 2012 at 7:42 pm | Permalink

    Thanks for the insight, Matt. I’ve been super busy

  14. Paul
    Posted May 14, 2012 at 10:18 pm | Permalink

    Thanks Tony, your post really shed a lot of insight on how Devise and CanCan work together.

    Would you be willing to share how you test your controllers though? Perhaps how you would set it up with fixtures like FactoryGirl (or whatever you use)? I understand you’re very busy, but it would be so awesome to see how you do it. Thanks again!

  15. Adam Stockland
    Posted August 21, 2012 at 7:47 am | Permalink

    Im curious about how the object is used in cancan. In your example you mixed up the object. Is there a difference between can? :read, User vs can? :read, u? It seems that User would be “can this person read users” and u would be “can this user read this user”.

    In the code below, from the example above… Why didnt you user u as the object for :read and :edit, but you did for :delete?

    Thanks
    Adam

    “Are you sure?”, :method => :delete) {} %>

  16. Ravi
    Posted November 20, 2012 at 1:22 pm | Permalink

    Edit

    resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %>

    (leave blank if you don’t want to change it)
    “off” %>

    (we need your current password to confirm your changes)

    Cancel my account

    Unhappy? { :confirm => “Are you sure?” }, :method => :delete %>.

    I am having this code but it’s not display role check box tag an sign up page….

3 Trackbacks

  1. [...] A part 2 of this post is now available No TweetBacks yet. (Be the first to Tweet this post) Share and Enjoy: [...]

  2. By Internet Schminternet » Devise and Cancan on August 2, 2011 at 3:56 pm
  3. [...] devise + cancanhttp://www.tonyamoyal.com/2010/09/29/rails-authentication-with-devise-and-cancan-part-2-restful-reso… [...]