11
5

More than 3 years have passed since last update.

【翻訳】Railsで権限管理を出来るgem, "Pundit"(前編)

Last updated at Posted at 2019-11-20

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

11
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5