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

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

I am trying to allow: admin roles to change user roles, but deny access to all other users.

Here is what I tried and it doesn't work:

#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 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 model:

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

Now, if the user with the role: moderator is trying to change the role of another user (or his own), this is what happens:

  • CanCan called :: Unauthorized exception and user is redirected to root_url
  • Roles changed for user

I am embarrassed. If an exception occurs, why are the changes still made? I'm probably doing something really bad :)

I tried to manipulate the request parameters depending on the user role in users_controller.rb. If I set the log statement immediately after def was updated, here is my conclusion:

 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 have to ignore something ...

+4
source share
4 answers

I ended up using the before_filter file, for example:

 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 

following Zayed's suggestion in ability. rb:

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

In addition, I reset Rolify and manage the roles myself.

0
source

First of all, you probably want to clear your abilities, because at the moment it seems a little confused. Ignoring everything else, it's probably a good idea to specify a custom action for changing passwords, for clarity. For instance:

 # ability.rb def initialize(user) ... cannot :manage_roles, :user if user.has_role? :admin can :manage_roles, :user else end 

(What exactly are you trying to achieve with the rest of the rules? Currently, if it seems that you allow moderators to read, edit and delete yourself only, is that what you intended?)

You will probably want to hide or disable the role section of your form for those who really can't do anything about it:

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

(Note that can? Accepts only two arguments, an action and an object / class.)

If you want to be more secure, you can also use the following check in the controller:

 # users_controller.rb def update @user = User.find(params[:id]) user_params = params[:user] if cannot? :manage_roles, @user user_params.delete_if { |k, v| k.to_sym == :role_ids } end if @user.update_without_password(user_params) redirect_to @user else render :action => "edit" end end 

(You will need to double check which correct key to remove from the params hash, I assume it is :role_ids based on your attr_accessible , but I don't know simple_form very well.)

+1
source

Wrap the roles with this users/_form.html.erb :

With a simple form:

 <% if can? :manage, User %> <%= f.association :roles, as: :check_boxes %> <% end %> 

Without a simple form:

 <% if can? :manage, User %> <div class="control-group"> <%= f.label :roles, class: "control-label" %> <div class="controls"> <% Role.all.each do |role| %> <%= check_box_tag "user[role_ids][]", role.id, @user.role_ids.include?(role.id) %> <%= role.name %><br /> <% end %> </div> </div> <% end %> 
0
source

I'm not sure if I understand what you want from the moderators, but here is the basic configuration that I use. I adapted it to your scenario as much as I could. If moderators do not need individual rights, simply delete the internal sentence.

 class Ability include CanCan::Ability def initialize(user) # Create guest user aka. anonymous (not logged-in) when user is nil. user ||= User.new if user.has_role? :admin can :manage, :all elsif user.has_role? :moderator can :manage, User, user_id: user.id can :create, User can :read, :all else # Guest user aka. anonymous can :read, :all end end end 
0
source

All Articles