Rails, Devise, CanCan, Rolify and User Company Role
During development of a web application, I came across a problem with users having one role per company.
For example, you have a user who can manage two companies. He created the first company and therefore he is the owner and an admin of that company, but he has been invited to second company, where he haves limited access of content, so he's the moderator of that company.
On data level, I have a User table created by Devise, Companies table and a Role which Rolify created for me.
Because I want to know which user has which role for a company; I need a table to store their IDs, so I created a Unities table.
create_table :unities do |t|
    t.integer :role_id
    t.integer :user_id
    t.integer :company_id
     t.timestamps
end
After that I created a model called Unity, which I will use for easier role manipulation.
unity.rb
class Unity < ActiveRecord::Base
    attr_accessible :role_id, :user_id, :company_id
    belongs_to :role
    belongs_to :company
    belongs_to :user
end
and I also added associtaions to company, role and user models.
user.rb
class User < ActiveRecord::Base
    rolify
    cattr_accessor :current_role 
    has_many :unities
    has_many :roles, through: :unities
    has_many :companies, through: :unities
end
I added current_role so I can check which role does user haves on a company that he is on but I will get to that later
role.rb
class Role < ActiveRecord::Base
  has_many :unities
  has_many :users, through: :unities
  has_many :companies, through: :unities
end
company.rb
class Company < ActiveRecord::Base
  cattr_accessor :current 
  has_many :unities
  has_many :users, through: :unities
  has_many :roles, through: :unities
end
Here I added a current company, so I can check wich company is active I will use that later.
What is cattr_accestor?
http://www.cowboycoded.com/2010/04/20/ruby-cattr_accessor-vs-attr_accessor/
After defining associations between these three tables, we can now define roles and abilities in Ability.rb which is used by CanCan
But before that, we need to find a current User role.
Since one user can manage multiple companies but on different level, first we need to find wich company is he currently on, and we can get that from params.
So in a ApplicationController add two methods.
class ApplicationController < ActionController::Base
    before_filter :initialize_company, :initialize_user_role
    def initialize_company
        if params.has_key?(:company_id)
        Company.current = Company.find(params[:company_id]) 
        end     
    end
    def initialize_user_role
        unless Company.current.nil?
            User.current_role = Unity.where(company_id: Company.current.id, user_id: current_user.id).first.role.name 
        end
    end
end
And now we are ready to create ability.rb
class Ability
    include CanCan::Ability
    def initialize(user)
        user ||= User.new
        if User.current_role == 'admin' 
            can :manage, :all 
        else
            if User.current_role == 'moderator'
                can :read, Products
                can :update, Products
                cannot :destroy, Products
                cannot :create, Products
                cannot :manage, Client
            end
            can :read, :all
        end
    end
end
From ApplicationController we can get current_role and check it up against the role we want.
if User.current_role == 'admin'
In this article I explained how things should work, and basic principle of creating a role per company and user. In next article I will describe how to change user permissions and how to add roles to users.
And here is gist for better view
https://gist.github.com/vmarcetic/6049929
This is just my way of doing things. If you have any suggestions, or you know how to do it better, write a comment.
Written by Vedran Marčetić
Related protips
7 Responses
Is this thread safe? I mean User.current_role and Company.current.
I think User.currentrole usage is not correct, cause Ability gets specified user. It's better to make: user.currentrole - meaning the use of given user of initialize method.
Thank you for posting this!
You can get rid of the nested conditionals by refactoring it like this:
def initialize(user)
    user ||= User.new
    can :read, :all
    if user.current_role == 'admin' 
        can :manage, :all 
    else
        can :read, Products
        can :update, Products
        cannot :destroy, Products
        cannot :create, Products
        cannot :manage, Client
    end
end
And in the case that you have multiple roles, you can do the following:
def initialize(user)
    user ||= User.new
    can :read, :all
    case user.current_role
        when 'admin'
            can :manage, :all 
        when 'moderator'
            can :read, Products
            can :update, Products
            cannot :destroy, Products
            cannot :create, Products
            cannot :manage, Client
        # ... other roles and abilities ...
    end
endYou're correct with this - hopefully, it was just a typo.
You're correct with this - hopefully, it was just a typo.
Hi, 
Did you complete your next article on this?
I need to know "how to change user permissions"