Search code examples
ruby-on-railsruby-on-rails-4user-roles

Rails 4 | Assign Role to User in Form using Bitmask | RoleModel


I got stuck on the following issue - I suspect there is a simple solution to it, but I just can't figure it out.

I am using Role Model Gem with Rails 4.

All works well, and the assigned roles are stored perfectly well in the roles_mask attribute (integer) of the user model as internal bitmask.

Now, I would like that Admins can assign as well as remove Roles from users via a FORM (view). I am not a Rails Ninja, so there might be a trick to do this.

According to the Doc, I can do the following:

# role assignment
>> u.roles = [:admin]  # ['admin'] works as well
=> [:admin]

# adding roles (remove via delete or re-assign)
>> u.roles << :manager
=> [:admin, :manager]

So that is understood.

And my approach was to query for all valid roles in the form:

# get all valid roles that have been declared
>> User.valid_roles
=> [:admin, :manager, :author]

Then list them as checkbox. Once the form gets submitted I assign / remove roles.

The question: Is that the right approach, does this even work, and if so how?


Solution

  • Since I had the problem not once I figured out a way - here is the solution:

    More details here.

    Many roles per user

    I assume in this answer that you have roles in form of symbols (below I modified this solution in a way that it works with symbols) - so to work with Pundit in case you are using it.

    # in models/user.rb
    ROLES = [:admin, :manager, :general, :custom_role, :another_custome_role, :banned]
    

    It is possible to assign multiple roles to a user and store it into a single integer column using a bitmask. First add a roles_mask integer column to your users table.

    rails generate migration add_roles_mask_to_users roles_mask:integer rake db:migrate Next you'll need to add the following code to the User model for getting and setting the list of roles a user belongs to. This will perform the necessary bitwise operations to translate an array of roles into the integer field.

    # in models/user.rb
    def roles=(roles)
      self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
    end
    
    def roles
      ROLES.reject do |r|
        ((roles_mask.to_i || 0) & 2**ROLES.index(r)).zero?
      end
    end
    

    Here a tiny modification to make it work with Rolify & Pundit

      def roles=(roles)
        self.roles_mask = (roles.map { |x| x.to_sym } & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
      end
    

    If you're using devise without strong parameters, don't forget to add attr_accessible :roles to you user model.

    If you're using devise with strong_parameters, either as a gem in a Rails 3 app, or as is built-in in Rails 4, dont forget to add the roles to the permitted list in the controller

    class ApplicationController < ActionController::Base
      before_filter :configure_permitted_parameters, if: :devise_controller?
    
      protected
    
      def configure_permitted_parameters
        devise_parameter_sanitizer.for(:sign_up) {|u| u.permit(:email, :password, :password_confirmation, roles: [])}
      end
    end
    

    See the section on strong parameters in the Devise documentation.

    You can use checkboxes in the view for setting these roles.

    <% for role in User::ROLES %>
      <%= check_box_tag "user[roles][#{role}]", role, @user.roles.include?(role), {:name => "user[roles][]"}%>
      <%= label_tag "user_roles_#{role}", role.humanize %><br />
    <% end %>
    <%= hidden_field_tag "user[roles][]", "" %>
    

    Finally, you can then add a convenient way to check the user's roles in the Ability class.

    # in models/user.rb
    def is?(role)
      roles.include?(role.to_s)
    end
    
    # in models/ability.rb
    can :manage, :all if user.is? :admin
    

    See Custom Actions for a way to restrict which users can assign roles to other users.

    This functionality has also been extracted into a little gem called role_model (code & howto).

    If you do not like this bitmask solution, see Separate Role Model for an alternative way to handle this.