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.

Similar Posts