Punditを使用した認可機能の作成方法

前提

scaffoldで作成したようなスタンダードなRailsアプリを想定しており、UserとPostモデルがあり1対多の関係にあるものを想定しています。

class User < ApplicationRecord
  has_many :posts

  enum state: { writer: 0, editor: 1, admin: 2 }
end

class Post < ApplicationRecord
  belongs_to :user
end

基本的な使い方

1. punditをインストールした後、application_controller.rbにPunditを適用させる
# Gemfile

gem 'pundit'
=> bundle installコマンドを実行

application_controller.rbにPunditを適用させる

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Pundit
end
2. 以下のコマンドを実行後、生成されたファイルに設定を記述する.

以下のコマンドを実行することで、app/policies/配下にapplication_policy.rbというファイルが生成される。

% bin/rails g pundit:install

application_policy.rbに以下の設定を記述する

# app/policies/application_policy.rb

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end
end

initializeメソッドでインスタンス変数を定義している。
第一引数はuserを渡してある。Punditはデフォルトでコントローラーでcurrent_userメソッドを呼んで、userとして自動的に第一引数のuserに渡してある。
2番目の引数は、認証をチェックしたい何らかのモデルオブジェクトです。

3. コントローラーの各アクションの実行前にauthorizeを実行する.
# # app/controllers/articles_controller.rb

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  before_action :authorize_post, only: [:index, :new, :create]

  def index; end

  def show; end

  def new; end

  def create; end

  def edit; end

  def update; end

  def destroy; end

  private

  def set_post
    @post = Post.find(params[:id])
    authorize @post
  end

  def authorize_post
    authorize Post
  end
end
4. 個別のPolicyファイルを作成し、設定を記述する.

Punditは、リソースに対して、どのユーザーであれば処理が許可されるのかを定義するものです。
「コントローラのアクション名 + ?」のメソッドを定義し、返り値が true なら許可、 false なら拒否になる。例えばPostモデルのオブジェクトに対して以下の設定を行う場合、

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def index?
    true
  end

  def show?
    true
  end

  def new?
    true
  end

  def create?
    user.admin? || user.editor?
  end

  def edit?
    true
  end

  def update?
    user.admin? || user.editor?
  end

  def destroy?
    user.admin? || user.editor?
  end
end

上記のようにポリシーファイルを設定すると、Postモデルのオブジェクトに対して、create、update、destoryメソッドは管理者、編集者のみが実行可能、他のアクションは全てのユーザーが実行可能、といった定義を行うことが出来る。

Scope

policy_scopeメソッドを呼ぶと、対象ポリシーファイル内で定義されたスコープを実行してくれる。
例えば、管理者はすべてのリソースにアクセスできるが、一般ユーザは自身が保持しているリソースのみといった認可を設定したい場合、以下のように実装すれば良い。

class PostPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if user.admin?
        scope.all
      else
        scope.where(user_id: user.id)
      end
    end
  end
end
class PostsController < ApplicationController
  before_action :authorize_post, only: [:index]

  def index
    @posts = policy_scope(Post)
  end

  private

  def authorize_post
    authorize policy_scope(Post)
  end
end

例外を403ステータスにするする方法

# config/application.rb

module Post
  config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
end

補足:Punditが用意してあるPolicySpecでテストコードを書く

spec_helper.rbに以下のコードを記述

# spec/spec_helper.rb

require "pundit/rspec"

上記の記述によりpermitというヘルパーが使用できるようになるので、これにユーザとリソースオブジェクトを渡すだけで書ける。

require 'rails_helper'

RSpec.describe TaxonomyPolicy do
  subject { described_class }

  permissions :index?, :create?, :update?, :destroy? do
    let(:admin) { create(:user, :admin) }
    let(:editor) { create(:user, :editor) }
    let(:writer) { create(:user, :writer) }
    let(:tag) { create(:tag) }
    let(:author) { create(:author) }
    let(:category) { create(:category) }

    it "ライターはタグ・著者・カテゴリーの一覧表示・編集・削除機能にアクセスできない" do
      expect(subject).not_to permit(writer, tag, author, category)
    end

    it "管理者はタグ・著者・カテゴリーの一覧表示・編集・削除機能にアクセスできない" do
      expect(subject).to permit(admin, tag, author, category)
    end

    it "編集者はタグ・著者・カテゴリーの一覧表示・編集・削除機能にアクセスできない" do
      expect(subject).to permit(editor, tag, author, category)
    end
  end
end

公式リファレンス・参考記事

https://github.com/varvet/pundit

https://qiita.com/zaru/items/8bf7b41b33f3f55bd27d#%E3%83%86%E3%82%B9%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E6%9B%B8%E3%81%8F