Ruby
Rails
pundit

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