Skip to content

Latest commit

 

History

History
177 lines (129 loc) · 6.18 KB

role_based_authorization.md

File metadata and controls

177 lines (129 loc) · 6.18 KB

Role-based Authorization

CanCanCan is decoupled from how you implement roles in the User model, but how might one set up basic role-based authorization? The pros and cons are described here.

The following approach allows you to simply define the role abilities in Ruby and does not need a role model. Alternatively, [[Separate Role Model]] describes how to define the roles and mappings in a database.

Since there is such a tight coupling between the list of roles and abilities, I recommend keeping the list of roles in Ruby. You can do so in a constant under the User class.

class User < ActiveRecord::Base
  ROLES = %i[admin moderator author banned]
end

But now, how do you set up the association between the user and the roles? You'll need to decide if the user can have many roles or just one.

One role per user

If a user can have only one role, it's as simple as adding a role string column to the users table.

rails generate migration add_role_to_users role:string
rake db:migrate

In your users_controller.rb add :role to the list of permitted parameters.

def user_params
  params.require(:user).permit(:name, :email, :password, :password_confirmation, :role)
end

If you're using ActiveAdmin don't forget to add role to the user.rb list of parameters as well

  permit_params :name, :email, :role

Now you can provide a select-menu for choosing the roles in the view.

<!-- in users/_form.html.erb -->
<%= f.collection_select(:role, User::ROLES, :to_s, lambda{|i| i.to_s.humanize}) %>

You may not have considered using collection_select when you aren't working with an association, but it will work perfectly. In this case the user will see the humanized name of the role, and the simple lower-cased version will be passed in as the value when the form is submitted.

It's then very simple to determine the role of the user in the Ability class.

can :manage, :all if user.role == "admin"

Many roles per user

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)
  roles = [*roles].map { |r| r.to_sym }
  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

If you're using devise, don't forget to add attr_accessible :roles to your user model or add following to application_controller.rb

  before_action :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

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.to_s.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 has_role?(role)
  roles.include?(role)
end

# in models/ability.rb
can :manage, :all if user.has_role? :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.

Role Inheritance

Sometimes you want one role to inherit the behavior of another role. For example, let's say there are three roles: moderator, admin, superadmin and you want each one to inherit the abilities of the one before. There is also a "role" string column in the User model. You should create a method in the User model which has the inheritance logic.

# in User
ROLES = %w[moderator admin superadmin]
def role?(base_role)
  ROLES.index(base_role.to_s) <= ROLES.index(role)
end

You then use this in the Ability class.

# in Ability#initialize
if user.role? :moderator
  can :manage, Post
end
if user.role? :admin
  can :manage, ForumThread
end
if user.role? :superadmin
  can :manage, Forum
end

Here a superadmin will be able to manage all three classes but a moderator can only manage the one. Of course you can change the role logic to fit your needs. You can add complex logic so certain roles only inherit from others. And if a given user can have multiple roles you can decide whether the lowest role takes priority or the highest one does. Or use other attributes on the user model such as a "banned", "activated", or "admin" column.

This functionality has been extracted into a gem called canard (code & howto).

Alternative Role Inheritance

If you would like to keep the inheritance rules in the Ability class instead of the User model it is easy to do so like this.

class Ability
  include CanCan::Ability

  def initialize(user)
    @user = user || User.new # for guest
    @user.roles.each { |role| send(role.name_to_symbol) }

    if @user.roles.size == 0
      can :read, :all #for guest without roles
    end
  end

  def manager
    can :manage, Employee
  end

  def admin
    manager
    can :manage, Bill
  end
end

Here each role is a separate method which is called. You can call one role inside another to define inheritance. This assumes you have a User#roles method which returns an array of all roles for that user.