[RUBY] Introducing # 15 pundit to build a bulletin board API with authentication authorization in Rails 6

Building a bulletin board API with authentication authorization in Rails 6 # 14 Seed execution time display

Difference between authentication and authorization

First, insert pundit. pundit is a gem that manages authorization.

What is the difference between authentication and authorization?

[Authentication] is like showing a driver's license. It is a process to prove who you are.

[Authorization] is what kind of car you can ride, which is written on the license. Depending on the license, there are various types of licenses such as those that can only ride a moped, medium-sized and large-sized ones. In other words, even on the system, you know who you are, but you need to manage that this process is not allowed.

And if you can't just authorize with devise (devise_token_auth), you can. However, using an authorization gem such as pundit has many merits such as being able to divide and manage files that are only responsible for authorization processing, and as the application grows, you will feel the advantages.

Install pundit

Now, let's put in pundit. As per the pundit documentation, install and initialize as follows.

Gemfile


+ gem “pundit”
$ bundle

app/controllers/v1/application_controller.rb


 class ApplicationController < ActionController::Base
+  include Pundit
…
 end
$ rails g pundit:install

After installing so far, stop rails s once and restart.

Make a post policy

See also: https://github.com/varvet/pundit#generator

$ rails g pundit:policy post

When executed, a policy file and a spec file will be created.

Now imagine the behavior of a typical bulletin board application.

There are three main files that need to be modified.

In addition, there are files that need to be modified only once by default.

Let's understand the behavior of pundit while modifying these 4 files.

app/controllers/v1/posts_controller.rb


     def index
       posts = Post.includes(:user).order(created_at: :desc).limit(20)
+      authorize posts
       render json: posts
     end
 
     def show
+      authorize @post
       render json: @post
     end
 
     def create
+      authorize Post
       post = current_v1_user.posts.new(post_params)
       if post.save
… 
     def update
+      authorize @post
       if @post.update(post_params)
… 
     def destroy
+      authorize @post
       @post.destroy
…

What should be noted here is where to put ʻauthorize`. I will explain it later in this article.

This authorize {model} will call the corresponding method in post_policy.rb. I haven't fixed post_policy.rb yet, so the superclass application_policy.rb's index? And show? Are called.

app/policy/application_policy.rb


  def index?
    false
  end

ʻIndex?Is false, isn't it? If the return value of the method corresponding to{action}?` is true, it is allowed, and if it is false, it is denied. Therefore, an authentication error will occur.

{"status":500,"error":"Internal Server Error","exception":"#\u003cNameError: undefined local variable or method `current_user' for #\u003cV1::PostsController:0x00000000036a49a8\u003e\nDid you mean?  current_v1_user

This error is quite a songwriter. There is no variable or method called current_user That's the error.

Actually, pundit, by default, calls the method called current_user and passes it to @ user in application_policy.rb and post_policy.rb. However, in this test application, the namespace of v1 is cut, so you have to call current_v1_user instead of current_user.

This can be addressed by overriding the method pundit_user in application_controller.rb.

app/controllers/application_controller.rb


class ApplicationController < ActionController::API
…

+  def pundit_user
+    current_v1_user
+  end

Now that current_v1_user is called in pundit instead of current_user, the previous ʻundefined local variable or method current_user' is resolved. Hit curl again.

{"status":500,"error":"Internal Server Error","exception":"#\u003cPundit::NotAuthorizedError: not allowed to index? this Post::ActiveRecord_Relation

It seems that a 500 error is returned when not allowed. If you don't have permission, a 403 error is appropriate, so it seems good to rescue Pundit :: NotAuthorizedError in application_controller.rb.

app/controllers/application_controller.rb


 class ApplicationController < ActionController::API
   include DeviseTokenAuth::Concerns::SetUserByToken
+  rescue_from Pundit::NotAuthorizedError, with: :render_403
…

+  def render_403
+    render status: 403, json: { message: "You don't have permission." }
+  end
…

Let's try again.

$ curl localhost:8080/v1/posts -i
HTTP/1.1 403 Forbidden
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
Cache-Control: no-cache
X-Request-Id: e19d413c-89c9-4701-94c5-ece2b12560a9
X-Runtime: 0.003657
Transfer-Encoding: chunked

{"message":"You don't have permission."}

The response code of 403 and the message were returned properly. If you try changing the def index? In ʻapp / policy / application_policy.rb` from false to true, the post list will be returned normally. However, application_policy.rb is a superclass, so let's leave everything false here in principle. Edit the inherited subclass post_policy.rb.

Edit post_policy.rb

I will write the final code first.

app/policies/post_policy.rb


# frozen_string_literal: true

#
#post policy class
#
class PostPolicy < ApplicationPolicy
  def index?
    true
  end

  def show?
    true
  end

  def create?
    @user.present?
  end

  def update?
    @record.user == @user
  end

  def destroy?
    @record.user == @user
  end

  #
  # scope
  #
  class Scope < Scope
    def resolve
      scope.all
    end
  end
end

This should work as intended. In addition, I will write the test at the end this time because I give priority to understanding the behavior of pundit. Now, remember where you inserted ʻauthorize` in controller.

app/controllers/v1/posts_controller.rb


     def index
       posts = Post.includes(:user).order(created_at: :desc).limit(20)
+      authorize posts
       render json: posts
     end
 
     def show
+      authorize @post
       render json: @post
     end
 
     def create
+      authorize Post
       post = current_v1_user.posts.new(post_params)
       if post.save
… 
     def update
+      authorize @post
       if @post.update(post_params)
… 
     def destroy
+      authorize @post
       @post.destroy
…

Notice that they are all called ** before the action needs to be done **.

It will be. What if you did ʻauthorize after save or update? If you do not have the authority, the response of 403 will be returned, but since the save process is completed, you should be able to rewrite it on the DB. If that is the case, there is no point in authorization. Also note that authorization will not be done unless you call ʻauthorize in the first place.

In conclusion, you need to be sure to call ʻauthorize` and make sure you know where to call it.

Finally, I will explain the processing of create? And ʻupdate?`.

app/policies/post_policy.rb


  def create?
    @user.present?
  end

@ user will contain current_v1_user, but if you are not logged in, @ user will contain nil. In other words, the above method returns 200 for true if logged in and 403 for false if not logged in.

app/controllers/v1/post_controller.rb


    def create
      authorize Post
      post = current_v1_user.posts.new(post_params)

The controller side is also paying attention. Notice that we are not doing ʻauthorize post under post = current_v1_user.posts.new (post_params) . Because, as mentioned above, current_v1_user is nil, so if you try to call ʻauthorize post underpost = current_v1_user.posts.new (post_params), the posts method does not exist and you get a 500 error.

Since it is not post but ʻuser that is required for judgment, Post` is passed appropriately and authorize is operated.

Second, about the behavior of update? And destory ?.

app/policies/post_policy.rb


  def update?
    @record.user == @user
  end

In this case, since the record to be updated is passed as ʻauthorize @ post in the controller, the record to be updated / deleted is passed to @ record. Compare the ʻuser of the record with the @ user that the current_v1_user is passing to, and determine if they match. So is it your own post? You are judging.

In the next article, I will explain how to test pundit and how to cut out the process into methods.

Continued

Building a bulletin board API with authentication authorization in Rails 6 # 16 policy settings [To the serial table of contents]

Recommended Posts

Introducing # 15 pundit to build a bulletin board API with authentication authorization in Rails 6
Introduced # 10 devise_token_auth to build a bulletin board API with authentication authorization in Rails 6
# 8 seed implementation to build bulletin board API with authentication authorization in Rails 6
Build a bulletin board API with authentication authorization in Rails # 13 Add authentication header
Introduced # 9 serializer to build bulletin board API with authentication authorization in Rails 6
# 6 show, create implementation to build bulletin board API with authentication authorization in Rails 6
Build a bulletin board API with authentication authorization in Rails 6 # 5 controller, routes implementation
Build a bulletin board API with authentication authorization with Rails 6 # 2 Introducing git and rubocop
Build a bulletin board API with authentication authorization in Rails # 17 Add administrator privileges
# 7 update, destroy implementation to build bulletin board API with authentication authorization in Rails 6
Build a bulletin board API with authentication authorization in Rails 6 # 14 seed Execution time display
Build a bulletin board API with authentication and authorization with Rails 6 # 1 Environment construction
Build a bulletin board API with authentication authorization in Rails # 12 Association of user and post
Build a bulletin board API with authentication authorization in Rails 6 # 11 User model test and validation added
Build a bulletin board API with authentication and authorization with Rails # 18 ・ Implementation of final user controller
Build a bulletin board API with authentication authorization with Rails 6 # 3 RSpec, FactoryBot introduced and post model
Building a bulletin board API with authentication authorization with Rails 6 Validation and test implementation of # 4 post
I tried to make a group function (bulletin board) with Rails
Try to create a bulletin board in Java
How to build API with GraphQL and Rails
[How to insert a video in haml with Rails]
How to set up a proxy with authentication in Feign
Introducing React to Rails with react-rails
How to rename a model with foreign key constraints in Rails
Steps to build a Ruby on Rails development environment with Vagrant
[Rails] How to log in with a name by adding a devise name column
How to insert a video in Rails
I tried to build a Firebase application development environment with Docker in 2020
How to build a Ruby on Rails development environment with Docker (Rails 6.x)
Steps to set a favicon in Rails
Create a SPA with authentication function with Rails API mode + devise_token_auth + Vue.js 3 (Rails edition)
How to build a Ruby on Rails development environment with Docker (Rails 5.x)
How to build Rails 6 environment with Docker
How to store data simultaneously in a model associated with a nested form (Rails 6.0.0)
Convert to a tag to URL string in Rails
Create a simple bulletin board with Java + MySQL
[Rails] rails new to create a database with PostgreSQL
One way to redirect_to with parameters in rails
How to implement a like feature in Rails
How to easily create a pull-down in Rails
[Rails] How to build an environment with Docker
How to make a follow function in Rails
[Rails] [Docker] Copy and paste is OK! How to build a Rails development environment with Docker
Docker command to create Rails project with a single blow in environment without Ruby
I made a function to register images with API in Spring Framework. Part 1 (API edition)
How to deal with errors in Rails s could not find a JavaScript runtime.
Throw raw SQL to a read replica in Rails
[Note] Build a Python3 environment with Docker in EC2
How to write a date comparison search in Rails
I want to define a function in Rails Console
How to store Rakuten API data in a table
How to query Array in jsonb with Rails + postgres
[Rails 6] How to set a background image in Rails [CSS]
Build Rails (API) x MySQL x Nuxt.js environment with Docker
[Rails] How to load JavaScript in a specific view
How to get started with creating a Rails app
A new employee tried to create an authentication / authorization function from scratch with Spring Security
I made a function to register images with API in Spring Framework. Part 2 (Client Edition)
Introducing CircleCI to Rails
Introducing Bootstrap to Rails 5
Introducing Bootstrap to Rails !!