Pundit in Rails

A few coworkers and I recently jumped on the Pundit train a few weeks ago, and by far we can all agree that it’s been the highlight of our recent development in Rails recently.

Pundit is an authorization gem that works with multiple finely tuned classes, as opposed to cancancan which restricts you to a single class. For more complex business logic, Pundit allows you to create hierarchies of inherited permissions, and split your rule sets out by resource.

Pundit classes look like the following.

class ProfilePolicy < ApplicationPolicy
def index?
admin?
end

def show?
true
end

def new?
(developer? && no_profile?) || admin?
end
end

Each class is called a policy and correlates to a resource with methods. In this case, this policy matches a PolicyController in a Rails app. It’s common practice to extend off a base policy where default permissions and useful private methods can be defined.

class ApplicationPolicy
attr_reader :user, :record

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

private

def developer?
@user.developer? if @user
end

def admin?
@user.admin? if @user
end
end

Now, this format does mean that updating controllers takes the extra steps of keeping your policies and policy tests up to date, but the logic involved is usually not that complex and worth the low level control that is provided.

Testing is also really simple as well with the built in test helpers. You only need to make sure that you have an instance of a user and a record. Then you just assert that the user has permission to act on the record in each permission block like so.

require 'rails_helper'

RSpec.describe ProfilePolicy do
subject { ProfilePolicy }

let(:admin) { User.new(role: User.admin) }
let(:developer) { User.new(role: User.developer) }
let(:developer_with_profile) { User.new(role: User.developer, profile_id: 1) }

permissions :index? do
it 'denies access unless user is an admin' do
expect(subject).to permit(admin, :profile)
expect(subject).not_to permit(developer, :profile)
end
end

permissions :show? do
it 'denies access to nobody' do
expect(subject).to permit(admin, :profile)
expect(subject).to permit(developer, :profile)
end
end

permissions :new? do
it 'denies access unless user is an admin or a profileless developer' do
expect(subject).to permit(admin, :profile)
expect(subject).to permit(developer, :profile)
expect(subject).not_to permit(developer_with_profile, :profile)
end
end

permissions :create? do
it 'denies access unless user is an admin or a profileless developer' do
expect(subject).to permit(admin, :profile)
expect(subject).to permit(developer, :profile)
expect(subject).not_to permit(developer_with_profile, :profile)
end
end

permissions :edit? do
it 'denies access unless the user is an admin or developer' do
expect(subject).to permit(admin, :profile)
expect(subject).to permit(developer, :profile)
end
end

permissions :update? do
it 'denies access unless the user is an admin or developer' do
expect(subject).to permit(admin, :profile)
expect(subject).to permit(developer, :profile)
end
end
end

Though the examples above are from my personal project called Gitfolio, this pattern has been tremendously successful in my current project at work, allowing us to adapt very quickly to changing requirements with fine detail.

Happy Coding!