Punditを使ってユーザーの権限を管理する

はじめに

PunditというGemを使ってユーザーの権限を管理する方法について。

前提

ユーザーの種類は、管理者(admin)、編集者(editor)、ライター(writer)の3種類があります。

そして、記事(article)を作成、更新、削除する際には、管理者か編集者でないとできないようにします。

設定方法

まずはGemをインストールします。

gem 'pundit'

続いてapplicationコントローラにインクルードします。

class ApplicationController < ActionController::Base
  include Pundit
end

punditをインストールします。

$ rails g pundit:install

するとapp/policies/配下にapplication_policy.rbが生成されます。

application_policy.rbに下記設定を記述します。

class ApplicationPolicy
  attr_reader :user, :record

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

  def scope
    Pundit.policy_scope!(user, record.class)
  end

  class Scope
    attr_reader :user, :scope

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

    def resolve
      scope
    end
  end
end

第一引数のuserには、current_userが入ります。

第二引数のscopeには、権限をチェックしたいオブジェクトが入ります。

続いて、上記のクラスを継承したarticle_policy.rbの設定をします。

class ArticlePolicy < ApplicationPolicy
  def index?
    true
  end

  def show?
    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

  class Scope < Scope
    def resolve
      scope
    end
  end
end

上記のように権限をチェックしたいメソッドに「?」をつけることで定義できます。

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

# createアクションで、userが管理者か編集者の場合は処理を実行できる。

コントローラ側では、第一引数にArticle@articleのように対象のオブジェクトを指定してauthorizeメソッドを呼び出します。

class Admin::ArticlesController < ApplicationController
  layout 'admin'

  before_action :set_article, only: %i[edit update destroy]

  def index
    authorize(Article)

    @search_articles_form = SearchArticlesForm.new(search_params)
    @articles = @search_articles_form.search.order(id: :desc).page(params[:page]).per(25)
  end

  def new
    @article = Article.new
  end

  def create
    authorize(Article)

    @article = Article.new(article_params)
    @article.state = :draft

    if @article.save
      redirect_to edit_admin_article_path(@article.uuid)
    else
      render :new
    end
  end

  def edit
    authorize(@article)
  end

  def update
    authorize(@article)

    @article.assign_attributes(article_params)
    @article.adjust_state
    if @article.save
      flash[:notice] = '更新しました'
      redirect_to edit_admin_article_path(@article.uuid)
    else
      render :edit
    end
  end

  def destroy
    authorize(@article)

    @article.destroy

    redirect_to admin_articles_path
  end

  private

  def article_params
    params.require(:article).permit(
      :title, :description, :slug, :state, :published_at, :eye_catch, :category_id, :author_id, :eyecatch_width, :eyecatch_location, tag_ids: []
    )
  end

  def search_params
    params[:q]&.permit(:title, :category_id, :author_id, :body, :tag_id)
  end

  def set_article
    @article = Article.find_by!(uuid: params[:uuid])
  end
end

最後に、権限がないユーザーが該当ページに訪れたときのテンプレートの作成と設定をします。

module Blog
  class Application < Rails::Application
    config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
  end
end
<!DOCTYPE html>
<html>
<head>
  <title>Access to this page is forbidden.</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <style>
  body {
    background-color: #EFEFEF;
    color: #2E2F30;
    text-align: center;
    font-family: arial, sans-serif;
    margin: 0;
  }

  div.dialog {
    width: 95%;
    max-width: 33em;
    margin: 4em auto 0;
  }

  div.dialog > div {
    border: 1px solid #CCC;
    border-right-color: #999;
    border-left-color: #999;
    border-bottom-color: #BBB;
    border-top: #B00100 solid 4px;
    border-top-left-radius: 9px;
    border-top-right-radius: 9px;
    background-color: white;
    padding: 7px 12% 0;
    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
  }

  h1 {
    font-size: 100%;
    color: #730E15;
    line-height: 1.5em;
  }

  div.dialog > p {
    margin: 0 0 1em;
    padding: 1em;
    background-color: #F7F7F7;
    border: 1px solid #CCC;
    border-right-color: #999;
    border-left-color: #999;
    border-bottom-color: #999;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
    border-top-color: #DADADA;
    color: #666;
    box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
  }
  </style>
</head>

<body>
  <!-- This file lives in public/404.html -->
  <div class="dialog">
    <div>
      <h1>Access to this page is forbidden.</h1>
      <p>You are not authorized to access this page.</p>
    </div>
    <p>If you are the application owner check the logs for more information.</p>
  </div>
</body>
</html>

開発環境でもこのエラーページが表示されるようにするには下記設定を変更します。

Rails.application.configure do
  config.consider_all_requests_local = false
  # falseにする。デフォルトではtrue
end

これで権限のないユーザーが該当ページに訪れると下記のようになります。

f:id:study-output:20210517190517p:plain