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
配下のファイルは「認可のルールを記述するファイル」と認識して差し支えないと思います。
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を作成してみましょう。
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を呼び出しましょう。
def update
@post = Post.find(params[:id])
authorize @post
# インスタンスモデルを利用しない(policy側ではuserの情報のみ扱う)場合に限り、
# authorize Post
# という記述が可能。
# ...
end
authorize
メソッドによって、先ほどのpolicyファイルに記述されたdef update?
が処理されます。
引数には対応するモデルオブジェクトを入れます。
簡単チュートリアルは以上となります。
利用Tips
ここではドキュメントを参照し、「知ってたら得するかも」と思ったPunditの利用方法について記述していきます。
異なるアクション名から特定のpolicyを参照したい場合
例えば、独自に定義したpublish
アクションでindex
アクションのpolicyを参照させたい場合は、
下記のように第二引数にシンボルを渡すことで可能となります。
def publish
@post = Post.find(params[:id])
authorize @post, :index?
# ...
end
異なるモデル名から特定のpolicyを参照したい場合
例えば、hoge
モデルから生成したインスタンスオブジェクトで、Post
モデル用のpolicyを参照したい場合、
下記のように第二引数にシンボルを渡すことで可能となります。
def update
@hoge = Hoge.find(params[:id])
authorize @hoge, policy_class: PostPolicy
# ...
end
authorize
メソッドの引数に割り当てるオブジェクトがない場合
例えば、下記のようなpolicyがあるとします。
class PostPolicy < ApplicationPolicy
def index?
user.admin?
end
end
user
の情報しか扱っていないので、authorize
メソッドの引数にオブジェクトを割り当てる必要がないです。
そんな時はこのように記述しましょう。
def index
@posts = Post.all
authorize Post
# ...
end
モデル名を引数にすることも可能です。
対応するモデルのないpolicyを作成したい場合
たとえば「ダッシュボード」ページ用のpolicyを作成したいとします。すると、このような流れで実装します。
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です。
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側でそれを呼び出すことで、簡単に制御することができます。
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user.admin? || user.owner_of?(post)
[:title, :body, :tag_list]
else
[:tag_list]
end
end
end
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