Punditをなるべくやさしく解説する

[Pundit](https://github.com/varvet/pundit)について、なるべくやさしく解説します。
ドキュメントを翻訳したものに近い内容となっております。

- ドキュメントを読めるほど実力に自身がない人
- ライブラリを調べる際にいろんな記事を見て時間を消耗する人

上記の方々の助けとなれば幸いです。

※Railsで利用することを前提に話します。


# Punditとは

- Rubyのgem(ライブラリ)
- **認可**の仕組みを提供してくれる

「認可(Authorization)」と「認証(Authentication)」は意味が異なります。
詳しくは「[よくわかる認証と認可](https://dev.classmethod.jp/security/authentication-and-authorization/)」をご参照ください。

簡単に言うと「ユーザーによってページ表示の許可・拒否をしたり、表示情報の範囲を変えたりすることができるgem」です。

似たようなgemとしてcancancanがよく比較されますが、その違いは[こちらの記事](https://qiita.com/zaru/items/8bf7b41b33f3f55bd27d)から引用させていただきます。

> cancancanはユーザに対して、どんなアクションが許可するかを定義するのに対して、Punditではリソースに対して誰が許可されるのかを定義します。反対からの目線ですね。cancancanがコントローラ寄りならば、Punditはモデル寄りの責務です。また、cancancanがDSLなのに対し、PunditはピュアRubyな書き方になっています。


それでは、[ドキュメント](https://github.com/varvet/pundit)に沿って、簡単なチュートリアルを行ってみます。


# 前提
- Rails 5.2.2
- Ruby 2.5.4
- Pundit 2.0.1


# 簡単なチュートリアル

まずはPunditをインストール。Gemfileに以下を追記します。

```
gem "pundit"
```

利用したいコントローラーの継承元でPunditをincludeしましょう。

```ruby
class ApplicationController < ActionController::Base
  include Pundit
end
```

利用した方が楽なのでgeneratorを使います。

```
$ rails g pundit:install
```

これで、`app/policies/`配下に`application_policy.rb`というファイルが作成されます。
※下記は省略して記載しております。

「policy」は「方針」という意味ですので、`policies`配下のファイルは「認可のルールを記述するファイル」と認識して差し支えないと思います。


```ruby:app/policies/applicaiton_policy.rb
class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end
end
```

このファイルで定義されるクラス`ApplicationPolicy`を継承して、コントローラーごとの認可ルールを記述していきます。
※継承せずともできますが、した方が楽です。

`initializer`で定義される`user`はデフォルトで`current_user`が引数に割り当てられるようになっています。
`record`の方には対応するモデルのインスタンスを手動で割り当てます。

一例として、`post`という名前のモデルに対してpolicyを作成してみましょう。

```ruby:app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def update?
    user.admin? or not record.published?
  end
end
```

このように、
- `モデル名_policy.rb`でファイルを作成
- `モデル名Policy`でクラス名を定義
- `def アクション名?`で認可ルール(policy)を記述

という流れで利用していきます。

`def アクション名?`の返り値によって、認可するか否かを判断しています。
例えば上記の`def update?`でfalseが返ってくれば、
PostControllerのupdateアクションは拒否されて、`Pundit::NotAuthorizedError`が投げられます。

※デフォルトではこのような流れとなりますが、Controller側の記述によって、他のモデル・アクションから特定のpolicyを参照させることも可能です。


先述のpolicyファイルを適用するために、Controller側からPunditを呼び出しましょう。

```ruby:app/controller/posts_controller.rb
def update
  @post = Post.find(params[:id])
  authorize @post
  # インスタンスモデルを利用しない(policy側ではuserの情報のみ扱う)場合に限り、
  # authorize Post
  # という記述が可能。
  # ...
end
```

`authorize`メソッドによって、先ほどのpolicyファイルに記述された`def update?`が処理されます。
引数には対応するモデルオブジェクトを入れます。


簡単チュートリアルは以上となります。

# 利用Tips

ここではドキュメントを参照し、「知ってたら得するかも」と思ったPunditの利用方法について記述していきます。

## 異なるアクション名から特定のpolicyを参照したい場合

例えば、独自に定義した`publish`アクションで`index`アクションのpolicyを参照させたい場合は、
下記のように第二引数にシンボルを渡すことで可能となります。

```ruby:app/controller/posts_controller.rb
def publish
  @post = Post.find(params[:id])
  authorize @post, :index?
  # ...
end
```


## 異なるモデル名から特定のpolicyを参照したい場合

例えば、`hoge`モデルから生成したインスタンスオブジェクトで、`Post`モデル用のpolicyを参照したい場合、
下記のように第二引数にシンボルを渡すことで可能となります。

```ruby:app/controller/hoges_controller.rb
def update
  @hoge = Hoge.find(params[:id])
  authorize @hoge, policy_class: PostPolicy
  # ...
end
```


## `authorize`メソッドの引数に割り当てるオブジェクトがない場合

例えば、下記のようなpolicyがあるとします。

```ruby:app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def index?
    user.admin?
  end
end
```

`user`の情報しか扱っていないので、`authorize`メソッドの引数にオブジェクトを割り当てる必要がないです。
そんな時はこのように記述しましょう。

```ruby:app/controller/posts_controller.rb
def index
  @posts = Post.all
  authorize Post
  # ...
end
```

モデル名を引数にすることも可能です。


## 対応するモデルのないpolicyを作成したい場合
たとえば「ダッシュボード」ページ用のpolicyを作成したいとします。すると、このような流れで実装します。

```ruby:app/policies/dashboard_policy.rb
class DashboardPolicy < Struct.new(:user, :dashboard)
  # ...
end
```

ControllerやViewからは、下記のように呼び出します。

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


## Scope機能を利用する
ユーザー属性によって、表示情報の範囲を変えたい時があります。
例えば、管理ユーザーの場合、非公開の記事も表示するようにしたいですよね。
そんな時に便利なのが、PunditのScopeです。

```ruby:app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve
      if user.admin?
        scope.all
      else
        scope.where(published: true)
      end
    end
  end
end
```

下記のようにController側から`policy_scope()`を呼び出すと、対応するモデルのpolicyファイルに定義された`resolve()`の返り値が返ってきます。
`initialize`では先述と同様、`user`にはデフォルトで`current_user`が、`scope`には`policy_scope`の第一引数が割り当てられます。

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

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

これにより、ユーザー属性による表示リソースの制御が可能となります。


## ユーザーによって取得するストロングパラメータを変える
ユーザーによって受け取るPOSTが異なる場合があります。Pundit側で受け取るパラメータを制御してみましょう。

policyファイル内に`permitted_attributes`を定義し、Controller側でそれを呼び出すことで、簡単に制御することができます。

```ruby:app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def permitted_attributes
    if user.admin? || user.owner_of?(post)
      [:title, :body, :tag_list]
    else
      [:tag_list]
    end
  end
end
```
```ruby:app/controller/posts_controller.rb
def publish
  def update
    @post = Post.find(params[:id])
    if @post.update_attributes(post_params)
      redirect_to @post
    else
      render :edit
    end
  end

  private

  def post_params
    params.require(:post).permit(policy(@post).permitted_attributes)
  end
end
```