Last Updated: November 15, 2017
·
21.6K
· vmarcetic

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.

7 Responses
Add your response

Is this thread safe? I mean User.current_role and Company.current.

over 1 year ago ·

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.

over 1 year ago ·

Thank you for posting this!

over 1 year ago ·

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

end
over 1 year ago ·

You're correct with this - hopefully, it was just a typo.

over 1 year ago ·

You're correct with this - hopefully, it was just a typo.

over 1 year ago ·

Hi,
Did you complete your next article on this?
I need to know "how to change user permissions"

over 1 year ago ·