Search code examples
ruby-on-railsdevisecancanrolify

Devise / Rolify / CanCan 2.0: Preventing users from changing a user's role


I am using Devise for authentication, Rolify for role management and CanCan 2.0 for authorization.

I am trying to allow the :admin role to change a user's roles, but disallow all other users access.

Here is what I have tried and is not working:

#ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    if user.has_role? :admin
      can :access, :all
    elsif user.has_role? :moderator
      can [:index, :read, :update, :destroy], :users, :user_id => user.id
      cannot :access, :users, [:role_ids]
    end
end

#application_controller.rb
...
rescue_from CanCan::Unauthorized do |exception|
    redirect_to root_url, :alert => exception.message
  end

I have intentionally left the association in my user form:

#_form.html.erb
<%= simple_form_for @user do |f| %>
  <%= f.association :roles, as: :check_boxes %>
  <%#= f.association :roles, as: :check_boxes if can? :update, @user, :roles %>
  <%= f.button :submit %>
<% end %>

controller

#users_controller.rb
class UsersController < ApplicationController

  before_filter :authenticate_user!
  load_and_authorize_resource

  def index
    @users = User.accessible_by(current_ability)
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(params[:user])
  end

  def show
    @user = User.find(params[:id])
  end

  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])

    @user.update_without_password(params[:user])

    if successfully_updated
      redirect_to @user
    else
      render :action => "edit"
    end
  end
end

and the model:

#user.rb
    class User < ActiveRecord::Base
      rolify

      attr_accessible :role_ids
    ...

Now if a user who has the role of :moderator tries to change another user's (or his own) roles, here is what happens:

  1. A CanCan::Unauthorized exception is thrown and the user is redirected to root_url
  2. The roles are changed for the user

I am confused. If the exception happens, why are the changes still made? I am probably doing something very wrong :)

I have tried manipulating the query params depending on a users role in users_controller.rb If I put a log statement right after def update, here is my output:

2013-04-24 12:42:21 [4161] DEBUG    (0.1ms)  BEGIN
2013-04-24 12:42:21 [4161] DEBUG    (0.3ms)  INSERT INTO "users_roles" ("user_id", "role_id") VALUES (5, 1)
2013-04-24 12:42:21 [4161] DEBUG    (0.4ms)  COMMIT
2013-04-24 12:42:21 [4161] DEBUG   User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", "5"]]
2013-04-24 12:42:21 [4161] DEBUG {"username"=>"Blabla", "email"=>"bla@bla.com", "password"=>"", "password_confirmation"=>"", "approved"=>"1", "role_ids"=>["1", "2", ""]}

I must be overlooking something...


Solution

  • I ended up using a before_filter, like this:

    before_filter :prevent_unauthorized_role_setting, :only => [ :create, :update ]
    
    def prevent_unauthorized_role_setting
      if cannot? :manage_roles, current_user
        params[:user].delete_if { |k, v| k.to_sym == :role_ids }
      end
    end
    

    while following Zaid's suggestion in ability.rb:

    cannot :manage_roles, :users
    if user.has_role? :admin
      can :manage_roles, :users
    end
    

    Also, I dropped Rolify and managed roles on my own.