rails向けの権限管理GemにPunditっていうものがあるんですが、
いい感じに権限周りの記述を1ファイルにまとめられて見通しの良いGemだなと感じたので、
ソースコードを読んでみました。
pundit (2.0.0)
https://github.com/varvet/pundit
簡単な説明
使い方の説明記事ではないのでざっと書くと、
各リソースに対して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つのファイル
https://github.com/varvet/pundit/blob/master/lib/pundit.rb
https://github.com/varvet/pundit/blob/master/lib/pundit/policy_finder.rb
アプリケーションとGemのインターフェースは先述の通り authorize
メソッドで、
これはPunditモジュールに定義されているのでアプリケーション側でincludeすると利用できるようになる。
リソースとポリシーの紐づけはauthorizeメソッドとPolicyFinderクラス内のfindメソッドで行っている。シンプルなアプリケーションであれば権限管理したいリソースのオブジェクトのみをauthorizeの引数として渡すことが多いと思うが、query
つまり権限設定に利用するメソッドと policy_class
権限設定が記述されているクラスをカスタマイズできる。デフォルトでは query
は "#{action_name}?"
であり、 policy
は "#{klass}#{SUFFIX}"
となる。
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
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