Railsアプリケーションで利用可能な認可gemであるaction_policyの機能を紹介します。
ここでは類似したパラダイムで構成されるpunditと比較しながら説明していきます。
他にはcancancan gemも有名ではありますが、こちらとの比較は既存記事の「Authorization in Rails controllers: Pundit versus CanCan」などに譲ります。
punditとaction_policy
punditとaction_policyの機能はかなり近しいです。action_policyがpunditを参考にしているため当然の話ではあります。
action_policy自身は「punditで必要とされていたHACKがaction_policyになった」と主張しており、これが両者の差異になります。
以下、細かい話ですが例を二つご紹介します。
namespaceの推論
action_policyではPolicyのnamespaceを推論することができます。
Namespaces -- Action Policy: authorization framework for Ruby/Rails applications
module Admin
class UsersController < ApplicationController
def index
# uses Admin::UserPolicy if any, otherwise fallbacks to UserPolicy
authorize!
end
end
end
一方でpundtiでは呼び出し時に明示的な指定が必要です。
Policy Namespacing -- varvet/pundit: Minimal authorization through OO design and pure Ruby classes
authorize(post) # => will look for a PostPolicy
authorize([:admin, post]) # => will look for an Admin::PostPolicy
authorize([:foo, :bar, post]) # => will look for a Foo::Bar::PostPolicy
policy_scope(Post) # => will look for a PostPolicy::Scope
policy_scope([:admin, Post]) # => will look for an Admin::PostPolicy::Scope
policy_scope([:foo, :bar, Post]) # => will look for a Foo::Bar::PostPolicy::Scope
認可対象名とPolicyクラス名の不一致の処理
例として、以下のように注文(Order
)をキャンセルするためのコントローラーを作成し、このアクションをリクエストするボタンをViewで実装することにします。
class Orders::CancelsController < ApplicationController
def create
# キャンセル可とする条件
# 1. ログインユーザーが申込の作成者である
# 2. かつ、申込が未承認である
end
end
punditで実装する場合、まず以下のようなPolicyクラスを作成します。
class Orders::CancelPolicy < ApplicationPolicy
attr_reader :user, :order
def initialize(user, order)
@user = user
@order = order
end
def create?
order.user == user && !order.approved?
end
end
punditでは、このように認可対象とクラス名が一致していないPolicyについて、Viewから呼び出すためのインターフェースが若干弱いです。
通常であれば #policy
というヘルパーメソッドを利用できます。
<#% PostPolicy を参照したい場合 %>
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
しかし、このメソッドを通じて Orders::CancelPolicy
のロジックを呼び出すことができません。
<#% Orders::CancelPolicy を参照したい場合 %>
<#% @order をPolicyに渡せない %>
<% if policy([:orders, :cancel]).create? %>
<%= button_to "Cancel order", orders_cancels_path(@order) %>
<% end %>
<#% シンタックスエラー(引数を2つ以上取れない) %>
<% if policy([:orders, :cancel], @order).create? %>
<%= button_to "Cancel order", orders_cancels_path(@order) %>
<% end %>
<#% OrderPolicy を参照してしまう %>
<% if policy(@order).create? %>
<%= button_to "Cancel order", orders_cancels_path(@order) %>
<% end %>
単にPolicyを呼び出すだけであれば、 Orders::CancelPolicy.new
とすれば可能です。
<% if Orders::CancelPolicy.new(current_user, @order).create? %>
<%= link_to "Cancel order", orders_cancels_path(@order) %>
<% end %>
ただし、punditにはPolicyインスタンスのキャッシュという機能があり、 #policy
メソッドを経由せずに作成されたインスタンスにはこのキャッシュが効きません。
キャッシュが効かないから致命的ということはないのですが、用意されたインターフェースを利用しないためにパフォーマンス上の恩恵を受けられず、HACKのような実装になってしまいます。
一方、action_policyにはAuthorization Contextという、その名の通り認可コンテキストを指定する機能が存在します。
Policyクラスを作成するときにこのコンテキストを指定します。
class Orders::CancelPolicy < ApplicationPolicy
authorize :order # これを足す
def create?
order.user == user && !order.approved?
end
end
Viewからは allow_to?
ヘルパーメソッドの with:
オプションを指定することで、Policyクラスのロジックを呼び出すことができます。
<% if allowed_to?(:create, @order, with: Orders::CancelPolicy) %>
<%= button_to "Cancel order", orders_cancels_path(@order) %>
<% end %>
これは素直な呼び出し方に見えますし、punditと違ってPolicyインスタンスのキャッシュも効きます。
punditで微妙に手が届かない箇所をカバーする一例として紹介しました。
その他、punditとの差分内容についてはaction_policyの公式ドキュメントをご参照ください。
個人的には、action_policyで不満足なことがないため、認可gemにはこちらを採用するようになりました。