Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: action_policy gemの紹介

Railsアプリケーションで利用可能な認可gemであるaction_policyの機能を紹介します。
ここでは類似したパラダイムで構成されるpunditと比較しながら説明していきます。

palkan/action_policy - GitHub
varvet/pundit - GitHub

他にはcancancan gemも有名ではありますが、こちらとの比較は既存記事の「Authorization in Rails controllers: Pundit versus CanCan」などに譲ります。

CanCanCommunity/cancancan - GitHub

punditとaction_policy

punditとaction_policyの機能はかなり近しいです。action_policyがpunditを参考にしているため当然の話ではあります。
action_policy自身は「punditで必要とされていたHACKがaction_policyになった」と主張しており、これが両者の差異になります。

What about the existing solutions? -- Action Policy: authorization framework for Ruby/Rails applications

以下、細かい話ですが例を二つご紹介します。

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の公式ドキュメントをご参照ください。

What about the existing solutions? -- Action Policy: authorization framework for Ruby/Rails applications

個人的には、action_policyで不満足なことがないため、認可gemにはこちらを採用するようになりました。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。