ruby-on-railsauthorizationrolesrole-base-authorization

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


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.


Solution

  • 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.