こんにちは!kossyです!
policy_classの使い方
例えば、CommentクラスのインスタンスにPostモデルのPolicyを実行したいケースです。
def show @comment = Comment.find(params[:id]) authorize(@comment, policy_class: PostPolicy) end
このように記述すると、PostPolicyが実行されるようになります。
class PostPolicy < ApplicationPolicy def show? user.admin? end end
authorizeメソッドの引数にはsymbolで実行したいPolicyのメソッド名も指定することができます。
# PostPolicyクラスのindex?メソッドを実行する authorize(@comment, :index?, policy_class: PostPolicy)
authorizeメソッドのコードを追ってみる
ではauthorizeメソッドのコードを追ってみて、どのように上記の挙動を実現しているのか確認してみましょう。
# 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
コメントアウト部分を日本語に訳してみます。
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] アクションを開始したユーザー
@param record [Object]パーミッションをチェックしているオブジェクト
@param query [Symbol、String]ポリシーをチェックするためのメソッド(例: show?)
@param policy_class [Class]強制的に使用するポリシークラス指定されたクエリメソッドがfalseを返した場合は@raise [NotAuthorizedError]
@return [Object]常に渡されたオブジェクトレコードを返します
引数と返り値についての記載がありました。
メソッドの中身を見ます。
policy = policy_class ? policy_class.new(user, record) : policy!(user, record)
この部分は、引数のpolicy_classが存在する場合、そのpolicy_classをuserとrecordを引数にしてイニシャライズしています。
policy_classがない場合はpolicy!メソッドを実行していますので、処理を見に行ってみます。
policy!
# Retrieves the policy for the given record. # # @see https://github.com/varvet/pundit#policies # @param user [Object] the user that initiated the action # @param record [Object] the object we're retrieving the policy for # @raise [NotDefinedError] if the policy cannot be found # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Object] instance of policy class with query methods def policy!(user, record) policy = PolicyFinder.new(record).policy! policy.new(user, pundit_model(record)) rescue ArgumentError raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called" end
コメントアウト部分を日本語に訳してみます。
Retrieves the policy for the given record.
指定されたレコードのポリシーを取得します。
@param user [Object] アクションを開始したユーザー
@param record [Object]ポリシーを取得するオブジェクト
ポリシーが見つからない場合は@raise [NotDefinedError]
ポリシーコンストラクターが誤って呼び出された場合は@raise [InvalidConstructorError]
@return [Object]クエリメソッドを使用したポリシークラスのインスタンス
引数と返り値、例外が発生する条件について記載がありました。
処理を見てみます。
policy = PolicyFinder.new(record).policy!
policy.new(user, pundit_model(record))
PolicyFinderインスタンスのpolicy!メソッドを見る必要がありそう。
def initialize(object) @object = object end # @return [Class] policy class with query methods # @raise [NotDefinedError] if policy could not be determined # def policy! policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`" end def policy klass = find(object) klass.is_a?(String) ? klass.safe_constantize : klass 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 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
findメソッドかfind_class_nameメソッドでPolicyを実行するクラスを返して、返されたモデル名がString型ならsafe_constantizeメソッドでクラスとして扱えるようにし、
String型でなければPolicyを実行するクラスをそのまま返却しています。
つまり、PolicyFinderクラスのインスタンスメソッドであるpolicy!メソッドの返り値はPolicyを実行するクラスが返ることになります。
だいぶコードを追ったので改めてPunditモジュールのpolicy!メソッドの処理を見ます。
def policy!(user, record) policy = PolicyFinder.new(record).policy! policy.new(user, pundit_model(record)) rescue ArgumentError raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called" end
引数に誤りがあれば例外を投げて、とくに誤りがなければPolicyFinderインスタンスのpolicy!メソッドで見つけたクラスをnewして返却しています。
ようやくauthorizeメソッドに返ってくることができます、、、
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
引数のpolicy_classが存在する場合、そのpolicy_classをuserとrecordを引数にしてイニシャライズしています。
policy_classが渡されていない場合は、recordのクラスのPolicyインスタンスを返却しています。
public_sendの返り値がnilまたはfalseの場合はNotAuthorizedErrorをthrowしています。
recordが配列ならrecordの最後のインスタンスを、配列でなければrecordをそのまま返します。
感想
policy_classを用いればかなり柔軟にPolicyを実行することができるので、エッジケースに対応できそうです。