Simple RBAC implementation with Rails


Authentication and Authorization are 2 security mechanisms to manage user access to a system. They are sometimes used interchangeable but they actually have different functions:

  • Authentication is the process of verifying who a user is.
  • Authorization is the process of verifying what a user has access to.

There are many techniques/strategies for authorization, such as:

  • Access control list (ACL)
  • Role-based access control (RBAC)
  • Attribute-based access control (ABAC)

This post will introduce about RBAC, a popular authroization technique for common web apps, and how to implement a simple RBAC system in Rails with the help from Pundit gem.

What is RBAC?

Role-based access control is "an approach to restricting system access to authorized users" (Wikipedia). It is about user management and role assignments. What a user could access is defined by their defined roles. A role is a collection of permissions that define actions (an operation on a resource) that a role can do.

Core actors of a RBAC system:

  • Users
  • Roles
  • Permissions
  • Operations
  • Resources

RBAC has advantage of simple to implement and execute but easy to be flooded with role explosions where admins keep adding roles for specific purpose. RBAC is also has difficulties with complex access rules like time-based rules, per asset access.

Implement a simple RBAC in Rails with Pundit

We're going to setup User, Role and Permission models with these assumptions:

  • A user may have many roles
  • A role may be attached to many users
  • A role may have my permissions
  • A permission may be attached to many roles
  • A permission name is formatted with {resource}.{action}, e.g. read.employees

User model

class User < ApplicationRecord
  has_and_belongs_to_many :roles

  def has_permission?(action, resource)
    permissions.include?("#{resource}.#{action}")
  end

  def permissions
    @permissions ||= roles.flat_map(&:permissions).map(&:name).uniq
  end
end

Role model

class Role < ApplicationRecord
  has_and_belongs_to_many :permissions
  has_and_belongs_to_many :users
end

Permission model

class Permission < ApplicationRecord
  has_and_belongs_to_many :roles
end

Pundit is a popular gem for authorzation on Rails. I won't explain how to use it here but go straight into the code for ApplicationPolicy which will be used as based for other policies.

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    user.has_permission?(:read, resource)
  end

  def show?
    index?
  end

  def create?
    user.has_permission?(:create, resource)
  end

  def new?
    create?
  end

  def update?
    user.has_permission?(:update, resource)
  end

  def edit?
    update?
  end

  def destroy?
    user.has_permission?(:delete, resource)
  end

  protected

  def resource
    raise NotImplementedError
  end
end

Then for a particular policy we only need to specify resource name

class EmployeePolicy < ApplicationPolicy

  def resource
    'employees'
  end

end

Concerns

What if a user could only access a portion of a resouce instead of all records of the resource? For example a manager should only be able to manage their own department employees.

There are 2 options:

  1. Implement a more complex RBAC system
  2. Use Pundit scope

There are no obvious choices. Deciding which option to implement depends on the requirements of the app and other factors.

Why just not use Pundit and roles only

Using RBAC we could dynamically add roles, change role permissions on UI without the need to change the code.