1

This may be more of a conceptual question. I've got a user table and I need to provide roles for each of the users. For example I would like for some users to create customers and some to only view customers. I figure that I could create an association like this:

User has_one :role
Role belongs_to :user

With this structure, I could create boolean fields on the Role model such as can_create_customer. If I did, then I could use the following code to check for permissions based on every user:

if role.can_create_customer?
    Customer.create(name: "Test")
end

Is there a better way of accomplishing the same thing?

notapatch
  • 6,569
  • 6
  • 41
  • 45
Cannon Moyer
  • 3,014
  • 3
  • 31
  • 75
  • Have you tried looking at [this](https://www.google.com/search?q=rails+roles+gem)? The first bit may be helpful. – jvillian Mar 06 '18 at 20:43

1 Answers1

6

This is how I do user roles and rights.

I create the following models:

class Role < ApplicationRecord
  has_and_belongs_to_many :users
  has_and_belongs_to_many :rights
  validates :name, presence: true
end

class Right < ApplicationRecord
  has_and_belongs_to_many :roles
  validates :name, presence: true
end

Make sure you have proper constraints and indexes set in your database:

add_index :roles, :name, unique: true
add_index :rights, :name, unique: true

From there, you will need join tables for roles_users as well as rights_roles (because it's a many to many)

Then in seeds I create a few roles and rights:

role_admin = Role.create!(name: 'admin')               
role_admin.rights.create!(name: 'full_access')         

role_cm = Role.create!(name: 'company_manager')     
role_cm.rights.create!(name: 'edit_company')        
role_cm.rights.create!(name: 'edit_all_invoices')      
role_cm.rights.create!(name: 'edit_all_users') 

Then you need to assign one or more roles to your user

current_user.roles << Role.find_by(name: 'company_manager') 

Now, I simply check the roles/rights on login, and store it in a session along with the user_id

def session_login(user)
  session[:user_id] = user.id
  session[:rights] = user.list_rights
end

And you can access roles/rights with several JOIN sql statements efficiently. You want to store it in a session on login so you dont need to make this query for every request. It does mean however if you change roles/rights in the middle of your users session they will need to log back out and in again to see the updated changes

class User < ApplicationRecord
  def list_roles
    roles.map(&:name)
  end

  def list_rights
    Right.joins(roles: :roles_users).where('roles_users.user_id = ?', id).map(&:name)
  end
end

Final notes

Now, when you do your 'checking', make sure you verify based on RIGHTS (dont check a users' role)

You can make this helper method

def current_user_has_right?(right)
  return false unless session[:rights]
  session[:rights].include? right
end

You can authorize! an entire controller for instance in this way:

def authorize!
  not_found unless current_user_has_right?('full_access')
end
Tallboy
  • 12,847
  • 13
  • 82
  • 173
  • This is great! I do have one question regarding security. Couldn't a user technically alter their session variable to include a right they didn't originally have and gain access to an unauthorized portion of the site? The only way I could think to get around that would be to actually make the query for each request. I guess if you encrypt the session then that wouldn't be an issue. – Cannon Moyer Mar 06 '18 at 21:06
  • 1
    No, a user session is not editable by a user because it's signed. There are cases where the session data WILL be visible to the user (unless you also choose to encrypt the session), but in no cases (at least with a default rails install) will the user be able to hand-edit a session variable. – Tallboy Mar 06 '18 at 21:13
  • 1
    https://stackoverflow.com/questions/41467012/what-is-the-difference-between-signed-and-encrypted-cookies-in-rails – Tallboy Mar 06 '18 at 21:14
  • 1
    Im 90% sure that Rails 5 both signs and encrypts cookies by default (it used to only sign) – Tallboy Mar 06 '18 at 21:15
  • One last question. Why are you using `add_index` on the tables? I thought this was to only be used on columns that had many identical values. – Cannon Moyer Mar 06 '18 at 22:43
  • 1
    no, it's adding an index AND a constraint. The index is used because you might want to find a specific role. To do that, it means `name` is going to be in the `WHERE` clause, so it should be indexed. The second, and more important thing, is that it's adding a unique index so that you cant have duplicate values. That could be bad, because say you add an `edit_posts` right... and want to go modify it later. You dont want to accidentially edit one and not realize you have that right duplicated. It should be 1 role, 1 right, and a many to many relationship linking them all – Tallboy Mar 06 '18 at 22:57
  • (so a right can be shared by multiple roles, you wouldnt have a duplicate right for each role) – Tallboy Mar 06 '18 at 22:57