Rails 4 User Roles and Permissions

Rails 4 user roles and permissions

Your first guess was right, use cancancan and you'll be good with it.

EDIT Jul 24, 2015

I've been using cancancan for a long time now and it was always working great. I've recently started working on a project where Pundit is used for authorization.

It is awesome. It prompts you to define the policy for each resource and it feels more natural than one bloated Ability class.

For bigger projects, I would definitely recommend Pundit.

how to add Roles and Permission in Rails even it's already build by devise

we can do this by using cancan gem.

Basic Steps to implement:

  1. import gem into Gemfile

  2. create a ability file by using the below command

rails g cancan:ability

  1. define the accesses like below.
class Ability
include CanCan::Ability

def initialize(user)

alias_action :create, :read, :update, :destroy, :to => :crud
if user.super_admin?
can :access, :rails_admin # grant access to rails_admin
can :dashboard # grant access to the dashboard

# Accss Level to Models

can :crud

end

end
end

See more details for Gems documentation

Rails: Is there any way to build dynamic role based authorization in rails?

Pundit vs CanCanCan

Your conclusions about CanCanCan and Pundit are just nonsense. Neither of them are "static" or "dynamic" and they have pretty much the same features. The architecture and design philosophy are radically different though.

CanCanCan (originally CanCan) is written as a DSL which was the hottest thing since pre-sliced bread back when Ryan Bates created CanCan 10 years ago. It scales down really well and is easy to learn but gets really ugly as soon as you reach any level of complexity. If anything doing "dynamic authorization" in CanCanCan is going to be a nightmare due its architecture. The ability class in CanCanCan is the god of all god objects.

Pundit is just Object Oriented Programming. In pundit your policies are just classes that take a user and resource as initializer arguments and respond to methods like show?, create? etc. Pundit is harder to understand initially but since its just OOP you can tailor it however you want. And since your authentication logic is stored in separate objects it scales up to complexity far better and adheres to the SOLID principles.

How do I setup a dynamic roles system?

This is you standard role system ala Rolify:

class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles
def has_role?(role, resource = nil)
roles.where({ name: role, resource: resource }.compact).exists?
end

def add_role(role, resource = nil)
role = Role.find_or_create_by!({ name: role, resource: resource }.compact)
roles << role
end
end

# rails g model user_roles user:belongs_to role:belongs_to
class UserRole < ApplicationRecord
belongs_to :user
belongs_to :role
end

# rails g model role name:string resource:belongs_to:polymorphic
class Role < ApplicationRecord
belongs_to :resource, polymorphic: true, optional: true
has_many :user_roles
has_many :users, through: :user_roles
end

You can then scope roles to resources:

class Forum < ApplicationRecord
has_many :roles, as: :resource
end

Rolify lets you go a step further and just defines roles with a class as the resource. Like for example user.add_role(:admin, Forum) which makes the user an admin on all forums.

How do I create a permissions system?

A simple RBAC system could be built as:

class Role < ApplicationRecord
has_many :role_permissions
has_many :permissions, through: :role_permissions

def has_permission?(permission)
permissions.where(name: permission).exists?
end
end

# rails g model permission name:string
class Permission < ApplicationRecord
end

# rails g model role_permission role:belongs_to permission:belongs_to
class RolePermission < ApplicationRecord
belongs_to :role
belongs_to :permission
end

So for example you could grant "destroy" to "moderators" on Forum.find(1) by:

role = Role.find_by!(name: 'moderator', resource: Forum.find(1))
role.permissions.create!(name: 'destroy')
role.has_permission?('destroy') # true

Although I doubt its really going to be this simple in reality.

User Roles in Ruby on Rails

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


Related Topics



Leave a reply



Submit