Skip to main content Link Search Menu Expand Document (external link)

Pundit

In this article, we will make an authorization feature that will strengthen our applications security with the help of ruby library Pundit.

Table of Contents

  1. What is Pundit?
  2. How to install Pundit
  3. Usage
  4. Custom error message.
  5. Configure Pundit User
  6. Policies
  7. Policy in View

What is Pundit?

Pundit is ruby library that has a set of helpers, that will walk you through the process of utilizing normal Ruby classes and object-oriented design principles to create a simple, robust, and scalable authorization system.

How to install Pundit

Reminder:

Make sure that your containers are up and running.

In your gemfile, add gem pundit.

gem 'pundit'

Then run bundle install.

 root@0122:/usr/src/app# bundle install
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
...
Installing pundit 2.3.0
Bundle complete! 20 Gemfile dependencies, 92 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

Usage

First, include the pundit library on the application controller.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  before_action :set_locale
+ include Pundit
+
  # ...
end

Generate the default application policy with rails g pundit:install.

 root@0122:/usr/src/app# rails g pundit:install
      create  app/policies/application_policy.rb

Custom error message.

Pundit throws a Pundit::NotAuthorizedError that you may handle in the application controller by using rescue_from. You can configure the user_not_authorized function in any controller.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  before_action :set_locale
  include Pundit
+ rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
  # ...
+ private
+
+ def user_not_authorized(exception)
+   policy_name = exception.policy.class.to_s.underscore
+   flash[:alert] = t "#{policy_name}.#{exception.query}", scope: 'pundit', default: :default,
+                     username: exception.policy.record&.user&.email
+   redirect_to(request.referrer || root_path)
+ end
end

Read Carefully:

NotAuthorizedErrors include information about what query (e.g. :create?), what record (e.g., an instance of Post), and what policy (e.g., an instance of PostPolicy) triggered the error.

Connecting the query, record, and policy characteristics with I18n to create error messages is one method to utilize them just like what we did in the user_not_authorized.

Create a translation for pundit error messages.

# config/locales/pundit.en.yml

en:
  pundit:
    default: You are not authorized to perform this action

Configure Pundit User

In some instances, your controller may not have access to current_user, or your current_user may not be the method that Pundit should call. For this, we can simply add a pundit_user method to our controller.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  # ...
  private
  # ...
+
+ def pundit_user
+   current_user
+ end
end

Policies

Pundit is structured on the concept of policy classes. Policy classes will have method that contains policy or condition that will be checked once you call the pundit authorization.

Let’s modify the validate_post_owner method in our posts controller and make it into post policy. It is highly recommended putting policy classes under app/policies.

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def edit?
    record.user == user
  end

  def update?
    record.user == user
  end

  def destroy?
    record.user == user
  end
end

What happened here is we inherited the application policy as our base class. To explain more about the policy class:

  • Policy takes two parameters and user is the first parameter. Pundit will call the current_user function in your controller to determine what to provide into this parameter.
  • The second parameter is an object whose authorization you wish to verify. This does not have to be an ActiveRecord or an ActiveModel object, it can be anything.
  • The class implements a query method. Typically, this corresponds to the name of a controller action.

Remove the validate_post_owner and use the policy in the posts controller.

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]
  before_action :set_post, only: [:show, :edit, :update, :destroy]
- before_action :validate_post_owner, only: [:edit, :update, :destroy]
  # ...
  def edit
+   authorize @post, :edit?, policy_class: PostPolicy
  end

  def update
+   authorize @post, :update?, policy_class: PostPolicy
    # ...
  end

  def destroy
+   authorize @post, :destroy?, policy_class: PostPolicy
    @post.destroy
    redirect_to posts_path
  end
  
  private
  # ...
- def validate_post_owner
-   unless @post.user == current_user
-     flash[:notice] = 'the post not belongs to you'
-     redirect_to posts_path
-   end
- end
end

Add translation for posts authorization error messages.

# config/locales/pundit.en.yml

en:
  pundit:
    default: You are not authorized to perform this action
+   post_policy:
+     edit?: You are not authorized to edit this post
+     update?: You are not authorized to edit this post

Let’s change our root path to posts page.

# config/routes.rb

Rails.application.routes.draw do
  devise_for :users
- root 'welcome#index'
+ get 'welcome' => 'welcome#index'
+ root 'posts#index'
  # ...
end

Then add sign-in and sign-out links to application layout.

<!-- app/views/layouts/application.html.erb  -->

<!-- ...  -->
+   <% if user_signed_in? %>
+     <%= link_to 'Sign out', destroy_user_session_path, data: { 'turbo-method': :delete } %>
+   <% else %>
+     <%= link_to 'Sign in', new_user_session_path %>
+   <% end %>
    <%= link_to 'EN', params.permit!.merge(locale: 'en') %>
    <%= link_to 'zh-CN', params.permit!.merge(locale: 'zh-CN') %>
    <%= yield %>
  </body>
</html>

Policy in View

Using policy method, you can easily access the instance of policy in both view and controller. This is extremely handy for displaying links or buttons in the view conditionally.

Try it out in our posts page. Replace the condition in our edit and delete links with policy.

<!-- app/views/posts/index.html.erb -->

    <!-- ... -->
      <td>
        <%= link_to 'Show', post_path(post) %>
-       <%= link_to 'Edit', edit_post_path(post) if user_signed_in? && post.user == current_user %>
-       <%= button_to 'Delete', post_path(post), method: :delete if user_signed_in? && post.user == current_user %>
+       <%= link_to 'Edit', edit_post_path(post) if policy(post).edit? %>
+       <%= button_to 'Delete', post_path(post), method: :delete if policy(post).destroy? %>
        <%= link_to 'Comments', post_comments_path(post) %>
        <%= link_to 'New Comment', new_post_comment_path(post) %>
      </td>
    <!-- ... -->

Now we successfully integrated the pundit in our application.


Back to top

Copyright © 2020-2022 Secure Smarter Service, Inc. This site is powered by KodaCamp.