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 protectedpundit_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.