Straightforward Rails Authorization with Pundit

Share this article

LTKkxELyc

This is a second article in the “Authorization with Rails” series. In the previous article, we discussed CanCanCan, a widely known solution started by Ryan Bates and now supported by a group of enthusiasts. Today I am going to introduce you a bit less popular, but still viable, library called Pundit created by the folks of ELabs.

“Pundit” means “a scholar”, “a clever guy” (sometimes used as a negative description), but you don’t need to be a genius to use it in your projects. Pundit is really easy to understand and, believe me, you’ll love it. I fell in love it when I browsed its documentation.

The idea behind Pundit is employing plain old Ruby classes and methods without involving any special DSLs. This gem only adds a couple of useful helpers, so, all in all, you can craft your system the way you see fit. This solution is a bit more low-level than CanCanCan, and it is really interesting to compare the two.

In this article we will discuss all of Pundit’s main features: working with access rules, using helper methods, and scoping and defining permitted attributes.

The working demo is available at sitepoint-pundit.herokuapp.com.

The source code can be found on GitHub.

Preparations

Our preparations will be super fast. Go ahead and create a new Rails app. Pundit works with Rails 3 and 4, but for this demo version 4.1 will be used.

Drop in the following gems into your Gemfile:

Gemfile

[...]
gem 'pundit'
gem 'clearance'
gem 'bootstrap-sass'
[...]

Clearance will be used to set up authentication very quickly. This gem was covered in my “Simple Rails Authentication with Clearance” article, so you can refer to it for more information.

Bootstrap will be used for basic styling, though you may skip this as always.

Don’t forget to run

$ bundle install

Now, run Clearance’s generator that is going to create a User model and some basic configuration:

$ rails generate clearance:install

Modify the layout to include flash messages, as Clearance and Pundit rely on them to display information to the user:

layouts/application.html.erb

[...]
<div id="flash">
  <% flash.each do |key, value| %>
    <div class="alert alert-<%= key %>"><%= value %></div>
  <% end %>
</div>
[...]

Create a new scaffold for Post:

$ rails g scaffold Post title:string body:text

We also want our users to log in before working with the app:

application_controller.rb

[...]
before_action :require_login
[...]

Set up the root route:

routes.rb

[...]
root to: 'posts#index'
[...]

Admin User

To finish setting up our lab environment, we need to add an admin field inside the users table, so modify the migration like this:

xxx_create_users.rb

[...]
t.boolean :admin, default: false, null: false
[...]

Run the migrations:

$ rake db:migrate

Lastly, let’s add a small button to easily switch between admin states:

layouts/application.html.erb

[...]
<% if current_user %>
  <div class="well well-sm">
    Admin: <strong><%= current_user.admin? %></strong><br>
    <%= link_to 'Toggle admin rights', user_path(current_user), method: :patch, class: 'btn btn-info' %>
  </div>
<% end %>
[...]

We have to check if current_user is present, otherwise non-authenticated users will see an error.

Add a route:

routes.rb

[...]
resources :users, only: [:update]
[...]

and a controller:

users_controller.rb

class UsersController < ApplicationController
  def update
    @user = User.find(params[:id])
    @user.toggle!(:admin)
    flash[:success] = 'OK!'
    redirect_to root_path
  end
end

That’s it! The lab environment is ready, so now we can start playing with Pundit.

Integrating Pundit

To start off, add the following line to ApplicationController:

application_controller.rb

[...]
include Pundit
[...]

Next, run Pundit’s generator:

$ rails g pundit:install

This is going to create a base class with policies inside the app/policies folder. Policy classes are the core of Pundit and we are going to work with them extensively. The base policy class looks like this:

app/policies/application_policy.rb

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    raise Pundit::NotAuthorizedError, "must be logged in" unless user
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    scope.where(:id => record.id).exists?
  end

  def create?
    false
  end

  def new?
    create?
  end

  # [...]
  # some stuff omitted

  class Scope
    # [...]
  end
end

Each policy is a basic Ruby class, but you have to keep in mind a couple of things:

  • Policies should be named after a model they belong to, but prefixed with the word Policy. For example, use PostPolicy for the Post model. If you don’t have an associated model, you still can use Pundit – read more here.
  • The first argument to the initialize method is a user record. Pundit uses the current_user method to get it, but if you do not have such a method, this behavior can be changed by overriding pundit_user. Read more here.
  • The second argument is a model object. It does not have to be an ActiveModel object though.
  • The policy class has to implement query methods like create? or new? to check access rights.

If you are using the base policy class and inherit from it, you don’t really need to worry about most of this stuff, but there may be times when you need a custom policy class (for example, when you don’t have a corresponding model).

Providing Access Rules

Now, let’s write our first access rule. For example, we only want admin users to be able to destroy posts. That’s easy to do! Create a new post_policy.rb file inside the policies folder and paste the following code:

policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def destroy?
    user.admin?
  end
end

As you can see, the destroy? method is going to return true or false, indicating whether a user is authorized to perform an action.

Inside the controller we need to check our rule:

posts_controller.rb

[...]
def destroy
  authorize @post
  @post.destroy

  redirect_to posts_url, notice: 'Post was successfully destroyed.'
end
[...]

authorize accepts a second optional argument to provide the name of the rule to use. This is useful if your action’s name is different, for example:

def publish
  authorize @post, :update?
end

You can also pass a class name instead of an instance, for example, if you don’t have a resource to work with:

authorize Post

If the user is not allowed to perform the action, an error will be raised. We need to rescue from it and display a useful message instead. The easiest solution will be to just render some basic text:

application_controller.rb

[...]
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

private

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

However, you might need to set up a custom message for different cases or translate it for other languages. That’s possible as well:

application_controller.rb

[...]
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

private

def user_not_authorized(exception)
  policy_name = exception.policy.class.to_s.underscore
  flash[:warning] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
  redirect_to(request.referrer || root_path)
end
[...]

Don’t forget to update translations file:

config/locales/en.yml

en:
 pundit:
   default: 'You cannot perform this action.'
   post_policy:
     destroy?: 'You cannot destroy this post!'

If a user cannot destroy a post, there is no point in rendering the “Destroy” button. Luckily, Pundit provides a special helper method:

views/posts/index.html.erb

[...]
<% if policy(post).destroy? %>
  <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
[...]

Go ahead and check it out! Everything should be working nicely.

You might be thinking that inside the policies we don’t handle a case when a user is not authenticated at all. To fix it, simply add the following line:

policies/application_policy.rb

[...]
def initialize(user, record)
  raise Pundit::NotAuthorizedError, "must be logged in" unless user
  @user = user
  @record = record
end
[...]

Just don’t forget to rescue from Pundit::NotAuthorizedError error inside the ApplicationController.

Setting Up Relations

Now, let’s set up a one-to-many relationship between posts and users. Create a new migration and apply it:

$ rails g migration add_user_id_to_posts user:references
$ rake db:migrate

Modify model files:

models/post.rb

[...]
belongs_to :user
[...]

models/user.rb

[...]
has_many :posts
[...]

Also, it would be great to populate the database with some demo records. Use seeds.rb for that:

20.times do |i|
  Post.create({title: "Post #{i + 1}", body: 'test body', user_id: i > 10 ? 1 : 2})
end

We just create 20 posts belonging to different users. Feel free to modify this code as needed.

Run

$ rake db:seed

to populate the database.

Modify the policy to allow users to destroy their own posts:

policies/post_policy.rb

[...]
def destroy?
  user.admin? || record.user == user
end
[...]

As you probably remember, record is being set to the object with which we are working. This is nice, but we can do more. How about creating a scope to load only the posts that user owns? Pundit supports that, too!

Working with Scopes

First of all, create a new, non-RESTful action:

posts_controller.rb

def user_posts
end

routes.rb

resources :posts do
  collection do
    get '/user_posts', to: 'posts#user_posts', as: :user
  end
end

Add a top menu:

layouts/application.html.erb

[...]
<nav class="navbar navbar-inverse">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Pundit', root_path, class: 'navbar-brand' %>
    </div>
    <div id="navbar">
      <ul class="nav navbar-nav">
        <li><%= link_to 'All posts', posts_path %></li>
        <li><%= link_to 'Your posts', user_posts_path %></li>
      </ul>
    </div>
  </div>
</nav>
[...]

We need to create an actual scope. Inside the application_policy.rb file there is a Scope class that has the following code:

policies/application_policy.rb

class Scope
  attr_reader :user, :scope

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

  def resolve
    scope
  end
end

Just as with policies, there are a couple of things to take into consideration:

  • The class should be named Scope and nested inside your policy class.
  • The first argument passed to the initialize method is user, just like with policies.
  • The second argument is scope (an instance of ActiveRecord or ActiveRecord::Relation or anything else).
  • The Scope class implements a method called resolve that returns an iterable result.

Just inherit from the base class and implement your own resolve method:

policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      scope.where(user: user)
    end
  end
  [...]
end

This scope simply loads all the posts that belong to a user. Use this new scope inside your controller:

posts_controller.rb

[...]
def user_posts
  @posts = policy_scope(Post)
end
[...]

You may want to extract some code from the index view into partials (and prettify the table a bit):

views/posts/_post.html.erb

<tr>
  <td><%= post.title %></td>
  <td><%= post.body %></td>
  <td><%= link_to 'Show', post %></td>
  <td><%= link_to 'Edit', edit_post_path(post) %></td>
  <% if policy(post).destroy? %>
    <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  <% end %>
</tr>

views/posts/_list.html.erb

<table class="table table-bordered table-striped table-condensed table-hover">
  <thead>
  <tr>
    <th>Title</th>
    <th>Body</th>
    <th colspan="3"></th>
  </tr>
  </thead>

  <tbody>
  <%= render @posts %>
  </tbody>
</table>

<br>

<%= link_to 'New Post', new_post_path %>

views/posts/index.html.erb

<h1>Listing Posts</h1>

<%= render 'list' %>

Now, just create a new view:

views/posts/user_posts.html.erb

<h1>Your Posts</h1>

<%= render 'list' %>

Boot the server and observe the result!

Inside the views you may use the following code to select only the required records:

<% policy_scope(@user.posts).each do |post| %>
<% end %>

Enforcing Authorization

If you want to check that authorization did take place in your controller, use verify_authorized as an after action. You can also choose what actions to check:

posts_controller.rb

[...]
after_action :verify_authorized, only: [:destroy]
[...]

The same can be done to ensure scoping took place:

posts_controller.rb

[...]
after_action :verify_policy_scoped, only: [:user_posts]
[...]

Under some circumstances, however, it may be unreasonable to perform the authorization check, so you might want to skip it. For example, if a record to destroy wasn’t found, we just return back without any further actions. For this skip_authorization method is used:

[...]
def destroy
  if @post.present?
    authorize @post
    @post.destroy
  else
    skip_authorization
  end

  redirect_to posts_url, notice: 'Post was successfully destroyed.'
end

private

def set_post
  @post = Post.find_by(id: params[:id])
end
[...]

Permitted Parameters

The last feature we are going to discuss is an ability to define permitted parameters in Pundit’s policies. Note that you have to use Rails 4 or Rails 3 with the strong_params gem for this to work properly.

Suppose, we have some “special” parameter that only administrators should be able to modify. Let’s add the corresponding migration:

$ rails g migration add_special_to_posts special:boolean

Modify the migration a bit:

xxx_add_special_to_posts.rb

[...]
add_column :posts, :special, :boolean, default: false
[...]

and apply it:

$ rake db:migrate

Now, define a new method in your policies:

policies/post_policy.rb

[...]
def permitted_attributes
  if user.admin?
    [:title, :body, :special]
  else
    [:title, :body]
  end
end
[...]

So, an administrator can set all attributes, whereas users can only modify title and body. Lastly, update the controller methods:

posts_controller.rb

[...]
def create
  @post = Post.new
  @post.update_attributes(permitted_attributes(@post))

  if @post.save
    redirect_to @post, notice: 'Post was successfully created.'
  else
    render :new
  end
end

def update
  if @post.update(permitted_attributes(@post))
    redirect_to @post, notice: 'Post was successfully updated.'
  else
    render :edit
  end
end
[...]

permitted_attributes is a helper method that expects a resource to be passed. There is a small gotcha inside the create method: you need to initialize @post and only then set its attributes. If you do this:

@post = Post.new(permitted_attributes(@post))

an error will be raised, because you are trying to pass a non-existant object to permitted_attributes.

Instead of employing permitted_attributes, you can stick to the basic post_params and modify it like this:

posts_controller.rb

[...]
def post_params
  params.require(:post).permit(policy(@post).permitted_attributes)
end
[...]

Lastly, modify the views:

views/posts/_form.html.erb

[...]
<div class="field">
  <%= f.label :special %><br>
  <%= f.check_box :special %>
</div>
[...]

views/posts/_list.html.erb

[...]
<thead>
<tr>
  <th>Title</th>
  <th>Body</th>
  <th>Special?</th>
  <th colspan="3"></th>
</tr>
</thead>
[...]

views/posts/_post.html.erb

<tr>
  <td><%= post.title %></td>
  <td><%= post.body %></td>
  <td><%= post.special? %></td>
  <td><%= link_to 'Show', post %></td>
  <td><%= link_to 'Edit', edit_post_path(post) %></td>
  <% if policy(post).destroy? %>
    <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  <% end %>
</tr>

Now boot the server and trying setting the “special” parameter to true while being a basic user – you shouldn’t be able to do so.

Conclusion

In this article we’ve discussed Pundit – a great authorization solution employing basic Ruby classes. We’ve taken a look at most of its features. I encourage you to browse its documentation, because you might be interested in how to provide additional context or manually specify the policy class.

Have you ever used Pundit before? Would you consider using it in future? Do you think it is more convenient than CanCanCan or is it just a different solution? Share your opinion in the comments!

As always, I thank you for staying with me. If you want me to cover a topic, don’t hesitate to ask. See you!

Frequently Asked Questions (FAQs) about Rails Authorization with Pundit

What is the Pundit gem in Ruby on Rails?

Pundit is a Ruby gem that provides a set of helpers to build a simple, robust and scalable authorization system in Ruby on Rails applications. It uses plain Ruby classes and object-oriented design patterns to make the authorization rules easy to read, write, and maintain. Pundit provides a flexible way to manage permissions and access control to resources in your application, based on the roles and privileges of the current user.

How do I install and set up Pundit in my Rails application?

To install Pundit, you need to add the gem to your Gemfile and run the bundle install command. After that, you can generate the Pundit installation by running the rails g pundit:install command. This will create an application policy with some basic setup. You can then create individual policies for your models using the rails g pundit:policy model_name command.

How does Pundit handle authorization in Rails?

Pundit uses policy classes to define the authorization rules. Each policy class corresponds to a model in your application and contains methods that correspond to the actions you want to authorize. These methods return a boolean value indicating whether the user is authorized to perform the action or not. You can use these methods in your controllers and views to control access to resources.

How can I use Pundit to authorize actions in my controllers?

You can use the authorize method provided by Pundit in your controller actions to check if the current user is authorized to perform the action. The authorize method takes two arguments: the record you want to authorize the action on, and the action you want to authorize. If the user is not authorized, Pundit will raise a Pundit::NotAuthorizedError.

How can I use Pundit to control access in my views?

Pundit provides the policy method that you can use in your views to check if the current user is authorized to perform a certain action. The policy method takes a record and returns the policy for that record. You can then call the authorization methods on the policy to check if the user is authorized.

How can I handle Pundit::NotAuthorizedError in my application?

You can handle Pundit::NotAuthorizedError in your application controller by rescuing the exception and redirecting the user to a safe location with a flash message. You can also customize the error message by defining a user_not_authorized method in your application policy.

How can I test my Pundit policies?

You can test your Pundit policies using any Ruby testing framework. Pundit provides a set of matchers that you can use in your tests to check if the policies authorize or deny the actions as expected.

Can I use Pundit with Devise or other authentication systems?

Yes, Pundit works well with any authentication system including Devise. Pundit uses the current_user method to get the user for authorization, so as long as your authentication system provides a current_user method, you can use Pundit.

Can I use Pundit for complex authorization scenarios?

Yes, Pundit is very flexible and can handle complex authorization scenarios. You can define any number of methods in your policy classes to handle different authorization rules. You can also use inheritance and composition to share common rules between policies.

How can I debug my Pundit policies?

You can debug your Pundit policies by raising exceptions in your policy methods and inspecting the error messages. You can also use the Rails console to test your policies interactively.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

authorizationGlennGrails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week