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

3

6

I am trying to achieve role-based authorization in Rails.

What we require:

  1. Roles should be dynamic, we should able to create, edit, or delete roles.
  2. Permissions also should be dynamic.

Findings:

  1. We can't use the pundit gem because its policies are static and we can't make it dynamic.
  2. We can use the cancan gem and we can use it dynamically but I didn't get how it can be done? And how it works with `database?

It's my first project on the authorization part. We have rails as the back-end and vue.js as the front end. Whatever roles are there, on the database all data should be empty at first. We'll use the seed to create a super-admin role and give all permissions. Super-admin will create roles, edit roles, destroy roles, and also add permissions, edit and destroy permissions eventually.

If there is any other helpful method then please let me know.

Thanks.

Ambidextrous answered 7/2, 2020 at 6:22 Comment(4)
Can you give more details of your use case? It looks like cancan gem could achieve your requirements. However, cancan manage authorization based on the model name. I have a project has dynamic role, dynamiac permission based on controller names, and we are using cancan with some tweaks.Philippines
"Pundit is static" - bullshit. Pundit is just object oriented programming so you can build whatever you want. I really would not recommend cancancan here as the whole awkward DSL will just get in your way. Cancancan is good at scaling down to trivial uses cases and not much else.Spendthrift
I like cancancan and rolifyEmory
@FeifeiXiong I have updated my question. Please let me know if you need more details.Ambidextrous
S
4

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.

Spendthrift answered 7/2, 2020 at 8:59 Comment(5)
Hey Max, thanks. Can you tell how we gonna implement pundit on it? And how database works?Ambidextrous
And does pundit provides API??Ambidextrous
You're going to have to do your own legwork. You're asking extremely broad questions that can only be answered by doing your own research. How exactly to implement this depends on the domain. Also I don't even know where to start on questions like "And how database works?". If you don't know that then you won't be able to even start on this task.Spendthrift
I know how database works but pundit provides API? I am talking about pundit only here. If I create role and permission table and use pundit as: class RolePolicy < ApplicationPolicy role_id = @current_user.role_id @@permission_for_role = Permission.find(role_id) def update? @@permission_for_role.u? end end .......... It works. Thanks anyway.Ambidextrous
What do you even mean by "provides API"? Pundit works for all kinds of applications, including API apps. It has an API (like almost all software) that it provides to your controllers and views (the authorize and policy methods) . It does not magically create a HTTP API.Spendthrift
C
2

If I understand your requirements correctly, you should be able to use Pundit to achieve this.

From what I understand,

  • Users have roles
  • Users can be assigned and unassigned roles at runtime
  • Permissions are given to either roles or users directly.
  • Permissions can be updated at runtime

So you can have something like,

class User
  has_many :user_role_mappings
  has_many :roles, through: :user_role_mappings
  has_many :permissions, through: :roles
  ...
end

class UserRoleMapping
  belongs_to :user
  belongs_to :role
end

class Role
  has_many :role_permission_mappings
  has_many :permissions, through :role_permission_mappings
  ...

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

class RolePermissionMapping
  belongs_to :role
  belongs_to :permission
end

class Permission
  ...
end

And in your policy, you can check if any of the user's roles has the required permission.

class PostPolicy < ApplicationPolicy
  def update?
    user.roles.any? { |role| role.has_permission?('update_post') }
  end
end

Edit:

Using the mapping tables, you can update the permissions for a role, and the roles for a user from an admin dashboard.

Cazzie answered 7/2, 2020 at 6:56 Comment(5)
As a small detail you don't really need to do .pluck(:name).include? you can just do a count query with permissions.where(name: name).exists?. .pluck is one the most misused methods in Rails.Spendthrift
I love your answer @anuj. Can I ask, I would like to add a model for creating, updating, and destroying permissions on the database for each controller's actions, can you please elaborate how this can be possible using Pundit, so that each users/roles permissions can be modified dynamically from the application User Interface and not from the backend. Thanks.Herndon
Thanks @PromisePreston. Updated the answer with some more details. Basically you will have a mapping table between User and Roles, and another between Roles and Permissions. You can create and delete these mappings to adjust user permissions from your dashboard.Cazzie
I will try this out and see how it goes. Although, I am still confused on how the Read, Write, Delete and Update permissions for each controller can be saved to the role permissions table. Like how can I save them to the database since they are actions and not really values.Herndon
The permissions required for each controller action will remain the same. You could save them in the Permissions table, example update_contacts. What you will be changing are the permissions for different roles / users, and not the permissions required for different actions.Cazzie
F
0

You can achieve dynamic roles with pundit. pundit let's you define plain Ruby objects with methods that are called to determine if a user has permission to perform an action. For example:

class PostPolicy < ApplicationPolicy
  def update?
    user.has_role('admin')
  end
end

If you want a user to have multiple roles you could set up a has_and_belongs_to_many: :roles association on your User model.

See: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association

Freeforall answered 7/2, 2020 at 6:59 Comment(2)
user.has_role('admin'), means admin role is static here. Isn't it??Ambidextrous
Not if has_role is a method that consults the database. You will need to write that logic yourself, you'll find some examples in the other answers.Freeforall

© 2022 - 2024 — McMap. All rights reserved.