81
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

Punditをなるべくやさしく解説する

Punditについて、なるべくやさしく解説します。
ドキュメントを翻訳したものに近い内容となっております。

  • ドキュメントを読めるほど実力に自身がない人
  • ライブラリを調べる際にいろんな記事を見て時間を消耗する人

上記の方々の助けとなれば幸いです。

※Railsで利用することを前提に話します。

Punditとは

  • Rubyのgem(ライブラリ)
  • 認可の仕組みを提供してくれる

「認可(Authorization)」と「認証(Authentication)」は意味が異なります。
詳しくは「よくわかる認証と認可」をご参照ください。

簡単に言うと「ユーザーによってページ表示の許可・拒否をしたり、表示情報の範囲を変えたりすることができるgem」です。

似たようなgemとしてcancancanがよく比較されますが、その違いはこちらの記事から引用させていただきます。

cancancanはユーザに対して、どんなアクションが許可するかを定義するのに対して、Punditではリソースに対して誰が許可されるのかを定義します。反対からの目線ですね。cancancanがコントローラ寄りならば、Punditはモデル寄りの責務です。また、cancancanがDSLなのに対し、PunditはピュアRubyな書き方になっています。

それでは、ドキュメントに沿って、簡単なチュートリアルを行ってみます。

前提

  • Rails 5.2.2
  • Ruby 2.5.4
  • Pundit 2.0.1

簡単なチュートリアル

まずはPunditをインストール。Gemfileに以下を追記します。

gem "pundit"

利用したいコントローラーの継承元でPunditをincludeしましょう。

class ApplicationController < ActionController::Base
  include Pundit
end

利用した方が楽なのでgeneratorを使います。

$ rails g pundit:install

これで、app/policies/配下にapplication_policy.rbというファイルが作成されます。
※下記は省略して記載しております。

「policy」は「方針」という意味ですので、policies配下のファイルは「認可のルールを記述するファイル」と認識して差し支えないと思います。

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を継承して、コントローラーごとの認可ルールを記述していきます。
※継承せずともできますが、した方が楽です。

initializerで定義されるuserはデフォルトでcurrent_userが引数に割り当てられるようになっています。
recordの方には対応するモデルのインスタンスを手動で割り当てます。

一例として、postという名前のモデルに対してpolicyを作成してみましょう。

app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def update?
    user.admin? or not record.published?
  end
end

このように、
- モデル名_policy.rbでファイルを作成
- モデル名Policyでクラス名を定義
- def アクション名?で認可ルール(policy)を記述

という流れで利用していきます。

def アクション名?の返り値によって、認可するか否かを判断しています。
例えば上記のdef update?でfalseが返ってくれば、
PostControllerのupdateアクションは拒否されて、Pundit::NotAuthorizedErrorが投げられます。

※デフォルトではこのような流れとなりますが、Controller側の記述によって、他のモデル・アクションから特定のpolicyを参照させることも可能です。

先述のpolicyファイルを適用するために、Controller側からPunditを呼び出しましょう。

app/controller/posts_controller.rb
def update
  @post = Post.find(params[:id])
  authorize @post
  # インスタンスモデルを利用しない(policy側ではuserの情報のみ扱う)場合に限り、
  # authorize Post
  # という記述が可能。
  # ...
end

authorizeメソッドによって、先ほどのpolicyファイルに記述されたdef update?が処理されます。
引数には対応するモデルオブジェクトを入れます。

簡単チュートリアルは以上となります。

利用Tips

ここではドキュメントを参照し、「知ってたら得するかも」と思ったPunditの利用方法について記述していきます。

異なるアクション名から特定のpolicyを参照したい場合

例えば、独自に定義したpublishアクションでindexアクションのpolicyを参照させたい場合は、
下記のように第二引数にシンボルを渡すことで可能となります。

app/controller/posts_controller.rb
def publish
  @post = Post.find(params[:id])
  authorize @post, :index?
  # ...
end

異なるモデル名から特定のpolicyを参照したい場合

例えば、hogeモデルから生成したインスタンスオブジェクトで、Postモデル用のpolicyを参照したい場合、
下記のように第二引数にシンボルを渡すことで可能となります。

app/controller/hoges_controller.rb
def update
  @hoge = Hoge.find(params[:id])
  authorize @hoge, policy_class: PostPolicy
  # ...
end

authorizeメソッドの引数に割り当てるオブジェクトがない場合

例えば、下記のようなpolicyがあるとします。

app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def index?
    user.admin?
  end
end

userの情報しか扱っていないので、authorizeメソッドの引数にオブジェクトを割り当てる必要がないです。
そんな時はこのように記述しましょう。

app/controller/posts_controller.rb
def index
  @posts = Post.all
  authorize Post
  # ...
end

モデル名を引数にすることも可能です。

対応するモデルのないpolicyを作成したい場合

たとえば「ダッシュボード」ページ用のpolicyを作成したいとします。すると、このような流れで実装します。

app/policies/dashboard_policy.rb
class DashboardPolicy < Struct.new(:user, :dashboard)
  # ...
end

ControllerやViewからは、下記のように呼び出します。

authorize :dashboard, :show?
<% if policy(:dashboard).show? %>
  <%= link_to 'Dashboard', dashboard_path %>
<% end %>

Scope機能を利用する

ユーザー属性によって、表示情報の範囲を変えたい時があります。
例えば、管理ユーザーの場合、非公開の記事も表示するようにしたいですよね。
そんな時に便利なのが、PunditのScopeです。

app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  class Scope
    attr_reader :user, :scope

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

    def resolve
      if user.admin?
        scope.all
      else
        scope.where(published: true)
      end
    end
  end
end

下記のようにController側からpolicy_scope()を呼び出すと、対応するモデルのpolicyファイルに定義されたresolve()の返り値が返ってきます。
initializeでは先述と同様、userにはデフォルトでcurrent_userが、scopeにはpolicy_scopeの第一引数が割り当てられます。

def index
  @posts = policy_scope(Post)
end

def show
  @post = policy_scope(Post).find(params[:id])
end

これにより、ユーザー属性による表示リソースの制御が可能となります。

ユーザーによって取得するストロングパラメータを変える

ユーザーによって受け取るPOSTが異なる場合があります。Pundit側で受け取るパラメータを制御してみましょう。

policyファイル内にpermitted_attributesを定義し、Controller側でそれを呼び出すことで、簡単に制御することができます。

app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def permitted_attributes
    if user.admin? || user.owner_of?(post)
      [:title, :body, :tag_list]
    else
      [:tag_list]
    end
  end
end
app/controller/posts_controller.rb
def publish
  def update
    @post = Post.find(params[:id])
    if @post.update_attributes(post_params)
      redirect_to @post
    else
      render :edit
    end
  end

  private

  def post_params
    params.require(:post).permit(policy(@post).permitted_attributes)
  end
end

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
81
Help us understand the problem. What are the problem?