Laravel の "認可" まとめ（Gate, Policy 等）

## はじめに
この記事は [Laravel Advent Calendar 2019 - Qiita](https://qiita.com/advent-calendar/2019/laravel) の 4日目 の記事です🎉

「この認可処理はどこに書くべきか」みたいなディスカッションから
Laravel の認可機能を深掘りする機会があったので、
改めて整理してみたいと思います💪

認証については、よろしければこちらも！
[Laravel の Guard（認証） って実際何をやっているのじゃ？](https://qiita.com/tomoeine/items/40a966bf3801633cf90f)

## そもそも "認可" とはなんなのか

**著者に対してだけブログ記事の更新を許可する**
みたいなやつが "認可" です。

"認可" を真面目に考察したら、それだけで一記事書けてしまう、深い深いテーマです:hole:

### "認可" についてのざっくりとした理解
いったんここでは、以下の理解で記事を進めようと思います。
（異論あればコメントにお願いします！）

- 特定の条件に対して、**リソース**に対する**アクション**の権限を与える  
  （e.g. 「記事の著者」という条件に対して、記事を更新する権限を与える）
- 多くの場合、**認証に基づく**  
  （著者としてログイン済の場合に↑の認可が成り立つ）


クラスメソッドさんの以下の記事がていねいな解説でわかりやすいので、
深掘りしたい方はぜひご参考ください！
[よくわかる認証と認可](https://dev.classmethod.jp/security/authentication-and-authorization/)

## Laravel で "認可" する方法
ここからが本題💪
Laravel では認可のために以下の仕組みが用意されています。

- Gate
- Policy
- Request -> authorize()

それぞれ詳しく見ていきます👀

### Gate
>Gates are Closures that determine if a user is authorized to perform a given action and are typically defined in the App\Providers\AuthServiceProvider class using the Gate facade. 
https://laravel.com/docs/6.x/authorization#gates

>ゲートは、特定のアクションを実行できる許可が、あるユーザーにあるかを決めるクロージャのことです。
https://readouble.com/laravel/6.x/ja/authorization.html#%E3%82%B2%E3%83%BC%E3%83%88

認可について「**リソース**に対する**アクション**の権限を与える」と上述しましたが、
なんといきなりこの「リソース」が登場しないのが Gate です🙃

- クロージャーでシンプルに実装できる
- モデルやリソースに紐づかないアクションも認可できる

というのが Gate の特徴です。

Gate で認可するには、 `AuthServiceProvider` の `boot` メソッド内に `Gate` ファサードを使って定義します。

```AuthServiceProvider.php
Gate::define('edit-settings', function ($user) {
    return $user->isAdmin;
});
```

ドキュメントには

>Gates are most applicable to actions which are not related to any model or resource, such as viewing an administrator dashboard.
>管理者のダッシュボードのように、モデルやリソースとは関連しないアクションに対し、ゲートは主に適用されます。

と書かれていますが、リソースに対する認可が「できない」わけではありません。
サンプルにある通り以下のような実装も可能です。

```AuthServiceProvider.php
Gate::define('update-post', function ($user, $post) {
    return $user->id === $post->user_id;
});
```

ただしなんでもかんでも Gate に実装すると破綻するので、原則

- モデルやリソースに関するものは Policy
- それ以外は Gate

のように使い分けるのが良さそうです。

### Policy
>policies, like controllers, group their logic around a particular model or resource. 
>//
>Once the policy has been registered, you may add methods for each action it authorizes. 
https://laravel.com/docs/6.x/authorization#writing-policies

>ポリシーとは、特定のモデルやリソースに対するロジックをまとめたものです。
>//
>ポリシーは特定のモデルやリソースに関する認可ロジックを系統立てるクラスです。
https://readouble.com/laravel/6.x/ja/authorization.html#%E3%82%A4%E3%83%B3%E3%83%88%E3%83%AD%E3%83%80%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3

Policy については「モデルやリソースに対する」と明記されており、より "認可" らしい（？）機能です。

使い方の詳細はドキュメントに記述されているので省略しますが、
以下の手続きで使えます。

- Policy クラスを作る
- `AuthServiceProvider` で Policy クラスを登録する（[省略可※後述](#モデルポリシーの自動検出機能（Auto-Discovery Of Model Policies）)）

Policy クラスはモデルに応じて、クラス内のメソッドはアクションに応じて作ります。

以下ドキュメントより引用

```PostPolicy.php
class PostPolicy
{
    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }
}
```

Policy は コントローラーや middleware で `can` `cant` を呼び出して利用します。

```PostController.php
if ($user->can('update', $post)) {
    //
}
```

```routes/web.php
Route::put('/post/{post}', function (Post $post) {
    // 現在のユーザーはこのポストを更新できる
})->middleware('can:update,post');
```

### Request -> authorize()
実は Request クラスでも認可処理を書くことができます。
`artisan` で Request クラスを作ると以下のメソッドが作られるはず。
このメソッド内でユーザーアクションを認可できます。

```
    public function authorize() {
        return true;
    }
```

自分はこの `authorize()` を触ることはほぼないんだけど、どういうときに使うのかな？🤔
「ポリシーを使いたいけど、 `$user->can()` を Controller に書きたくないし、　`middleware` として使うのにも適していない」
みたいなシーンがあれば、ここに書くと良いのかもしれない。
Request クラスの中に認可とバリデーションがまとまってると、後から見たときにわかりやすいかも？
機会があれば試してみよう🙌


## Gate / Policy の便利機能たち
### モデルポリシーの自動検出機能（Auto-Discovery Of Model Policies）
Laravel 5.8 で追加された機能です。
Policy を使うには モデルとポリシーのマッピングを `AuthServiceProvider` で登録する必要がありましたが、
標準命名規則に従っている場合はこの登録作業が不要になりました。

`app/User.php` というモデルがある場合は `app/Policies` 下の **UserPolicy** をPolicy として自動検出します。
しかしほとんどの場合、 app 直下にモデルを置くことはないでしょう。
`app/Models` 以下ににモデルを配置したときは `app/Models/Policies` に Policy を書けということになりますが、これは気持ち悪い・・・
こんなとき、 `AuthServiceProvider` の `boot()` 内で以下のように定義しておけば、`app/Policies` 以下で自動検出してくれるようです🎉

```AuthServiceProvider.php
Gate::guessPolicyNamesUsing(function ($modelClass) {
    return 'App\\Policies\\' . class_basename ($modelClass).'Policy';
});
```

### 認可レスポンスの改善（Improved Authorization Responses）
Laravel 6 で追加された機能です。
Gate や Policy で true / false を返す代わりに `Response::allow()` / `Response::deny()` を返し、
呼び出し側では `Gate::inspect()` を呼び出すことで、 Policy から認可レスポンスを受け取ることができるようになりました。
deny 時に Policy で詳細なメッセージを設定してそれを画面表示する、みたいなことが簡単にできるようになりましたね😃

```PostPolicy.php
public function update(User $user, Post $post)
{
    return $user->id === $post->user_id
                ? Response::allow()
                : Response::deny('You do not own this post.');
}
```

````PostController.php
$response = Gate::inspect('update', $post);

if ($response->allowed()) {
    // アクションは認可された…
} else {
    echo $response->message();
}
```

### ゲストユーザーゲート／ポリシー（Guest User Gates / Policies）
これまでゲートやポリシーでは、未認証のユーザーに対しては一律 false を返していましたが、
以下のようにタイプヒントで `$user` を nullable にすることでチェックをパスさせられるようになりました。

ドキュメントには以下の例が載っていて

```AuthServiceProvider.php
Gate::define('update-post', function (?User $user, Post $post) {
    // ...
});
```
どう考えてもダメな使い方（未ログインなら記事を編集できちゃう）なので、「この機能いる？？」と疑問が湧きます😅
ユースケースとしては、以下の PR への Gary さんのコメントがわかりやすかったです。

```php
$this->authorize('view', $promotion) -> Is it christmas time? You can view that promotion, even as a guest.
$this->authorize('view', $picture) -> Is the picture publicly viewable? You can view those pictures, even as a guest.
$this->authorize('view', $page) -> Is this page published? You can view the page, even as a guest.
$this->authorize('view', $dashboard) -> Is this guest from a known EU IP address? You can view the dashboard, even as guest.
```

最初に認可は「多くの場合、認証に基づく」と書きましたが、
日時・リソースの状態・IPなど、 **認証に基づかない認可**を実現したいときに、この機能を利用できます。


PR コメントで賛否のディスカッションが盛り上がっていて面白かったです。
[[5.7] Allow Gates / Policies To Accept "Guests"](
https://github.com/laravel/framework/pull/24576)

## まとめ
やはり認可の基本は `Gate` / `Policy` ですね！
バージョンアップ毎に機能改善が行われているのでじゃんじゃん活用しましょう💪