Recently we started using Pundit extensively on a project, after some time experimenting, here’s a description of how we use it

Not long ago, we needed to establish a authorisation system for resources and actions in one of our applications. We immediately thought of the classic CanCan, but at that time it wasn’t totally compatible with Rails 4, so we had to look for a substitute. Luckily for us, the Swedish company Elabs had come up with a very nice solution, a gem called Pundit.

Basic usage

The basic usage and philosophy behind Pundit is very well explained on the gem’s README file, as well as on this blog post by its creators. The main point is to extract the authorisation rules into policy files, which are POROs:

class ReferencePolicy
  attr_reader :user, :reference

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

  def new?
    user.has_roles?("Recruitment")
  end

  alias_method :outgoing?, :new?

  def give?
    user.has_roles?("HR admin") && reference.is_draft?
  end

  alias_method :incoming?, :give?
end

Notice the flexibility of being able to set up conditions that affect only our user (new?), instead of having to depend on a resource. The second step would be adding Pundit directives and methods to your controller:

class ReferenceController < ApplicationController
  include Pundit

  after_action :verify_authorized, :only => :give
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  ...

  def give
    @reference = Reference.find(params[:id])
    authorize @reference
    ...
  end

  ...

  private

  def user_not_authorized
    flash[:error] = "You are not authorized to perform this action."
    redirect_to(request.referrer || root_path)
  end
end

As you can see, we can set rules for our controller actions, and clean them, as well as our models, from authorisation logic. Even more, we can use Pundit in our views:

- if policy(@user, @employee).advanced?
  = render "form"
- else
  = render "reduced_form"

Testing

Pundit works brilliantly with Rspec out-of-the-box, as you can see on Pundit’s Github page. The recommended article on that section about Thunderbolt Labs approach is great, and it’s how we test our policies – as by using a matcher, we can get very clean and readable test files:

require 'spec_helper'

describe CompanyPolicy do
  subject { CompanyPolicy.new(user, company) }

  let(:company) { create(:company) }

  describe "with a global admin user" do
    let(:user)  { create(:company_user, :global_admin) }

    it { should permit(:show) }
    it { should permit(:edit) }
    it { should permit(:update) }
    it { should permit(:access_menu_item) }
  end

  describe "without a global admin user" do
    let(:user) { create(:company_user) }

    it { should_not permit(:show) }
    it { should_not permit(:edit) }
    it { should_not permit(:update) }
    it { should_not permit(:access_menu_item) }
  end
end

Going a bit further

Your Pundit policies may apply to just one model/controller, to namespaces of your application, or to the whole system. When you start applying policies to your controllers, you’ll soon see the need to refactor a bit keep things DRY

The ApplicationPolicy

It’s the default one created by the gem, contains the basic initialisation code for a policy and the rest of the policies should inherit from it:

class ApplicationPolicy
  attr_reader :user, :record

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

  def index?
    false
  end

  def global_admin?
    user.has_role?('Global Admin')
  end

  def hr_user?
    user.has_role('HR Admin')
  end
  ....
end

class CategoryPolicy < ApplicationPolicy
  def upload?
    user.roles.select{|role| record.roles.include?(role) }.any?
  end

  def index
    global_admin? || hr_user?
  end

  def download?
    global_admin?
  end
end

This way we can have the basic initialisation as generic in one policy, and put there every method we need throughout several other policies.

The Parent Controller

When you’ve got several controllers using the Pundit hooks and methods, most times the best thing to do is keeping them DRY by absorbing this behaviour into the ApplicationController (or the parent controller for a given namespace). Once the line include Pundit is on the controller, every Pundit option can be either on each controller (if needed) or absorbed into the parent controller, including:

  • Pundit user: if the user to be authorized or not is different from the classic current_user – let Pundit know by defining a protected pundit_user method returning your custom user
  • Rescue from user_not_authorized: this can be a bit trickier, as you’ll probably want each controller to return the user to a different path depending on the resource, and the messages will be different. Although by using internationalization and inflection this could be solved.

The idea, in the end, would be having in our controllers just the after_action :verify_authorized directive and the authorize @resource on the corresponding methods.

Even further: Pundit is not just for controllers

Logically, you can use Pundit outside the controllers, for example, in custom services, or as stated before, in your views. About this: there is a nice example coming soon as a blog post by Rob Paskin on how he managed to create a flexible and clean system for displaying a menu on an application based on user roles and Pundit.

Conclusion

So far our experience with Pundit has been very positive, and we love how it enables us to keep models and controllers free from authorisation code, yet allowing to keep the resource logic separated in different files naturally, which is a big plus when coming from CanCan. Also, the flexibility and simplicity of POROs adds to the ease of use.

As the only ‘minus’, we could speak about how it would be better to use Pundit from the start in a project (or when you start adding authorisation) instead of adding it in the middle of the development, but anyway, this is basically common to adopting new systems or practices (as we saw earlier with localisation and internationalisation) on every project. In any case, Pundit is easy enough to use to provide an easy transition into it.

Picture by Transguyjay at Flickr