Railsで権限管理を実装できるPunditの公式リファレンスを翻訳します。
Railsでの権限管理はcancancan
が有名です。cancancan
はユーザーに対して権限を実装するのに対して、pundit
はmodelに対して権限を実装するという違いがあります。
Pundit
Punditは、通常のRubyクラスとオブジェクト指向のデザインパターンを活用して、シンプルで堅牢かつ拡張性のある認証システムを構築するためのヘルパーを提供します。
インストール
gem "pundit"
ApplicationControllerにPunditをincludeします。
class ApplicationController < ActionController::Base
include Pundit
protect_from_forgery
end
必要に応じてgeneratorを実行出来ます。これにより、便利なデフォルトを使用してapplication policyが生成されます。
rails g pundit:install
application policyを生成後、Railsサーバーを再起動して、Railsが新しい app/policies/
ディレクトリ内のクラスを取得できるようにします。
Policy
Punditは、ポリシークラスの概念に焦点を当てています。これらのクラスをapp/policies
に配置することを推奨します。以下は、ユーザーが管理者である場合、または投稿が非公開の場合に投稿を更新できる簡単な例です。
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.admin? or not post.published?
end
end
ご覧のとおり、これは単なるRubyクラスです。 Punditは、このクラスについて次のことを想定しています。
- このクラスには、モデルクラスと同じ名前が付けられますが、接尾辞は「ポリシー」のみです。
- 最初の引数はユーザーです。コントローラーで、Punditはcurrent_userメソッドを呼び出して、この引数に渡します。
- 2番目の引数は、モデルオブジェクトであり、権限確認をしたいものです。これはActiveRecordまたはActiveModelオブジェクトである必要はなく、実際には何でもかまいません。
- このクラスは、ある種のクエリメソッド、この場合はupdate?を実装します。通常、これは特定のコントローラーアクションの名前にマップされます。
通常は、generatorによって作成されたapplication policyから継承するか、継承するための独自の基本クラスを設定します。
class PostPolicy < ApplicationPolicy
def update?
user.admin? or not record.published?
end
end
生成されたApplicationPolicy
の中で、モデルオブジェクトはrecord
を呼び出します。
Post
クラスのインスタンスがあると仮定すると、Punditによってコントローラーで以下のように記述できるようになります。
def update
@post = Post.find(params[:id])
authorize @post
if @post.update(post_params)
redirect_to @post
else
render :edit
end
end
authorizeメソッドは、Post
に一致するPostPolicyクラスがあることを自動的に推測し、このクラスをインスタンス化して、current_userと指定されたレコードを渡します。次に、アクション名から、ポリシーインスタンスのupdate?
を呼び出す必要があると推測します。この場合、authorizeは次のようなことをすると想像できます。
unless PostPolicy.new(current_user, @post).update?
raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}"
end
チェックする権限の名前がアクション名と一致しない場合は、2番目の引数を渡して認証できます。例えば、
def publish
@post = Post.find(params[:id])
authorize @post, :update?
@post.publish!
redirect_to @post
end
必要に応じて、ポリシークラスをオーバーライドする引数を渡すことができます。例えば、
def create
@publication = find_publication # assume this method returns any model that behaves like a publication
# @publication.class => Post
authorize @publication, policy_class: PublicationPolicy
@publication.publish!
redirect_to @publication
end
許可する最初の引数のインスタンスがない場合は、クラスを渡すことができます。例えば:
Policy:
class PostPolicy < ApplicationPolicy
def admin_list?
user.admin?
end
end
Controller:
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
end
authorizeは渡されたオブジェクトを返すため、次のようにチェーンできます。
def show
@user = authorize User.find(params[:id])
end
viewとcontrollerの両方でpolicy methodを使用して、policy
のインスタンスを簡単に取得できます。これは、viewにリンクまたはボタンを条件付きで表示する場合に特に便利です。
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
Headless policies
対応する model / ruby クラスのないpolicyがある場合、シンボルを渡すことで取得できます。
# app/policies/dashboard_policy.rb
class DashboardPolicy < Struct.new(:user, :dashboard)
# ...
end
headless policyには2つの引数を渡す必要があることに注意してください。 2番目の引数は、この場合、シンボル:dashboard
になります。これは、以下で許可するレコードとして渡されるものです。
# In controllers
authorize :dashboard, :show?
# In views
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
Scopes
多くの場合、特定のユーザーがアクセスできるレコードを一覧表示ためのviewが必要になります。 Punditを使用する場合、policy scopeと呼ばれるクラスを定義する必要があります。次のようになります。
class PostPolicy < ApplicationPolicy
class Scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
end
end
private
attr_reader :user, :scope
end
def update?
user.admin? or not record.published?
end
end
Punditは、このクラスについて次のことを想定しています。
- クラスはScopeという名前を有しており、policy classにネストされています。
- 最初の引数はユーザーです。コントローラーで、Punditはこの引数に渡すためにcurrent_userを呼び出します。
- 2番目の引数は、何らかのクエリを実行するための何らかのスコープです。通常はActiveRecordクラスまたは
ActiveRecord :: Relation
になりますが、まったく別のものにもなります。 - このクラスのインスタンスは
resolve
メソッドに応答します。resolve
メソッドは繰り返すことのできる結果を返す必要があります。 ActiveRecordクラスの場合、これは通常ActiveRecord :: Relation
になります。
generatorによって生成されたapplication policy scopeから継承するか、継承する独自の基本クラスを作成する必要があるでしょう。
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
end
end
end
def update?
user.admin? or not record.published?
end
end
これで、policy_scope
メソッドを介してコントローラーからこのクラスを使用できます。
def index
@posts = policy_scope(Post)
end
def show
@post = policy_scope(Post).find(params[:id])
end
authorizeメソッドと同様に、policy scope classをオーバーライドすることもできます。
def index
# publication_class => Post
@publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope)
end
ポリシーと同様に、PostPolicy :: Scope
クラスを使用することを自動的に推測し、このクラスをインスタンス化し、インスタンスでresolve
を呼び出します。この場合、以下が実行するためのショートカットになります。
def index
@posts = PostPolicy::Scope.new(current_user, Post).resolve
end
viewでこのメソッドを使用することができ、そしてその使用が推奨されます。
<% policy_scope(@user.posts).each do |post| %>
<p><%= link_to post.title, post_path(post) %></p>
<% end %>
INFO
この記事はPundit公式に翻訳許可を頂いた上で公開しております。
gem version:2.1.0
参照元:https://github.com/varvet/pundit README