🐟

[Rails]gem Punditによる権限管理 (認可)

2021/10/23に公開

Punditとは

PunditとはRubyのgemであり、「認可」の仕組みを提供するものです。
「ユーザーによってページの表示の許可・拒否をしたり、表示情報の範囲を変えられるgem」です。
Punditはcurrent_user(ログイン中のユーザー)メソッドを扱うので、sorcery gem などが「認証」の仕組みありきです。認可は認証に依存しています。
似ているgemでcancancanがありますが、ちょっと違います。

認証と認可とは

認証と認可は似ているようで全く別の概念です。

認証 Authentication

通信の相手が誰(何)であるかを確認することが「認証」
純粋な認証には「リソース」やそれに対する「権限」という概念はありません。
いわゆる「証明書の確認」のようなものです。
マイナンバーカードで身元を確認するようなことです。
ただ、〇〇さんというのは確認できても、何かが許されるといった話は関係ないです。
つまり、相手が誰なのか確認する。それだけです。

認可 Authorization

とある特定の条件に対して、リソースアクセスの権限をあたえることが「認可」
純粋な「認可」には、「誰」という考え方はありません。
いわゆる「鍵の発行」や「チケットの発行」のようなものです。
チケットがあるからといって誰かの身元が明らかになる話とは関係ないです。
電車のチケットなら、乗車の許可があるだけで、誰かというのは関係ありません。
認可はそれを持っているだけで何か(リソースへのアクセス)が許可されます。
鍵があればドアが開くし、持っていなければ開きません。
つまり、ただの許可章の発行だけです。
ただ多くの場合、認証できないと認可できないので、認可は認証に依存していると言えます。

認可の仕組み

Punditはコントローラの各アクションでauthorize リソースオブジェクトを呼ぶと、**対象のリソースに対して権限があるかどうかを確認してくれます。**その設定をapp/policiesにあるポリシーファイルで細かく定義できます。

導入方法&使用例

インストール

gem "pundit"
bundle install

Punditをinclude

使いたいコントローラでPunditをincludeします。

class ApplicationController < ActionController::Base
  include Pundit
end

認可のルールを記述するファイルを作成

generatorで作成します。

rails g pudit:install

上記のコマンドを実行すると、app/policies/application_policy.rbというファイルが生成されます。

app/policies/applicaiton_policy.rb

class ApplicationPolicy
	# 読み取りの属性を定義している
  attr_reader :user, :record

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

  def index?
    false
  end
end

このファイルで定義しているApplicationPolicyクラスを継承して、他のコントローラごとの認可ルールを記述していきます。

initializeで定義される最初の引数は、userです。Punditはcurrent_userメソッドを呼び出して、この引数に送ります。

第二引数のrecordは認可をチェックしたいモデルオブジェクトです。対応するモデルのインスタンスを手動で割り当てます。

これを利用することで、アクセスしているユーザーオブジェクトと、対象のリソースオブジェクトを知ることができます。

認可ファイルを作成してみる

例としてPostという名前のモデルに対してpolicyを作成してみます。

app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy

  def create?
    # アクセスユーザー権限がadminまたはeditorのときのみ認可
    user.admin? || user.editor?
  end

end
  • モデル名_policy.rbでファイル作成
  • モデル名Policyでクラス名定義
  • def アクション名?で認可ルール(policy)を記述

**def アクション名?の返り値によって、認可するか否かを判断しています。**

例えば、上記のdef create?falseが返ってくれば、

PostControllerのcreateアクションは拒否されて、Pundit::NotAuthorizedErrorが発生します。

コントローラ側からPundit呼び出し

先程のpolicyファイルを適用するために、コントローラからPunditを呼び出します。

app/controller/posts_controller.rb

def create

  @tag = Tag.find(params[:id])

  authorize(Tag)

- 省略 -

end
  • authorizeメソッドで、先程のpolicyファイルに記述されたdef create?が処理されます。
  • 引数にはモデル(リソース)オブジェクトを入れます。
  • インスタンスかモデルか認可状況を確認します。

Pundit用静的403エラー画面の作成

Pundit::NotAuthorizedErrorを捕捉してエラーページを表示させる

各認可がfalseだった場合、authorizePundit::NotAuthorizedErrorraiseするので、エラーを拾って403を返す仕組みを作っておく必要があります。

実装の観点として下記2つを意識します。

  • ヘッダーやフッターなど共通のページレイアウトをエラーページに表示させるかどうか
  • 開発環境で403エラーページの表示させるかどうか

今回は、共通レイアウトは表示させずに、自作のエラー画面public/403.htmlを表示させます。

また、上記のエラー画面は、本番環境では表示されますが、開発環境ではデフォルトで非表示となっています。正しく表示されるか開発環境で確認する方法も後で記述します。

エラー画面のテンプレートを作成

まずはエラー画面のpublic/403.htmlを作成しておきます。

<!DOCTYPE html>
<html>
<head>
  <title>権限がありません(401)</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>

<body>
  <p>権限がありません。</p>
</body>
</html>

本番環境でユーザー向けの403エラー画面を表示させる設定

Production環境でユーザー向けの403エラー画面を表示させるには下記の通り、config/application.rbに設定を記述して、サーバを再起動させます。

config/application.rb

#例外を403HTTPステータスにします。これを付けないと500になる。
# :forbiddenというシンボルはステータスコード403と定義されている。
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden

Pundit::NotAuthorizedErrorを補足させ、:forbiddenを指定することで、HTTPステータスコードが403になります。このシンボルは
https://github.com/rack/rack/blob/1-6-stable/lib/rack/utils.rb#L661
で定義されています。

開発環境で403エラー画面を確認する

上記の設定でPundit::NotAuthorizedErrorが発生した場合、本番環境では、public/403.htmlが表示されるようになりました。これを開発環境で確認するには、config/environments/development.rbconfig.consider_all_requests_localfalseにして、サーバを再起動することで可能となります。

config/environments/development.rb

# エラー画面404と403をデバッグ用か本番用か切り替えられる
  # config.consider_all_requests_local = true
  config.consider_all_requests_local = false

確認ができたらtrueに戻しておきましょう。

上記の設定で確認したのが下図になります。

Pundit::NotAuthorizedErrorが発生するパスにアクセスするとpublic/403.htmlが表示されていることが確認できました。また、ステータスコードも403になっております。

以上。

※ 共通レイアウトも表示させる場合

共通レイアウトも表示させる場合は、ApplicationControllerでrescue処理を記載して、app/view以下のテンプレートファイルを用意してrenderさせます。

class ApplicationController < ActionController::Base
  include Pundit

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    render 'error/403', status: :forbidden
  end
end

参考

https://github.com/varvet/pundit)

https://dev.classmethod.jp/articles/authentication-and-authorization/

https://github.com/varvet/pundit#rescuing-a-denied-authorization-in-rails

Discussion