有名なGemのソースコードを読んで仕組みを理解する Pundit編
rails向けの権限管理GemにPunditっていうものがあるんですが、 いい感じに権限周りの記述を1ファイルにまとめられて見通しの良いGemだなと感じたので、 ソースコードを読んでみました。
pundit (2.0.0)
github.com
簡単な説明
使い方の説明記事ではないのでざっと書くと、 各リソースに対してPolicyというテンプレートファイルを内で権限をメソッド単位で定義できるというもの。
たとえばHoge.rbの権限を設定したかったらApplicationControllerでPunditをincludeしたあとに、
class ApplicationController < ActionController::Base include Pundit end
Policyファイルを作って
class HogePolicy def show? #ここにHogeリソースの閲覧権限を書く。 end . . . end
使うときは
class HogeController def show authorize @hoge end end
みたいな感じに、
if current_user hoge
みたいな条件分岐記述が分散せず1箇所にまとめられて見通しがよくなる。
Gemの中身の概要
中身は結構シンプルでコアは以下の2つのファイル
pundit/pundit.rb at master · varvet/pundit · GitHub
pundit/policy_finder.rb at master · varvet/pundit · GitHub
アプリケーションとGemのインターフェースは先述の通り authorize
メソッドで、
これはPunditモジュールに定義されているのでアプリケーション側でincludeすると利用できるようになる。
リソースとポリシーの紐づけはauthorizeメソッドとPolicyFinderクラス内のfindメソッドで行っている。シンプルなアプリケーションであれば権限管理したいリソースのオブジェクトのみをauthorizeの引数として渡すことが多いと思うが、query
つまり権限設定に利用するメソッドと policy_class
権限設定が記述されているクラスをカスタマイズできる。デフォルトでは query
は "#{action_name}?"
であり、 policy
は "#{klass}#{SUFFIX}"
となる。
https://github.com/varvet/pundit/blob/master/lib/pundit.rb#L202
def authorize(record, query = nil, policy_class: nil) query ||= "#{action_name}?" @_pundit_policy_authorized = true policy = policy_class ? policy_class.new(pundit_user, record) : policy(record) raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query) record end
pundit/policy_finder.rb at master · varvet/pundit · GitHub
def find(subject) if subject.is_a?(Array) modules = subject.dup last = modules.pop context = modules.map { |x| find_class_name(x) }.join("::") [context, find(last)].join("::") elsif subject.respond_to?(:policy_class) subject.policy_class elsif subject.class.respond_to?(:policy_class) subject.class.policy_class else klass = find_class_name(subject) "#{klass}#{SUFFIX}" end end
以上でリソースとポリシーの紐づけが済むので、
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
で、HogePolicyの該当アクションを実行することで権限がfalseであれば NotAuthorizedErrorを流せるというわけだ。
非常にシンプルである。
ちなみに
本題とは若干それるが、スコープごとに権限管理を行う対象(user)やポリシーファイル自体もカスタマイズできるようで policy
, pundit_policy_scope
, pundit_user
のオーバーライドが可能なようだ。このあたりActiveSupport::Concern
とかhelper_method
で動的に実現しているのもrubyらしいのかなと思いなかなか興味深かった(普段java/kotlinみたいな静的な言語を書くことが多いので尚更)
included do helper Helper if respond_to?(:helper) if respond_to?(:helper_method) helper_method :policy helper_method :pundit_policy_scope helper_method :pundit_user end end