1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rails 6で認証認可入り掲示板APIを構築する #16 policyの設定

Last updated at Posted at 2020-09-21

Rails 6で認証認可入り掲示板APIを構築する #15 pundit導入

post_policyの編集

まずはspec/spec_helper.rbを以下のように変更します。
punditの公式にあるように、以下の通り追加すればpundit用のrspecメソッドが使えるようになります。

spec/spec_helper.rb
 RSpec.configure do |config|
...

+  require "pundit/rspec"
 end

次にspec/policies/post_policy_spec.rbにテストを組み込んでいきます。

spec/policies/post_policy_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe PostPolicy, type: :policy do
  let(:user) { create(:user) }
  let(:post) { create(:post) }

  subject { described_class }

  permissions :index?, :show? do
    it "未ログインの時に許可" do
      expect(subject).to permit(nil, post)
    end
  end

  permissions :create? do
    it "未ログインの時に不許可" do
      expect(subject).not_to permit(nil, post)
    end
    it "ログインしている時に許可" do
      expect(subject).to permit(user, post)
    end
  end

  permissions :update?, :destroy? do
    it "未ログインの時に不許可" do
      expect(subject).not_to permit(nil, post)
    end
    it "ログインしているが別ユーザーの時に不許可" do
      expect(subject).not_to permit(user, post)
    end
    it "ログインしていて同一ユーザーの時に許可" do
      post.user = user
      expect(subject).to permit(user, post)
    end
  end
end

なんとなく読み解けると思いますが、念の為解説を。

  permissions :index?, :show? do
    it "未ログインの時に許可" do
      expect(subject).to permit(nil, post)
    end
  end

index?とshow?は条件が同じのためまとめてテストをしています。
permit(nil, post)は第1引数にログインユーザーmodelを、第2引数に対象modelを指定します。
すると第1引数のユーザーが第2引数のオブジェクトのindex?やshow?の権限があるかテストをします。

  permissions :update?, :destroy? do
...
    it "ログインしているが別ユーザーの時に不許可" do
      expect(subject).not_to permit(user, post)
    end
    it "ログインしていて同一ユーザーの時に許可" do
      post.user = user
      expect(subject).to permit(user, post)
    end
  end

not_toは見たままですが、許可されていないことのテストですね。
そして、postの所有ユーザーを一致したことで最後のテストはパスします。

request specの修正

一旦rspecを動かしてみます。
するとspec/requests/v1/posts_request_spec.rbが結構コケます。

原因は上記と同じく、#updateや#destoryが所属ユーザーのログインじゃないと403になるからです。
ですが、posts_request_spec.rbで使っているauthorized_user_headersヘルパは内部でcreate(:user)をしているため、ログインユーザーとpostユーザーを一致できません。

そのため、以下のように修正を加えます。

spec/support/authorization_spec_helper.rb
 module AuthorizationSpecHelper
-  def authorized_user_headers
-    user = create(:user)
+  def authorized_user_headers(user = nil)
+    user = create(:user) if user.nil?
     post v1_user_session_url, params: { email: user.email, password: "password" }

これで、authorized_user_headersに引数無しで渡した場合は内部でuserが作られ、引数でuserを渡した場合はそれを利用します。

ただしauthorized_user_headersが少し複雑になってしまいrubocopのAbcSizeに引っかかるので、以下の対応をします。

.rubocop.yml

...
+
+# AbcSize デフォルト15はキツいので20に上げる
+Metrics/AbcSize:
+  Max: 20

さて、ようやくrequest specの修正です。

spec/requests/v1/posts_request_spec.rb
...
     it "正常レスポンスコードが返ってくる" do
-      put v1_post_url({ id: update_param[:id] }), params: update_param
+      post = Post.find(update_param[:id])
+      put v1_post_url({ id: update_param[:id] }), params: update_param, headers: authorized_user_headers(post.user)
       expect(response.status).to eq 200
     end
     it "subject, bodyが正しく返ってくる" do
-      put v1_post_url({ id: update_param[:id] }), params: update_param
+      post = Post.find(update_param[:id])
+      put v1_post_url({ id: update_param[:id] }), params: update_param, headers: authorized_user_headers(post.user)
       json = JSON.parse(response.body)
       expect(json["post"]["subject"]).to eq("update_subjectテスト")
       expect(json["post"]["body"]).to eq("update_bodyテスト")
     end
     it "不正パラメータの時にerrorsが返ってくる" do
-      put v1_post_url({ id: update_param[:id] }), params: { subject: "" }
+      post = Post.find(update_param[:id])
+      put v1_post_url({ id: update_param[:id] }), params: { subject: "" }, headers: authorized_user_headers(post.user)
       json = JSON.parse(response.body)
       expect(json.key?("errors")).to be true
     end
@@ -106,13 +109,13 @@ RSpec.describe "V1::Posts", type: :request do
       create(:post)
     end
     it "正常レスポンスコードが返ってくる" do
-      delete v1_post_url({ id: delete_post.id })
+      delete v1_post_url({ id: delete_post.id }), headers: authorized_user_headers(delete_post.user)
       expect(response.status).to eq 200
     end
     it "1件減って返ってくる" do
       delete_post
       expect do
-        delete v1_post_url({ id: delete_post.id })
+        delete v1_post_url({ id: delete_post.id }), headers: authorized_user_headers(delete_post.user)
       end.to change { Post.count }.by(-1)
     end

そこまで大きな変更は無いですね。
authorized_user_headersにpostの所有ユーザーを渡すことで、認可を通過します。

所有ユーザー一致判定メソッドの作成

ここをもう少し直感的に変えていきます。
自分自身のものか判定する処理はpostに限らず、今後もいろいろなmodelで流用しそうですよね。

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

そのため、application_policy.rbに自分自身のものか判定するプライベートメソッドを作ります。

app/policies/application_policy.rb
class ApplicationPolicy
...

+  private
+
+  def mine?
+    @record.user == @user
+  end

   #
   # scope
   #
  class Scope
...

post_policyに反映してみます。

app/policies/post_policy.rb
   def update?
-    @record.user == @user
+    mine?
   end
 
   def destroy?
-    @record.user == @user
+    mine?
   end

スッキリしましたね。
これで、今後はuserの関連を持つmodelが自身が所有している場合のみ実行するactionは、policyファイルを作ってmine?メソッドを配置するだけ。超お手軽ですね。

続き

Rails 6で認証認可入り掲示板APIを構築する #17 管理者権限の追加
連載目次へ

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?