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.
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.
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.
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 **.
render json
render json
@ post.destroy
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.
→ Building a bulletin board API with authentication authorization in Rails 6 # 16 policy settings [To the serial table of contents]