Ruby
Article

Straightforward Rails Authorization with Pundit

By Ilya Bodrov-Krukowski

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!

  • http://careersreport.com ruth_boyer2

    Dis is how U can get 97 bucks each hour… After being un-employed for six months , I commenced earning over this web-site and today I possibly could not be more satisfied. 4 months have passed since being on my new job and my income is around five-thousand five hundred $/month If this interests you: 1)navigate to the site link in the description

  • http://www.aerogami.com.br Mohamad El-Husseini

    You can use `assign_attributes` instead of `update_attributes` in your `new` action.

  • Efe

    thanks for the effort, but it’s too long to follow.

  • http://borowskiy.ru Dmitriy Borowskiy

    Thank you, Ilya. Very nice article! This very helped me with authorization.

  • Louise Rains

    I have a problem with this method:

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

    @post is nil. I have tried various options without success. What should the correct line be?

    Other than a few typos, very nice presentation.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.