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
- What is Pundit?
- How to install Pundit
- Usage
- Custom error message.
- Configure Pundit User
- Policies
- 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 ofPost
), and what policy (e.g., an instance ofPostPolicy
) 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.