Now that’ve got a handle on, fundamentally, how authorization is done — with conditionals and redirects — for an application of any real size, it’s probably worth using an authorization library. A good one will give us helper methods that will allow us to be more concise, and more importantly will help us avoid letting security holes slip through the cracks.
There are many Ruby authorization libraries, but my go-to is one called Pundit. Add it to your Gemfile
and bundle
.
Pundit revolves around the idea of policies. Create a folder within app/
called policies/
, and within it create a file called photo_policy.rb
. This will be a plain ol’ Ruby object (PORO):
class PhotoPolicy
end
The idea is that we want to encapsulate all knowledge about who can do what with a photo inside instance methods within this “policy” class. Let’s set up the class to accept a user and a photo when instantiated:
class PhotoPolicy
attr_reader :user, :photo
def initialize(user, photo)
@user = user
@photo = photo
end
end
And now, for example, to figure out who can see a photo, let’s define a method called show?
1 that will return true
if the user is allowed to see the photo and false
if not:
class PhotoPolicy
attr_reader :user, :photo
def initialize(user, photo)
@user = user
@photo = photo
end
# Our policy is that a photo should only be seen by the owner or followers
# of the owner, unless the owner is not private in which case anyone can
# see it
def show?
user == photo.owner ||
!photo.owner.private? ||
photo.owner.followers.include?(user)
end
end
Let’s test it out in rails console
:
[1] pry(main)> alice = User.first
=> #<User id: 15>
[2] pry(main)> bob = User.second
=> #<User id: 16>
[3] pry(main)> alice.followers.include?(bob)
=> false
[4] pry(main)> alice.private?
=> true
[5] pry(main)> photo = alice.own_photos.first
=> #<Photo id: 157>
[6] pry(main)> policy_a = PhotoPolicy.new(alice, photo)
=> #<PhotoPolicy:0x00007fca9886d8e0>
[7] pry(main)> policy_a.show?
=> true
[8] pry(main)> policy_b = PhotoPolicy.new(bob, photo)
=> #<PhotoPolicy:0x00007fca5fa3a6e8>
[9] pry(main)> policy_b.show?
=> false
Great! Now we can replace the conditionals that we have scattered about the application with this method.
Let’s start by locking down access to the Photos#show
action. Instead of redirecting inside a before_action
(which is a solid strategy, mind you), let’s take a slightly different tack: we’ll raise an exception if the current_user
isn’t authorized.
def show
unless PhotoPolicy.new(current_user, @photo).show?
raise Pundit::NotAuthorizedError, "not allowed"
end
end
Test it out by visiting a show page that you shouldn’t be able to (you can remove the /edit
from an edit page to find a URL.)
Great! But what if we don’t want to show an error page? Redirecting with a flash message was pretty nice. Well, we can rescue that specific exception in ApplicationController
:
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_back(fallback_location: root_url)
end
Now we have all of the same functionality back, but we’re nicely set up to encapsulate all of our authorization logic in one place.
So far, Pundit hasn’t done anything for us at all, other than providing the exception class that we raised. We wrote all the Ruby ourselves. But now, let’s use some some helper methods from Pundit that will allow us to be very concise, if we follow conventional naming.
First, include Pundit
in ApplicationController
to gain access to the methods:
# app/controllers/application_controller.rb
include Pundit
Now, we can use the authorize
method. Instead of all this:
def show
unless PhotoPolicy.new(current_user, @photo).show?
raise Pundit::NotAuthorizedError, "not allowed"
end
end
We can write just this:
def show
authorize @photo
end
🤯 What just happened? When you pass the authorize
method an instance of Photo
:
PhotoPolicy
in app/policies
.current_user
.current_user
as the first argument and whatever you pass to authorize
as the second argument to a new instance of PhotoPolicy
.?
appended on the new policy instance.false
, it raises Pundit::NotAuthorizedError
.In view templates, we now have a policy
helper method that will make it easier to conditionally hide and show things. For example, assuming we define an update?
method in our policy:
<% if policy(@photo).update? %>
<%= link_to "Edit photo", edit_photo_path(@photo) %>
<% end %>
Since policies are just POROs, we can bring all our Ruby skills to bear: inheritance, aliasing, etc.
To start with, we can run the generator rails g pundit:install
which creates a good starting point policy to inherit from in app/policies/application_policy.rb
. Take a look and see what you think. If you like it, let’s inherit from it:
# app/policies/photo_policy.rb
class PhotoPolicy < ApplicationPolicy
So — that means that all we need to do from now on is:
authorize
within every action.If we’ve done #2, which will force us to do #1, that will ensure that we’ve at least thought about who can get into every action.
Try visiting /follow_requests
right now — oops! Quite a big security hole, and one that’s depressingly common to find left open after a resource has been generated with scaffold
.
Pundit includes a method called verify_authorized
that we can call in an after_action
to help enforce the discipline of pruning our unused routes:
after_action :verify_authorized
There’s another method called policy_scope
similar to authorize
that’s used for collections rather than single objects. Usually, you’ll want to ensure that either one or the other is called with something like the following in ApplicationController
:
# app/controllers/application_controller.rb
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
Now try visiting /follow_requests
or some other scaffolded route that was insecurely left reachable. You’ll see that you can’t.
If necessary, you can make the choice to skip_before_action :verify_authorized
on a case-by-case basis, as we did for :authenticate_user!
. We are now secure-by-default instead of insecure-by-default.
There’s more to Pundit that you should read about in the README, but not a ton more. That’s what I like about it — it’s relatively lightweight, but gets the job done well. The way that it is structured also plays great with Rails I18n and other libraries. Another powerful tool for your belt.
A question mark at the end of a method name doesn’t do anything special, functionally; it’s just another letter in the method name, as far as Ruby is concerned.
It’s a convention among Rubyists to end the names of methods that return true
or false
with a question mark, so I’m following that convention here with show?
. ↩