sky’s 雑記

露頭に迷うエンジニア雑記

有名な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