Introduction to Authorization in Rails app using Pundit with Policy Testing

5 Min. Read
Aug 12, 2019

Introduction

Pundit provides various helpers that help in adding authorization to our application. Our application can be served according to the user type using pundit. Pundit can also be seen as a tool to secure your application. This can be done by restricting access to various URL’s in a web application to a particular type of user. For example restricting a guest user to make changes in the database for a particular entity such as a BookingItem, Establishment, or any other entity which directly connected and related to a relation in a database.

Foremost you need to install a gem to use pundit. This can be done using:

1
2
  # Gemfile
  gem "pundit"

Optionally, you can run the generator, which will set up an application policy with some useful defaults for you:

1
rails g pundit:install

After generating your application policy, restart the Rails server so that Rails can pick up any classes in the new app/policies/ directory.


Policies

Pundit runs on various policies. In your app/policies/ folder you can define various policies class for your Entities. Usually, the class name is in the format, where the entity name is followed by a Policy keyword. So if you had a entiy named Booking, the policy class for it would be BookingPolicy. In this policy class you can define various policies that could either restrict or access users to certain information on the web application depending upon their authority.

The Policy class also maintains some query methods, that return a boolean value. These methods can also be called predicate methods. These methods are then called within a controller or a view page to restrict what a user could and could not see. Below is a simple a simple example on writing a policy:

1
2
3
4
5
class BookingPolicy
  def update?
      user.admin?
  end
end

In the example above, the BookingPolicy consists of a update? query method, that is used to either return true or false on the basis of the authority of the user. Alternatively, various instance of entity could also be passed to the policy method for further filtering the authorization process. For example:

1
2
3
4
5
class BookingPolicy
  def update?
      user.admin? && @booking.canceled?
  end
end

The above example helps in checking if the user is admin or not and also if the instance of the booking entity is canceled or not. Only if both the conditions are satisfied the method returns true.

Usage

The policies that are defined above can be called either in a controller or a view. In a controller, you can call the query methods defined in the policy using the authorize keyword.

1
2
3
4
5
6
7
8
9
def update
  @booking = Booking.find(params[:id])
  authorize @booking
  if @booking.update(post_params)
    redirect_to @post
  else
    render :edit
  end
end

The authorize @booking in the above code, tells ruby to look for policies that are defined in the BookingPolicy class. This is done by passing an instance of the Booking Entity in the authorize method. Which can be seen in the part of the code authorize @booking. The authorize method then looks for a query method name update? in the BookingPolicy class, since the authorize method was called within the update method of the controller.

Alternatively, if you want to call a different query method in a Policy class, or a different policy class all together, you can use the following codes respectively:

1
2
3
4
5
6
7
8
9
def publish
 @booking = Booking.find(params[:id])
  authorize @booking, :update?
  if @booking.update(post_params)
    redirect_to @post
  else
    render :edit
  end
end

The above code looks for a update? query method in the policy rather than a update? method.

1
2
3
4
5
6
def create
  @publication = find_publication
  authorize @publication, policy_class: PublicationPolicy
  @publication.publish!
  redirect_to @publication
end

Here, we has explicitly defined the Policy Class that the authorize method is supposed to look for.

In view, the policy query methods are called using policy keyword which defines a method.

1
2
3
if policy(@post).update?
  link_to "Edit post", edit_post_path(@post)
end

The @post variable is passed as an argument to the policy method which defines the policy class, which then further calls the update? query method within the policy class.

Additional context

If you find yourself needing more context while using policy query methods, you could also pass additional parameters.

Start with creating a user context class in your app/model directory accepting all the contextual parameters you need.

In this case we will be using a session object.

1
2
3
4
5
6
7
8
class UserContext
  attr_reader :user, :session

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

Then you can override the user record used by pundit with an instance of your UserContext class.

1
2
3
4
5
6
class ApplicationController
  include Pundit
  def pundit_user
    UserContext.new(current_user, session)
  end
end

Finish by making your application policy accept the context. If you want to stay compliant with your old policies, delegate those methods to the context.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ApplicationPolicy
  attr_reader :context, :user, :session

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

  delegate :user, to: :context
  delegate :session, to: :context

  ...

end

Testing Policies

It’s quite simple to test the policies that we have written. In most of the cases the test cases are written to figure out if the query methods are return the correct boolean value or not. Below we will discuss how policies can be tested using RSpec.

Follow the steps below for testing using Rspec:

Require pundit/rspec in your spec_helper.rb:

1
require "pundit/rspec"

Then put your policy specs in spec/policies. The policy spec would look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe PostPolicy do
  subject { described_class }

  permissions :update?, :edit? do
    it "denies access if post is published" do
      expect(subject).not_to permit(User.new(admin: false), Post.new(published: true))
    end

    it "grants access if post is published and user is an admin" do
      expect(subject).to permit(User.new(admin: true), Post.new(published: true))
    end

    it "grants access if post is unpublished" do
      expect(subject).to permit(User.new(admin: false), Post.new(published: false))
    end
  end
end

Breakdown of the above code:

The described class refers to PostPolicyin this context. Permissions point to the query methods likeupdate?` defined in the policy class that are to be tested.

Summary

With the use of Pundit gem, one could write various policies for their application and these policies help in making your web application secure, user-friendly, and user-centric. Pundit policies also help in removing various redundant codes in your application, and help implement DRY principle.