Role-Based Authorization for Rails: How We Built Rabarber
The Problem
We built an admin area for a sales company that included a custom CRM, marketing tools, content management, and internal developer utilities. Different employees had different responsibilities:
- Managers handled customer calls and notes
- Analysts worked with marketing reports
- Accountants dealt with invoices
- Administrators oversaw everything
- Developers maintained internal tools
The business needed an authorization system that enforced access strictly according to employee responsibilities. When employees left the company, we also needed to revoke their access without deleting accounts and associated historical data.
Our system needed to:
- Support different access levels for employees
- Provide admin-managed role assignments
- Preserve user accounts
- Allow revoking access when an employee leaves
- Be secure by default (everything denied unless explicitly permitted)
We looked at existing solutions but none fit perfectly:
- Pundit – was too verbose for our taste, we’d need to create policy objects everywhere just to answer questions like “can a manager access Companies controller?”
- CanCanCan – we preferred checks in controllers before business logic runs, not spread across the application.
Other less popular solutions were either too complex or poorly maintained.
We needed something simpler: controller-level checks that answer “can this role access this resource?”
The Solution
We created a simple role-based authorization system. It worked so well that we continued to use it across several projects and eventually extracted it into Rabarber – a gem that handles role management, storage, and authorization checks while developers simply define the rules.
Note: The code examples use Rabarber 5.2 API, but the concepts are the same as our original implementation.
Basic Controller Setup
class Admin::BaseController < ApplicationController
include Rabarber::Authorization
# Apply authorization checks to all actions
with_authorization
# Administrators and developers can access everything in the admin area
grant_access roles: [:admin, :developer]
end
Assuming users must be authenticated at this point (Devise was configured in ApplicationController), this basic setup gave us a clean inheritance model, all actions require authorization, and administrators and developers can access everything by default.
Specific Controller Rules
class Admin::ClientsController < Admin::BaseController
grant_access action: :index, roles: [:manager, :analyst]
def index
# Managers and analysts can see client list
end
grant_access action: :show, roles: [:manager, :analyst]
def show
# Managers and analysts can see client details
end
grant_access action: :update, roles: :manager
def update
# Only managers can update client info
end
def destroy
# Only administrators and developers can delete (from base controller)
end
end
class Admin::ClientNotesController < Admin::BaseController
# Only managers work with client notes
grant_access roles: :manager
def index; end
def create; end
end
We followed this pattern throughout the admin area: InvoicesController let accountants view invoices but only managers could modify them, MarketingReportsController was analyst-only, and internal dev tools were restricted to developers.
We even protected developer utilities such as Sidekiq UI with role checks using Rabarber’s has_role? helper and Devise’s authenticate constraint:
authenticate :user, ->(user) { user.internal? && user.has_role?(:developer) } do
mount Sidekiq::Web => '/sidekiq'
end
We also needed to hide UI elements that certain roles couldn’t access, which we did using view helpers:
<%= visible_to(:accountant, :admin) do %>
<%= link_to "Invoices", admin_invoices_path %>
<% end %>
This kept our navigation menu clean – employees only saw links they were authorized to access.
Dynamic Rules
Initially, we added dynamic rules for a specific use case: one manager needed exclusive access to user activity reports.
class Admin::UserActivityReportsController < Admin::BaseController
grant_access roles: :analyst
grant_access action: :show, roles: :manager, if: -> { current_user.special_manager? }
def show
# ...
end
end
Later, we realized this was overengineering. We didn’t need a dynamic check – we needed a role:
class Admin::UserActivityReportsController < Admin::BaseController
grant_access roles: [:analyst, :user_activity_report_viewer]
def show
# ...
end
end
Then we simply assigned user_activity_report_viewer to whoever needed it. This pattern applied to all our granular permissions: shop_manager, blog_editor, and other roles where a specific work position didn’t exist but someone still needed access to parts of the system to do the job.
That said, dynamic rules still proved themselves useful for edge cases, like time-based access or checking team membership, so we left them for those rare complex scenarios.
Role Management Interface
We built a simple admin interface for role management:
class Admin::EmployeesRolesController < Admin::BaseController
grant_access roles: :admin
def index
@employees = User.where(internal: true) # iterate and show roles for each employee
end
def edit
@employee = User.find(params[:id])
@available_roles = Rabarber.roles
end
def update
User.find(params[:id]).assign_roles(*params[:roles]) # array of roles [:manager, :blog_editor]
redirect_to employees_path, notice: 'Roles updated'
end
def destroy
User.find(params[:id]).revoke_all_roles
redirect_to employees_path, notice: 'Access revoked'
end
end
This gave administrators a simple UI to assign and revoke roles. When someone left the company, we’d revoke all their roles – keeping their historical data intact while blocking admin area access.
And when adding new features that required new roles, we’d create them via data migrations:
class AddBlogEditorRole < ActiveRecord::Migration[7.0]
def up
Rabarber.create_role(:blog_editor)
end
def down
Rabarber.delete_role(:blog_editor)
end
end
After deployment, the new role would appear in the admin UI, ready to be assigned.
When Rabarber Works And When It Doesn’t
Rabarber excels when:
- Authorization is primarily role-based
- Controller-level checks are sufficient
- You have clear role definitions
- You need a simple role assignment UI
- You’re not dealing with complex permission logic
However, if you need complex logic like “can user X edit post Y based on ownership, status, and team membership,” you most likely need policy-based authorization. In this case, reach for Pundit or ActionPolicy, or even use plain Ruby objects – every policy is just a simple class after all.
You can mix approaches – Rabarber for most checks and custom policies for edge cases, but keep in mind that this mixed approach only makes sense if your app is primarily role-based with very few exceptions. If your entire app requires complex policies everywhere, a policy-based gem is a better fit from the start.
So, if your app fits Rabarber’s model, check out Rabarber on GitHub. The README has comprehensive examples and the gem supports both global and contextual roles for multi-tenant applications, which I’ll talk about in a future article.