Punditとは
Pundit は認可を実装をする時に使用されたりするgem。基本的な使用方法は今回省略。
まずが全体構成
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── lib
│ ├── generators
│ │ ├── pundit
│ │ │ ├── install
│ │ │ │ ├── USAGE
│ │ │ │ ├── install_generator.rb
│ │ │ │ └── templates
│ │ │ │ └── application_policy.rb
│ │ │ └── policy
│ │ │ ├── USAGE
│ │ │ ├── policy_generator.rb
│ │ │ └── templates
│ │ │ └── policy.rb
│ │ ├── rspec
│ │ │ ├── policy_generator.rb
│ │ │ └── templates
│ │ │ └── policy_spec.rb
│ │ └── test_unit
│ │ ├── policy_generator.rb
│ │ └── templates
│ │ └── policy_test.rb
│ ├── pundit
│ │ ├── policy_finder.rb # このファイル
│ │ ├── rspec.rb
│ │ └── version.rb
│ └── pundit.rb # このファイル
├── pundit.gemspec
└── spec
├── policies
│ └── post_policy_spec.rb
├── policy_finder_spec.rb
├── pundit_spec.rb
└── spec_helper.rb
今回主に見るのはpundit.rbとpolicy_finder.rbです。
authorizeメソッド
使うときにはコントローラー内でinclude Punditしてauthorizeを呼ぶと思います。
なので
def authorizeでgrepしてみると2箇所ヒット。
# lib/pundit.rb 70行目あたり
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class << self # Retrieves the policy for the given record, initializing it with the # record and user and finally throwing an error if the user is not # authorized to perform the given action. # # @param user [Object] the user that initiated the action # @param record [Object] the object we're checking permissions of # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`) # @param policy_class [Class] the policy class we want to force use of # @raise [NotAuthorizedError] if the given query method returned false # @return [Object] Always returns the passed object record def authorize(user, record, query, policy_class: nil) policy = policy_class ? policy_class.new(user, record) : policy!(user, record) raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query) record.is_a?(Array) ? record.last : record end |
これはReadmeでいうとこのあたりの話で、今回見たいケース(コントローラーにincludesして使う)ではないので飛ばします。
# lib/pundit.rb 207行目あたり
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# Retrieves the policy for the given record, initializing it with the record # and current user and finally throwing an error if the user is not # authorized to perform the given action. # # @param record [Object] the object we're checking permissions of # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`). # If omitted then this defaults to the Rails controller action name. # @param policy_class [Class] the policy class we want to force use of # @raise [NotAuthorizedError] if the given query method returned false # @return [Object] Always returns the passed object record 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.is_a?(Array) ? record.last : record end |
こちらがコントローラー内でinclude Punditして使うときのメソッドと思われます。
まず気づくのが引数にuserっぽいものがないこと。
代わりにpundit_userを呼んでおりpundit_userではcurrent_userを呼んでいます。
# lib/pundit.rb 312行目あたり
1 2 3 4 5 6 7 8 |
# Hook method which allows customizing which user is passed to policies and # scopes initialized by {#authorize}, {#policy} and {#policy_scope}. # # @see https://github.com/varvet/pundit#customize-pundit-user # @return [Object] the user object to be used with pundit def pundit_user current_user end |
つまりcurrent_userというメソッドを予め定義しておくだけで良いことになります。deviseなどを使っていればたいていログイン中ユーザはcurrent_userというメソッドで取得できると思います。もちろんここをオーバーライドすれば任意のメソッドとしてユーザを取得することができます。
Readmeのこのあたりの内容と思われます。
話を戻して、次に第二引数のquery = nil の部分ですが、指定しなければ”#{action_name}?”となっているので、
indexアクション内でauthorize(record)とすると、対象のポリシークラスのindex?メソッドを実行することになります。
index?の結果がfalseかの場合NotAuthorizedErrorをrasieしているので、エラーハンドリングを行いたいときはこのエラーをcatchすれば良いこともわかります。
第三引数のpolicy_class: nilですが、
policy_class ? policy_class.new(pundit_user, record) : policy(record)
なので、指定した場合のみそのクラスをnewする仕組みのようです。指定がない場合は、policy(record)が実行されて、
# lib/pundit.rb 261行目あたり
1 2 3 4 5 6 7 8 |
# Retrieves the policy for the given record. # # @see https://github.com/varvet/pundit#policies # @param record [Object] the object we're retrieving the policy for # @return [Object, nil] instance of policy class with query methods def policy(record) policies[record] ||= Pundit.policy!(pundit_user, record) end |
さらにここを追っていくと最終的には
# lib/pundit/policy_finder.rb 73行目あたり
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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 def find_class_name(subject) if subject.respond_to?(:model_name) subject.model_name elsif subject.class.respond_to?(:model_name) subject.class.model_name elsif subject.is_a?(Class) subject elsif subject.is_a?(Symbol) subject.to_s.camelize else subject.class end end |
ポリシークラスを決定するロジックが書かれています。
if subject.is_a?(Array) ではjoin(“::”)は
ポリシークラスを階層化する機能を実現していると思われます。
あとはそれぞれ特定メソッドが定義されているかを優先順位をもとに確認していってポリシークラスを決定しています。
subject.respond_to?(:model_name)やsubject.class.respond_to?(:model_name)があるあたり、インスタンスを渡してもクラスを渡しても探してくれる感じになっています。
post = Post.find(1)
authorize(record)
とかすると、Post + SUFFIX でPostPolicyクラスを探してくれそうです。
(SUFFIX = “Policy”と定義されています。)