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

Railsで権限管理を実装できるPunditの公式リファレンスを翻訳します。
Railsでの権限管理は`cancancan`が有名です。`cancancan`はユーザーに対して権限を実装するのに対して、`pundit`はmodelに対して権限を実装するという違いがあります。

# Pundit
Punditは、通常のRubyクラスとオブジェクト指向のデザインパターンを活用して、シンプルで堅牢かつ拡張性のある認証システムを構築するためのヘルパーを提供します。

## インストール
```
gem "pundit"
```

ApplicationControllerにPunditをincludeします。

```ruby

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`に配置することを推奨します。以下は、ユーザーが管理者である場合、または投稿が非公開の場合に投稿を更新できる簡単な例です。

```ruby

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から継承するか、継承するための独自の基本クラスを設定します。

```ruby

class PostPolicy < ApplicationPolicy
  def update?
    user.admin? or not record.published?
  end
end
```

生成された`ApplicationPolicy`の中で、モデルオブジェクトは`record`を呼び出します。
`Post`クラスのインスタンスがあると仮定すると、Punditによってコントローラーで以下のように記述できるようになります。

```ruby

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は次のようなことをすると想像できます。

```ruby

unless PostPolicy.new(current_user, @post).update?
  raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}"
end
```

チェックする権限の名前がアクション名と一致しない場合は、2番目の引数を渡して認証できます。例えば、

```ruby

def publish
  @post = Post.find(params[:id])
  authorize @post, :update?
  @post.publish!
  redirect_to @post
end
```

必要に応じて、ポリシークラスをオーバーライドする引数を渡すことができます。例えば、

```ruby

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：

```ruby

class PostPolicy < ApplicationPolicy
  def admin_list?
    user.admin?
  end
end
```

Controller:

```ruby

def admin_list
  authorize Post # we don't have a particular post to authorize
  # Rest of controller action
end
```

authorizeは渡されたオブジェクトを返すため、次のようにチェーンできます。

```ruby

def show
  @user = authorize User.find(params[:id])
end
```

viewとcontrollerの両方でpolicy methodを使用して、`policy`のインスタンスを簡単に取得できます。これは、viewにリンクまたはボタンを条件付きで表示する場合に特に便利です。

```erb

<% if policy(@post).update? %>
  <%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
```

## Headless policies
対応する model / ruby クラスのないpolicyがある場合、シンボルを渡すことで取得できます。

```ruby

# app/policies/dashboard_policy.rb
class DashboardPolicy < Struct.new(:user, :dashboard)
  # ...
end
```
headless policyには2つの引数を渡す必要があることに注意してください。 2番目の引数は、この場合、シンボル`：dashboard`になります。これは、以下で許可するレコードとして渡されるものです。

```ruby

# In controllers
authorize :dashboard, :show?
```

```ruby
# In views
<% if policy(:dashboard).show? %>
  <%= link_to 'Dashboard', dashboard_path %>
<% end %>
```

## Scopes
多くの場合、特定のユーザーがアクセスできるレコードを一覧表示ためのviewが必要になります。 Punditを使用する場合、policy scopeと呼ばれるクラスを定義する必要があります。次のようになります。

```ruby
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から継承するか、継承する独自の基本クラスを作成する必要があるでしょう。

```ruby
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`メソッドを介してコントローラーからこのクラスを使用できます。

```ruby
def index
  @posts = policy_scope(Post)
end

def show
  @post = policy_scope(Post).find(params[:id])
end
```

authorizeメソッドと同様に、policy scope classをオーバーライドすることもできます。

```ruby
def index
  # publication_class => Post
  @publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope)
end
```

ポリシーと同様に、`PostPolicy :: Scope`クラスを使用することを自動的に推測し、このクラスをインスタンス化し、インスタンスで`resolve`を呼び出します。この場合、以下が実行するためのショートカットになります。

```ruby
def index
  @posts = PostPolicy::Scope.new(current_user, Post).resolve
end
```

viewでこのメソッドを使用することができ、そしてその使用が推奨されます。

```ruby
<% policy_scope(@user.posts).each do |post| %>
  <p><%= link_to post.title, post_path(post) %></p>
<% end %>
```

# INFO
この記事はPundit公式に[翻訳許可](https://github.com/varvet/pundit/issues/629)を頂いた上で公開しております。
gem version：2.1.0
参照元：https://github.com/varvet/pundit README