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:
- Implement a more complex RBAC system
- 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.