Laravelのゲートを使用してみた

Laravelには認証に加えて認可も手軽に実装できる方法を提供しています。Laravelの認可には「ゲート」と「ポリシー」の２つが用意されています。今回は「ゲート」を利用して特定のユーザーのアクションを制御しみます。

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

「ポリシー」についてはこちらの記事で解説しています。

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

ゲートを利用するために、サクッと認証を用意するには下記の記事を参考に。

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

## ゲートの作成

``App\Providers\AuthServiceProvider``クラスに処理を追加します。例では、各記事の更新ページは、ログインしているユーザーが作成したページのみ表示できるようにしています。

```php:app/Providers/AuthServiceProvider.php
namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use App\Models\Article;
use App\Models\User;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Models\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Gate::define('update-article', function (User $user, Article $article) {
            return $user->id === $article->user_id;
        });
    }
}

```

厳密な型の比較でusersテーブルのidとarticlesテーブルのuser_idを比較しています。ここで少しハマりポイントなのですが、articlesテーブルのuser_idをint型で定義していてもmodelではstringとして扱われてしまうので、この比較ではすべて``false``が返ってしまします。ですので、型をあわせるためにarticleモデルのほうでuser_idをintにキャストします。

```php:app/Models/Article.php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasFactory;

    protected $guard = ['id'];

    protected $casts = [
        'user_id' => 'integer'
    ];
}

```

## ゲートの利用

``AuthServiceProvider``で定義したゲートを利用してアクションを認可してみます。``ArticlesController``からゲート認可メソッドを呼び出して、認証済みユーザーが自身が作成したページ以外の記事更新ページをリクエストしたら``403``を返すようにします。

```php:app/Http/Controllers/ArticlesController.php
namespace App\Http\Controllers;

use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;

class ArticlesController extends Controller
{
    // 更新メソッドなど

    public function edit(Article $article)
    {
        if (! Gate::allows('update-article', $article)) {
            abort(403);
        }

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

```

### 特定のユーザーにはすべてのページを見れるようにする

例えば、一般ユーザーは自身が作成したページのみしか更新できないようにして、管理者はすべてのページを更新可能にしたいことがあるかと思います。「ゲートチェックの割り込み」を利用することによってこの機能を実現できるようになります。ゲートチェックの割り込みはすべての認可メソッドの前後に挟むことができます。今回はすべての認可メソッドの前に割り込みし、「admin」ロールが付与されたユーザーはすべての更新ページを見れるようにします。


### usersテーブルにroleカラムを追加

まずはmigrationを利用してusersテーブルにroleカラムを追加します。usersテーブルを作成するマイグレーションファイルはlaravelのインストール時に含まれているので、カラム追加用のマイグレーションファイルを用意します。

```
php artisan make:migration add_role_to_users_table --table=users
```

マイグレーションファイル

```php:xxxx_xx_xx_xxxxxx_add_role_to_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddRoleToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('role', 16)
                ->after('password')
                ->default('user');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('role');
        });
    }
}

```

作成したマイグレーションファイルを実行します。

```
php artisan migrate
```

usersテーブルにroleカラムが追加されました。任意のユーザーのroleをadminに変更します。

```sql
UPDATE users SET role='admin' WHERE id=3
```

これで一般ユーザーと管理者ユーザーが作成できました。

### 割り込み処理を追加

Gateファサードの``before``メソッドを使用することで、すべての認可チェックの前に特定の処理を実行できます。今回は認証済みユーザーのroleがadminの場合にアクションを許可するようにします。

```php:app/Providers/AuthServiceProvider
namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use App\Models\Article;
use App\Models\User;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Models\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Gate::before(function ($user, $ability) {
            if ($user->role === 'admin') {
                return true;
            }
        });

        Gate::define('update-article', function (User $user, Article $article) {
            return $user->id === $article->user_id;
        });
    }
}
```

これによって、adminロールが付与されたユーザーは自身が作成したページ以外のページも見れるようになります。