Laravelのポリシーを使用してみた

Laravelには認可機能として「ゲート」と「ポリシー」が用意されています。

https://readouble.com/laravel/8.x/ja/authorization.html

「ゲート」は下記の記事で試してみたので、今回は「ポリシー」を試してみます。

https://qiita.com/kiyo27/items/fb8520250cd9792b4b48


## ポリシーの生成

``artisan``コマンドでポリシーを生成できます。``--model``オプションを使用すればモデルに関連付けたポリシーを生成することができます。

```
php artisan make:policy ArticlePolicy --model=Article
```

``artisan``コマンドを実行すると``app/Policies``ディレクトにファイルが生成されます。生成されたファイルにはいくつかのメソッドがあらかじめ作られています。

```php:app/Policies/ArticlePolicy.php
<?php

namespace App\Policies;

use App\Models\Article;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class ArticlePolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view any models.
     *
     * @param  \App\Models\User  $user
     * @return mixed
     */
    public function viewAny(User $user)
    {
        //
    }

    /**
     * Determine whether the user can view the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Article  $article
     * @return mixed
     */
    public function view(User $user, Article $article)
    {
        //
    }

    /**
     * Determine whether the user can create models.
     *
     * @param  \App\Models\User  $user
     * @return mixed
     */
    public function create(User $user)
    {
        //
    }

    /**
     * Determine whether the user can update the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Article  $article
     * @return mixed
     */
    public function update(User $user, Article $article)
    {
        //
    }

    /**
     * Determine whether the user can delete the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Article  $article
     * @return mixed
     */
    public function delete(User $user, Article $article)
    {
        //
    }

    /**
     * Determine whether the user can restore the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Article  $article
     * @return mixed
     */
    public function restore(User $user, Article $article)
    {
        //
    }

    /**
     * Determine whether the user can permanently delete the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Article  $article
     * @return mixed
     */
    public function forceDelete(User $user, Article $article)
    {
        //
    }
}
```

## ポリシーを使用したアクションの認可

ここから、ポリシーを使用してユーザーにある操作の許可・拒否を実装していきます。例えば記事の更新は、記事を作成したユーザーのみしかできないようにしてみます。

```php:app/Policies/ArticlePolicy.php
    /**
     * Determine whether the user can update the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Article  $article
     * @return mixed
     */
    public function update(User $user, Article $article, $category, $role)
    {
        return $user->id === $article->user_id;
    }
```


定義したアクションは、

- ユーザーモデル経由
- コントローラヘルパ経由
- ミドルウェア経由
- Bladeテンプレート経由

で呼び出せます。

### ユーザーモデル経由

``App\Models\User``モデルにはアクションを認可するための特別なメソッド``can``・``cannot``が含まれていて、これらのメソッドを使用することでアクションを認可することができます。

```php:app/Http/Controllers/ArticlesController.php
    public function edit(Request $request, Article $article)
    {
        if ($request->user()->cannot('update', $article)) {
            abort(403);
        }

        return view('articles.edit', ['article' => $article]);
    }
```

### コントローラヘルパ経由

コントローラから``authorize``メソッドを呼び出すことによっても、アクションを認可することができます。

```php:app/Http/Controllers/ArticlesController.php
    public function edit(Request $request, Article $article)
    {
        $this->authorize('update', $article);

        return view('articles.edit', $article);
    }
```

### ミドルウェア経由

``can``ミドルウェアを使用することで、アクションの認可ができます。

```php:routes/web.php
Route::get('/article/edit/{article}', 'App\Http\Controllers\ArticlesController@edit')
    ->middleware(['auth'])
    ->middleware('can:update,article');
```

### Bladeテンプレート経由

bladeテンプレート内で``@can``または``@cannot``ディレクティブを使用することで、アクションを認可できます。

```html:edit.blade.php
@can('update', $article)
<input type="submit" value="send">
@else
<h3>更新できません</h3>
@endcan
```

### ポリシーフィルター

ポリシーファイルで``before``メソッドを定義すると、ほかのポリシーメソッドが呼び出される前に実行されます。例えば``admin``ユーザーはすべてのアクションを許可したいとなった場合、

```php:app/Policies/ArticlePolicy.php
    public function before(User $user, $ability)
    {
        if ($user->role === 'admin') {
            return true;
        }
    }
```

このように定義すると``admin``ユーザーをすべてのアクションが許可されます。

### ポリシーメソッドに追加のパラメータを渡す

例えば、あるカテゴリーの記事に関してはすべてのユーザーが更新できるようにしたいとなった場合、

```php:ArticlePolicy.php
    /**
     * Determine whether the user can update the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Article  $article
     * @return mixed
     */
    public function update(User $user, Article $article, $category)
    {
        return $user->id === $article->user_id || $category === 'free';
    }
```

このように、``update``メソッドの第3引数に``$category``を追加します。ポリシーを呼び出す側では、

```php:ArticlesController.php
    public function edit(Request $request, Article $article)
    {
        $this->authorize('update', [$article, $request->category]);

        return view('articles.edit', $article);
    }
```

このように、最初の引数にアクション名を渡し2番目の引数に配列を渡します。配列の1番目には、どのポリシーを使用するかを指定し、2番目以降にポリシーメソッドに渡すパラメータを指定します。

## おまけ

ポリシーを使用するにあたって、認証機能・必要なテーブルデータを用意する必要がある場合は下記の記事を参考に。

https://qiita.com/kiyo27/items/5c6d0cd4c344ebe81b71

https://qiita.com/kiyo27/items/8ddb01bf5ef3d769cf66