Railsの認可Gem「Pundit」で、漏れのない認可設定を上手に作るTips

* アプリ内の特定のデータに対して、**「このユーザーからは見れるようにしたいけど、あのユーザーには見せたくない」**といったケース、結構ありますよね。:frowning2:
* このように**「特定のユーザーにだけ、特定の行動を許可すること」**を**「認可」**といいます。
* 今回はそんな「認可設定」をしたい時に便利なGem、[Pundit](https://github.com/varvet/pundit)について色々と上手に使う方法を書いていこうと思います。
* **:warning:注意点**：Punditでは、**`current_user`（ログイン中のユーザーを取得する）メソッドを使う**ので、前もって**Devise Gemなどの「認証の仕組み」を入れておく必要があります**。

# Punditの概要
* まずは`pundit`をGemインストールした上で、`ApplicationController`に`Pundit`を`include`して、Punditを使う準備をします。（[リポジトリ参照](https://github.com/varvet/pundit#installation)）

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

* 次に、例えば「飼い主」に対して紐づいている、**「飼い犬のデータ:dog:」**に対して、以下のような「認可設定」をしたいとします。
    * **名前が記載されている:dog:にだけ**アクセスできる。
    * ログイン中のユーザーが**飼い主か、飼い主の知り合いの場合だけ**アクセスできる。
* この「アクセス」というのは、色々な方法があるのですが、今回は**「Dogの詳細ページにアクセスする」**場合のみ考えます。
* するとPunditではこう書けます。

```ruby
# app/policies/dog_policy.rb

class DogPolicy < ApplicationPolicy
  # 必ずtrueかfalseを返すメソッドにする
  # recordにはデータ（対象となるDogインスタンス）が自動で入る
  # userはcurrent_userが自動で入る
  def show?
    dog_owner = record.owner
    record.name? && (dog_owner == user || dog_owner.friend?(user))
  end
end
```

* これすごくよくわかりやすくないですか？:sunny:
* あとはこれを必要な箇所（主にコントローラー）から、`authorize`で呼び出します。

```ruby
class DogsController < ApplicationController
  def show
    # ログインユーザーがアクセスできるのかチェック
    @dog = authorize Dog.find(params[:id])
  end
end
```

* このGemの賢いのは、`authorize`で呼び出したアクション名とモデル名から**自動的にポリシーメソッドを選び出して適用してくれる**のです。
* 今回は「`Dog`モデルに対して`show`アクション内で呼び出している」ので、**`DogPolicy#show?`が自動で選ばれる**わけです。
* :warning:注意点としては、`show?`が`false`を返した場合、authorizeが`Pundit::NotAuthorizedError`をraiseするので、`ApplicationController`などで、**エラーを拾って403を返す仕組みを作っておく必要がある**ことです。

```ruby
# app/controllers/application_controller.rb

unless Rails.env.development?
  rescue_from Pundit::NotAuthorizedError, with: :render_403
end

def render_403
  # 中身は各自に任せる
  render template: 'pages/403', status: 403
end
```

# Punditを使う時に意識すること
* ここからが本題です。
* 先ほどのコードに戻ります。:point_down:

```ruby
# app/policies/dog_policy.rb

class DogPolicy < ApplicationPolicy
  # 必ずtrueかfalseを返すメソッドにする
  def show?
    dog_owner = record.owner
    record.name? && (dog_owner == user || dog_owner.friend?(user))
  end
end
```

* これ、分析してみると二つのセクションに分かれてるんです
    * データ:dog:の状態に関する条件文
    * ログインユーザーに関する条件文

* 当たり前と思われるかもしれませんが、認可のコードを書く時にこれらの二つをごちゃごちゃにしてしまって、**「認可の漏れや誤作動」が発生することがあります**。
* そのような事態を防ぐためにも、まずは
    * それぞれの認可設定で、**どちら（もしくは両方）の条件を考える必要があるのか**
    * 条件が複数ある場合、**「最も強い制約」がかかっているのはどの条件か**

をしっかりと認識する必要があります。

# 例題
* ちょっとややこしい例を考えてみます。
    * 名前が記載されている:dog:にだけアクセスできる。
    * それぞれの:dog:に対して**「非公開設定」ができて、非公開の場合には、飼い主以外その:dog:にアクセスできないようにする**。
    * ただし、**飼い主から「ライセンス」が与えられている場合には、「非公開」でもアクセスできる**。

* この場合まず押さえるべきは、**「どの条件が一番強いのか」**です。
* 例えば今回の仕様で、**「:dog:の名前がないとそもそもページが表示できない」**場合、**「最も強い制約」は「:dog:に名前が記載されていること」です**。
* なぜなら、たとえ飼い主であっても、:dog:の名前が無いのであれば**そもそもページが見れない（アプリに支障をきたす）からです**。
* なので、**「:dog:の名前があること」**をトップにもってきます。:point_down:

```ruby
class DogPolicy < ApplicationPolicy
  def show?
    record.name?
  end
end
```

* 次は何かと考えると、次は**「飼い主であること」か「飼い主のDog閲覧ライセンスをもっていること」**がくると思います。
* なぜなら「犬の名前が無い」ことを除いて、**この二つの制約を否定できる条件が一つもないから**です。（非公開設定に関わらず、アクセスできる）
* よって次はこうなります:point_down:

```ruby
class DogPolicy < ApplicationPolicy
  def show?
    return false unless record.name?

    dog_owner = record.owner
    dog_owner == user || dog_owner.gave_license_to?(user)
  end
end
```

* 最後に、残った「公開か非公開か」の条件文が入ってフィニッシュです。:point_down:

```ruby
class DogPolicy < ApplicationPolicy
  def show?
    return false unless record.name?

    dog_owner = record.owner
    return true if dog_owner == user || dog_owner.gave_license_to?(user)

    record.is_public?
  end
end
```

# まとめ
* Punditを使うと、「認可設定」を楽に実装できる。
* ただ認可設定をする時には、**「強さの順番」や「データの条件かログインユーザーの条件か」**をしっかりと認識して**「認可チェックをする時の流れをストーリーにすること」**が大事になる。