Introduction to Authorization in Rails app using Pundit with Policy Testing
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 PostPolicy
in this context. Permissions point to the query methods like
update?`
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.