Advertisement
  1. Code
  2. Ruby

Authorization With Pundit

Scroll to top
Read Time: 9 min

Pundit is a tool that allows you to restrict certain parts of your Rails application to authorized users. It does this by providing you with certain helpers.

In this tutorial, you will build a blog that restricts parts such as creating, updating and deleting articles to authorized users only.

Getting Started

Start by generating a new Rails application.

1
rails new pundit-blog -T

The -T flag tells Rails to generate the new application without the default test suite. Running the command will generate your Rails application and install the default gems.

Go ahead and add the following gems to your Gemfile. You will be using bootstrap-sass for the layout of your application, and Devise will handle user authentication.

1
#Gemfile
2
3
...
4
gem 'bootstrap-sass'
5
gem 'devise'

Run the command to install the gem.

1
bundle install

Now rename app/assets/stylesheets/application.css to app/assets/stylesheets/application.scss. Add the following lines of code to import bootstrap.

1
#app/assets/stylesheets/application.scss
2
3
...
4
@import 'bootstrap-sprockets';
5
@import 'bootstrap';

Create a partial named _navigation.html.erb to hold your navigation code; the partial should be located in app/views/layouts directory. Make the partial look like what I have below.

1
#app/views/layouts/_navigation.html.erb
2
3
<nav class="navbar navbar-inverse">
4
  <div class="container">
5
    <div class="navbar-header">
6
      <%= link_to 'Pundit Blog', root_path, class: 'navbar-brand' %>
7
    </div>
8
    <div id="navbar">
9
 
10
    <ul class="nav navbar-nav pull-right">
11
      <li><% link_to 'Home', root_path %></li>
12
      <ul class="nav navbar-nav pull-right">
13
        <% if user_signed_in? %>
14
        <li><%= current_user.email %></li>
15
        <li><%= link_to 'Log out', destroy_user_session_path, method: :delete %></li>
16
        <% else %>
17
          <li><%= link_to 'Log In', new_user_session_path %></li>
18
          <li><%= link_to 'Sign Up', new_user_registration_path %></li>
19
        <% end %>
20
      </ul>
21
    </ul>
22
  </div>
23
</nav>

For the navigation to be used, you need to render it in your application layout. Tweak your application layout to look like what I have below.

1
#app/views/layouts/application.html.erb
2
3
<!DOCTYPE html>
4
<html>
5
  <head>
6
    <title>Pundit-Blog</title>
7
    <%= csrf_meta_tags %>
8
9
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
10
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
11
  </head>
12
13
  <body>
14
    <%= render "layouts/navigation" %>
15
    <div id="flash">
16
      <% flash.each do |key, value| %>
17
        <div class="flash <%= key %>"><%= value %></div>
18
      <% end %>
19
    </div>
20
    <div class="container-fluid">
21
      <%= yield %>
22
    </div>
23
  </body>
24
</html>

Generate the User Model

Run the command to install Devise.

1
rails generate devise:install

Now generate your User model.

1
rails generate devise User

Migrate your database.

1
rake db:migrate

Generate Article Resources

Run the command to generate your Article resources.

1
rails generate scaffold Articles title:string body:text

This will generate your ArticlesController and Article Model. It will also generate the views needed.

Now migrate your database by running:

1
rake db:migrate

Open up app/views/articles/_form.html.erb and make it look like what I have below.

1
#app/views/articles/_form.html.erb
2
3
<%= form_for(article) do |f| %>
4
  <% if article.errors.any? %>
5
    <div id="error_explanation">
6
      <h2><%= pluralize(article.errors.count, "error") %> prohibited this article from being saved:</h2>
7
8
      <ul>
9
      <% article.errors.full_messages.each do |message| %>
10
        <li><%= message %></li>
11
      <% end %>
12
      </ul>
13
    </div>
14
  <% end %>
15
16
  <div class="field">
17
    <%= f.label :title %>
18
    <%= f.text_field :title %>
19
  </div>
20
21
  <div class="field">
22
    <%= f.label :body %>
23
    <%= f.text_area :body %>
24
  </div>
25
26
  <div class="actions">
27
    <%= f.submit %>
28
  </div>
29
<% end %>

For your index file, it should look like this.

1
#app/views/articles/index.html.erb
2
3
<table class="table table-bordered table-striped table-condensed table-hover">
4
  <thead>
5
  <tr>
6
    <th>Title</th>
7
    <th>Body</th>
8
    <th colspan="3"></th>
9
  </tr>
10
  </thead>
11
12
  <tbody>
13
    <% @articles.each do |article| %>
14
    <tr>
15
      <td><%= article.title %></td>
16
      <td><%= article.body %></td>
17
      <td><%= link_to 'Show', article %></td>
18
      <td><%= link_to 'Edit', edit_article_path(article) %></td>
19
      <td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %></td>
20
    </tr>
21
    <% end %>
22
  </tbody>
23
</table>
24
25
<br>
26
27
<%= link_to 'New article', new_article_path %>

The above code arranges the articles on the index page into a table format to make it look presentable.

Open up your routes file and add the route for articles resources.

1
#config/routes.rb
2
3
...
4
  resources :articles
5
  root to: "articles#index"

Integrate Pundit

Add the Pundit gem to your Gemfile.

1
#Gemfile
2
3
...
4
gem 'pundit'

Run the command to install.

1
bundle install

Integrate Pundit to your application by adding the following line to your ApplicationController.

1
#app/controllers/application_controller.rb
2
3
...
4
  include Pundit
5
...

Run Pundit's generator.

1
rails g pundit:install

This will generate an app/policies folder which contains a base class with policies. Each policy is a basic Ruby class.

This is how the base class policy looks.

1
#app/policies/application_policy.rb
2
3
class ApplicationPolicy
4
  attr_reader :user, :record
5
6
  def initialize(user, record)
7
    @user = user
8
    @record = record
9
  end
10
11
  def index?
12
    false
13
  end
14
15
  def show?
16
    scope.where(:id => record.id).exists?
17
  end
18
19
  def create?
20
    false
21
  end
22
23
  def new?
24
    create?
25
  end
26
27
  def update?
28
    false
29
  end
30
31
  def edit?
32
    update?
33
  end
34
35
  def destroy?
36
    false
37
  end
38
39
  def scope
40
    Pundit.policy_scope!(user, record.class)
41
  end
42
43
  class Scope
44
    attr_reader :user, :scope
45
46
    def initialize(user, scope)
47
      @user = user
48
      @scope = scope
49
    end
50
51
    def resolve
52
      scope
53
    end
54
  end
55
end

Create the Article Policy

Now you need to write your own policy. For this tutorial, you want to allow only registered users to create new articles. In addition to that, only creators of an article should be able to edit and delete the article.

To achieve this, your article policy will look like this.

1
#app/policies/article_policy.rb
2
3
class ArticlePolicy < ApplicationPolicy
4
  def index?
5
    true
6
  end
7
8
  def create?
9
    user.present?
10
  end
11
12
  def update?
13
    return true if user.present? && user == article.user
14
  end
15
16
  def destroy?
17
    return true if user.present? && user == article.user
18
  end
19
20
  private
21
22
    def article
23
      record
24
    end
25
end

In the above, you are permitting everyone (registered and non-registered users) to see the index page. To create a new article, a user has to be registered. You use user.present? to find out if the user trying to perform the action is registered.

For updating and deleting, you want to make sure that only the user who created the article is able to perform these actions.

At this point, you need to establish a relationship between your Article and User model.

You do so by generating a new migration.

1
rails generate migration add_user_id_to_articles user:references

Next, migrate your database by running the command:

1
rake db:migrate

Open the User model and add the line that seals the relationship.

1
#app/models/user.rb
2
3
...
4
  has_many :articles

Your Article model should have this.

1
#app/models/article.rb
2
3
...
4
  belongs_to :user

Now you need to update your ArticlesController so it is in sync with what you have done so far.

1
#app/controllers/articles_controller.rb
2
3
class ArticlesController < ApplicationController
4
  before_action :set_article, only: [:show, :edit, :update, :destroy]
5
6
  # GET /articles
7
  # GET /articles.json
8
  def index
9
    @articles = Article.all
10
    authorize @articles
11
  end
12
13
  # GET /articles/1
14
  # GET /articles/1.json
15
  def show
16
  end
17
18
  # GET /articles/new
19
  def new
20
    @article = Article.new
21
    authorize @article
22
  end
23
24
  # GET /articles/1/edit
25
  def edit
26
  end
27
28
  # POST /articles
29
  # POST /articles.json
30
  def create
31
    @article = Article.new(article_params)
32
    @article.user = current_user
33
    authorize @article
34
35
    respond_to do |format|
36
      if @article.save
37
        format.html { redirect_to @article, notice: 'Article was successfully created.' }
38
        format.json { render :show, status: :created, location: @article }
39
      else
40
        format.html { render :new }
41
        format.json { render json: @article.errors, status: :unprocessable_entity }
42
      end
43
    end
44
  end
45
46
  # PATCH/PUT /articles/1
47
  # PATCH/PUT /articles/1.json
48
  def update
49
    respond_to do |format|
50
      if @article.update(article_params)
51
        format.html { redirect_to @article, notice: 'Article was successfully updated.' }
52
        format.json { render :show, status: :ok, location: @article }
53
      else
54
        format.html { render :edit }
55
        format.json { render json: @article.errors, status: :unprocessable_entity }
56
      end
57
    end
58
  end
59
60
  # DELETE /articles/1
61
  # DELETE /articles/1.json
62
  def destroy
63
    @article.destroy
64
    respond_to do |format|
65
      format.html { redirect_to articles_url, notice: 'Article was successfully destroyed.' }
66
      format.json { head :no_content }
67
    end
68
  end
69
70
  private
71
    # Use callbacks to share common setup or constraints between actions.
72
    def set_article
73
      @article = Article.find(params[:id])
74
      authorize @article
75
    end
76
77
    # Never trust parameters from the scary internet, only allow the white list through.
78
    def article_params
79
      params.require(:article).permit(:title, :body, :user_id)
80
    end
81
end

At this point in your application, you have successfully implemented the policies that restrict certain parts of your application to selected users.

You want to add a standard error message that shows whenever a non-authorized user tries to access a restricted page. To do so, add the following to your ApplicationController.

1
#app/controllers/application_controller.rb
2
3
...
4
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
5
6
  private
7
8
    def user_not_authorized
9
      flash[:warning] = "You are not authorized to perform this action."
10
      redirect_to(request.referrer || root_path)
11
    end

This code simply renders a basic text that tells the user s/he is not authorized to perform the action.

Run:

1
$ rails server

To start your Rails server, point your browser to https://localhost:3000 to see what you have.

Conclusion

In this tutorial, you learned how to work with both Devise and Pundit. You were able to create policies that allowed only authorized users to view certain parts of the application. You also created a basic error text that shows when a non-authorized user tries to access a restricted part of the application.

You can learn more about Pundit by checking the GitHub page.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.