←Rails 6で認証認可入り掲示板APIを構築する #17 管理者権限の追加
全18回に渡る連載も今回で終了です。
今回はuser controllerを作ります。
今までの集大成なので、例示するコードを見ずに作ってみることをオススメします。
仕様
主に管理者がユーザー管理をする用途と、ユーザーが自分自身の更新削除をするための機能群として想定しています。
- #index 管理者のみ閲覧可能
 - #show 自分自身か管理者のみ閲覧可能
 - #create 管理者のみ作成可能
 - #update 自分自身か管理者のみ更新可能
 - #destroy 管理者のみ削除可能
 
手順
- users_controllerの作成
 - user_policyの作成
 - user_policyテストの実装
 - user_policyの実装
 - users_controllerテストの実装
 - users_controllerの実装
 
という手順で進めてみます。
1. users_controllerの作成
実装例
$ rails g controller v1/users
rubocopに怒られないように微修正したファイル群がこちら。
# frozen_string_literal: true
module V1
  #
  #  users controller
  #
  class UsersController < ApplicationController
  end
end
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "V1::Users", type: :request do
end
2. user_policyの作成
実装例
コマンド叩いてファイル作ります。
$ rails g pundit:policy user
rubocopに怒られないように微修正を加えたらとりあえず完成。
# frozen_string_literal: true
#
# userのポリシークラス
#
class UserPolicy < ApplicationPolicy
  #
  # scope
  #
  class Scope < Scope
    def resolve
      scope.all
    end
  end
end
# frozen_string_literal: true
require "rails_helper"
RSpec.describe UserPolicy, type: :policy do
  let(:user) { User.new }
  subject { described_class }
  permissions ".scope" do
    pending "add some examples to (or delete) #{__FILE__}"
  end
  permissions :show? do
    pending "add some examples to (or delete) #{__FILE__}"
  end
  permissions :create? do
    pending "add some examples to (or delete) #{__FILE__}"
  end
  permissions :update? do
    pending "add some examples to (or delete) #{__FILE__}"
  end
  permissions :destroy? do
    pending "add some examples to (or delete) #{__FILE__}"
  end
end
3. user_policyテストの実装
実装例
仕様のおさらい。
- #index 管理者のみ閲覧可能
 - #show 自分自身か管理者のみ閲覧可能
 - #create 管理者のみ作成可能
 - #update 自分自身か管理者のみ更新可能
 - #destroy 管理者のみ削除可能
 
index, #create, #destroyで共通化、#show, #updateで共通化できそうですね。
それを念頭に実装したのがこちら。
# frozen_string_literal: true
require "rails_helper"
RSpec.describe UserPolicy, type: :policy do
  let(:user) { create(:user) }
  let(:another_user) { create(:user) }
  let(:admin_user) { create(:user, :admin) }
  subject { described_class }
  permissions :index?, :create?, :destroy? do
    it "未ログインの時に不許可" do
      expect(subject).not_to permit(nil, user)
    end
    it "adminユーザー以外でログインしている時に不許可" do
      expect(subject).not_to permit(user, user)
    end
    it "adminユーザーでログインしている時に許可" do
      expect(subject).to permit(admin_user, user)
    end
  end
  permissions :show?, :update? do
    it "未ログインの時に不許可" do
      expect(subject).not_to permit(nil, user)
    end
    it "ログインしているが別ユーザーの時に不許可" do
      expect(subject).not_to permit(user, another_user)
    end
    it "adminユーザーでログインしている時に許可" do
      expect(subject).to permit(admin_user, user)
    end
    it "ログインしていて同一ユーザーの時に許可" do
      expect(subject).to permit(user, user)
    end
  end
end
another_userという別ユーザーを定義しているのがポイント。
さて、この時点ではpolicyを設定していないのでテストがいくつかコケることを確認してください。
4. user_policyの実装
実装例
# frozen_string_literal: true
#
# userのポリシークラス
#
class UserPolicy < ApplicationPolicy
  def index?
    admin?
  end
  def show?
    me? || admin?
  end
  def create?
    admin?
  end
  def update?
    me? || admin?
  end
  def destroy?
    admin?
  end
  private
  def me?
    @record == @user
  end
  #
  # scope
  #
  class Scope < Scope
    def resolve
      scope.all
    end
  end
end
me?というprivateメソッドをuser_policy.rbに定義しているのがポイントです。
application_policy.rbにあるmine?は、@record.user == @userと、@recordの持つuserが自分のものか比較していました。
ですが今回は@recordと@userが一致するか確認するので、英語の文法的にmine?ではなくme?になります。その上、user自身と比較するのは現時点ではusers_controllerでしかなさそうなので、application_policyではなくuser_policyに提議しました。
あとは特筆することもなく、テストが通過するようになったはずです。
5. users_controllerテストの実装
実装例
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "V1::Users", type: :request do
  before do
    @user = create(:user, name: "userテスト")
    @authorized_headers = authorized_user_headers @user
    admin = create(:user, :admin)
    @authorized_admin_headers = authorized_user_headers admin
  end
  describe "GET /v1/users#index" do
    before do
      create_list(:user, 3)
    end
    it "正常レスポンスコードが返ってくる" do
      get v1_users_url, headers: @authorized_admin_headers
      expect(response.status).to eq 200
    end
    it "件数が正しく返ってくる" do
      get v1_users_url, headers: @authorized_admin_headers
      json = JSON.parse(response.body)
      expect(json["users"].length).to eq(3 + 2) # headers用2件を含む
    end
    it "id降順にレスポンスが返ってくる" do
      get v1_users_url, headers: @authorized_admin_headers
      json = JSON.parse(response.body)
      first_id = json["users"][0]["id"]
      expect(json["users"][1]["id"]).to eq(first_id - 1)
      expect(json["users"][2]["id"]).to eq(first_id - 2)
      expect(json["users"][3]["id"]).to eq(first_id - 3)
      expect(json["users"][4]["id"]).to eq(first_id - 4)
    end
  end
  describe "GET /v1/users#show" do
    it "正常レスポンスコードが返ってくる" do
      get v1_user_url({ id: @user.id }), headers: @authorized_headers
      expect(response.status).to eq 200
    end
    it "nameが正しく返ってくる" do
      get v1_user_url({ id: @user.id }), headers: @authorized_headers
      json = JSON.parse(response.body)
      expect(json["user"]["name"]).to eq("userテスト")
    end
    it "存在しないidの時に404レスポンスが返ってくる" do
      last_user = User.last
      get v1_user_url({ id: last_user.id + 1 }), headers: @authorized_headers
      expect(response.status).to eq 404
    end
  end
  describe "POST /v1/users#create" do
    let(:new_user) do
      attributes_for(:user, name: "create_nameテスト", email: "email+create_test@example.com", admin: true)
    end
    it "正常レスポンスコードが返ってくる" do
      post v1_users_url, params: new_user, headers: @authorized_admin_headers
      expect(response.status).to eq 200
    end
    it "1件増えて返ってくる" do
      expect do
        post v1_users_url, params: new_user, headers: @authorized_admin_headers
      end.to change { User.count }.by(1)
    end
    it "name, email, adminが正しく返ってくる" do
      post v1_users_url, params: new_user, headers: @authorized_admin_headers
      json = JSON.parse(response.body)
      expect(json["user"]["name"]).to eq("create_nameテスト")
      expect(json["user"]["email"]).to eq("email+create_test@example.com")
      expect(json["user"]["admin"]).to be true
    end
    it "不正パラメータの時にerrorsが返ってくる" do
      post v1_users_url, params: {}, headers: @authorized_admin_headers
      json = JSON.parse(response.body)
      expect(json.key?("errors")).to be true
    end
  end
  describe "PUT /v1/users#update" do
    let(:update_param) do
      update_param = attributes_for(:user, name: "update_nameテスト", email: "email+update_test@example.com", admin: true)
      update_param[:id] = @user.id
      update_param
    end
    it "正常レスポンスコードが返ってくる" do
      put v1_user_url({ id: update_param[:id] }), params: update_param, headers: @authorized_headers
      expect(response.status).to eq 200
    end
    it "name, email, adminが正しく返ってくる" do
      put v1_user_url({ id: update_param[:id] }), params: update_param, headers: @authorized_headers
      json = JSON.parse(response.body)
      expect(json["user"]["name"]).to eq("update_nameテスト")
      expect(json["user"]["email"]).to eq("email+update_test@example.com")
      expect(json["user"]["admin"]).to be false # admin権限は書き換えできると困るのでfalseのまま
    end
    it "不正パラメータの時にerrorsが返ってくる" do
      put v1_user_url({ id: update_param[:id] }), params: { name: "" }, headers: @authorized_headers
      json = JSON.parse(response.body)
      expect(json.key?("errors")).to be true
    end
    it "存在しないidの時に404レスポンスが返ってくる" do
      last_user = User.last
      put v1_user_url({ id: last_user.id + 1 }), params: update_param, headers: @authorized_admin_headers
      expect(response.status).to eq 404
    end
  end
  describe "DELETE /v1/users#destroy" do
    it "正常レスポンスコードが返ってくる" do
      delete v1_user_url({ id: @user.id }), headers: @authorized_admin_headers
      expect(response.status).to eq 200
    end
    it "1件減って返ってくる" do
      expect do
        delete v1_user_url({ id: @user.id }), headers: @authorized_admin_headers
      end.to change { User.count }.by(-1)
    end
    it "存在しないidの時に404レスポンスが返ってくる" do
      last_user = User.last
      delete v1_user_url({ id: last_user.id + 1 }), headers: @authorized_admin_headers
      expect(response.status).to eq 404
    end
  end
end
いくつかpostの時とは違う考慮点があります。
- headerを作る際に
create(:user)をしているので、その2件分を考慮してテストを書くこと。- 存在しないid確認の際も同様、last_userを取得して+1
 
 - adminフラグは自由に書き換えれてしまうと困るので、createでadminフラグは反映されるがupdateでは反映されないこと
 
あたりを取り入れています。
また、レスポンスにadmin判定があるので、serializerの修正も必要ですね。
 # user serializer
 #
 class UserSerializer < ActiveModel::Serializer
-  attributes :id, :name, :email
+  attributes :id, :name, :email, :admin
 end
以上で準備完了。次にcontroller実装に入ります。
6. users_controllerの実装
実装例
controllerにアクセスできるようにするため、routesの修正をします。
 Rails.application.routes.draw do
   namespace "v1" do
     resources :posts
+    resources :users
     mount_devise_token_auth_for "User", at: "auth"
   end
続いてcontrollerです。
こちらはpostのほぼ流用でいけますね。
# frozen_string_literal: true
module V1
  #
  #  users controller
  #
  class UsersController < ApplicationController
    before_action :set_user, only: %i[show update destroy]
    def index
      users = User.order(created_at: :desc).limit(20)
      authorize users
      render json: users
    end
    def show
      authorize @user
      render json: @user
    end
    def create
      user = User.new(user_create_params)
      user[:provider] = :email
      authorize user
      if user.save
        render json: user
      else
        render json: { errors: user.errors }
      end
    end
    def update
      authorize @user
      if @user.update(user_params)
        render json: @user
      else
        render json: { errors: @user.errors }
      end
    end
    def destroy
      authorize @user
      @user.destroy
      render json: @user
    end
    private
    def set_user
      @user = User.find(params[:id])
    end
    def user_create_params
      # createの時のみadmin権限を設定できるようにする
      params.permit(:name, :email, :admin, :password)
    end
    def user_params
      params.permit(:name, :email, :password)
    end
  end
end
テストのセクションで書いたように、adminはcreateの時のみ付与できるようにしています。
以上です。
全18回に渡りご覧いただきありがとうございました。
ぜひこれを元に、コメント機能やソーシャルログイン等の機能拡張していってください。
目次
【連載目次へ】