OctoberCMS 管理画面実装テク：Laravel Auth Policyを使ってリソースの権限管理を実装する

# 概要
October管理画面は独自にPermissionという機能ごとのアクセス制御機能を備えているが、リソースごとのアクセス制御機能は提供していない。

そこでPermissionに加えて、LaravelのAuthorization Policyを使いたいと思ったが、残念ながらOctober管理画面ではPolicyをサポートしていなかったので、Policyの使い方を調べたのでメモ。

Policyでのリソースアクセス制御はUserモデル経由、ミドルウェア経由、コントローラヘルパなど、いくつかの方法で制御を実施できるが、今回はUserモデルのみ試している。

後日追記：シンプルなリソースアクセス制御の場合、無理してPolicyを使わずに、モデルイベントのフックとエラーハンドラできれいに実装できた。[こちらにそれをまとめている。](https://qiita.com/pikanji/items/1cd69edccb6408af40da)


# 使用方法
LaravelはGateとPolicyの２種類を提供しているが、ロジックをまとめるためPolicyを使用する。

[ポリシーについてのLaravel公式ドキュメント](https://laravel.com/docs/6.x/authorization#creating-policies)

### ポリシーを作成する
`php artisan make:policy`は無いので、Laravelから拝借して、微修正したテンプレートをここに貼る。
UserクラスはBackend\Models\Userに置き換える。

下記はProductというモデルクラスを対象の例としたアクセス制御Policyのテンプレート。

```php
<?php namespace MyAuthor\MyPlugin\Classes\Auth\Policies;

use Backend\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

use MyAuthor\MyPlugin\Models\Product;

class CustomProductPolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view all the custom product.
     *
     * @param  User  $user
     * @return bool
     */
    public function viewAny(User $user)
    {
        return ...;
    }

    /**
     * Determine whether the user can view the custom product.
     *
     * @param  User  $user
     * @param  Product $product
     * @return bool
     */
    public function view(User $user, Product $product)
    {
        return ...;
    }

    /**
     * Determine whether the user can create custom products.
     *
     * @param  User  $user
     * @return bool
     */
    public function create(User $user)
    {
        return ...;
    }

    /**
     * Determine whether the user can update the custom product.
     *
     * @param  User  $user
     * @param  Product $product
     * @return bool
     */
    public function update(User $user, Product $product)
    {
        return ...;
    }

    /**
     * Determine whether the user can delete the custom product.
     *
     * @param  User  $user
     * @param  Product $product
     * @return bool
     */
    public function delete(User $user, Product $product)
    {
        return ...;
    }

    /**
     * Determine whether the user can restore the custom product.
     *
     * @param  User  $user
     * @param  Product $product
     * @return bool
     */
    public function restore(User $user, Product $product)
    {
        return ...;
    }

    /**
     * Determine whether the user can permanently delete the custom product.
     *
     * @param  User  $user
     * @param  Product $product
     * @return bool
     */
    public function forceDelete(User $user, Product $product)
    {
        return ...;
    }
}

```
アクセスを許可する場合は `true` 、拒否する場合は `false` を返すように、それぞれのメソッドにそれを決定するロジックをニーズに応じて実装する。

### サービスプロバイダーを作成する
サービスプロバイダーを作成してモデルとポリシーの紐付けを定義する。
* `Illuminate\Foundation\Support\Providers\AuthServiceProvider` を extend してServiceProvider実装する。
* `$policies`にポリシーを適用する対象のモデルとポリシーを設定する。

```php
<?php namespace MyAuthor\MyPlugin\Classes\Auth;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider;
use Illuminate\Support\Facades\Gate;

use MyAuthor\MyPlugin\Models\Product;
use MyAuthor\MyPlugin\Classes\Auth\Policies\ProductPolicy;

class BackendAuthServiceProvider extends AuthServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        Product::class => ProductPolicy::class,
    ];

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

    public function registerPolicies()
    {
        foreach ($this->policies() as $key => $value) {
            Gate::policy($key, $value);
        }
    }
}

```

### サービスプロバイダーを登録する
作成したサービスプロバイダーを `config/app.php`の`providers` に登録する

```php:config/app.php
'providers' => array_merge(include(base_path('modules/system/providers.php')), [
    \System\ServiceProvider::class,
    \Illuminate\Auth\AuthServiceProvider::class,
    \Goodbet\Gbc\Classes\Auth\BackendAuthServiceProvider::class,
]),
```
ここでは、作成した`BackendAuthServiceProvider`の他に`ServiceProvider`と`AuthServiceProvider`も登録する。これがないとGateファサードが使用できないからだ。


### Userモデルを拡張する
Userモデル経由、ミドルウェア経由、コントローラヘルパなど、いくつかの方法で制御を実施できるが、ここではUserモデルでやってみる。
https://laravel.com/docs/6.x/authorization#authorizing-actions-using-policies

Laravelでは下記のような使い方になる。

```php
if ($this->user->can('view', $product)) {
    // OK、リソースのページ表示処理を続行する
} else {
    // Error、403ページを表示するレスポンスを返す
}
```

しかし、Octoberの管理画面ユーザはLaravelのユーザとは実装が異なるため、そのままでは`can`メソッドが使えない。

LaravelのUserクラスはベースが`Illuminate\Foundation\Auth\User`で、これが使用している`\Illuminate\Foundation\Auth\Access\Authorizable` traitが `can` メソッドなどを提供している。

Octoberの管理画面ユーザも`Illuminate\Foundation\Auth\User`をベースクラスにしたいところだが、管理画面ユーザもモデルクラスは`\Backend\Classes\AuthManager::$userModel`で`Backend\Models\User`にハードコードされている。

`AuthManager`を置き換えてしまえばできなくもないが、管理画面ユーザクラスに動的にcanメソッドなどを追加する方が簡単なのでそうする。

コントローラのコンストラクタの中で、`addDynamicMethod`を使用してメソッドを追加する。追加するメソッドは`\Illuminate\Foundation\Auth\Access\Authorizable` traitに実装されているものを参考にしている。

```php:ProductsController.php
class ProductsController extends \Backend\Classes\Controller
{
    public function __construct()
    {
        parent::__construct();

        /** @var \Backend\Models\User $currUser */
        $currUser = $this->user;
        $currUser->addDynamicMethod('can', function ($abilities, $arguments = []) use ($currUser) {
            return app(Gate::class)->forUser($currUser)->check($abilities, $arguments);
        });
        $currUser->addDynamicMethod('cant', function ($abilities, $arguments = []) use ($currUser) {
            return ! $currUser->can($abilities, $arguments);
        });
        $currUser->addDynamicMethod('cannot', function ($abilities, $arguments = []) use ($currUser) {
            return $currUser->cant($abilities, $arguments);
        });
    }
}
```
`$this->user`は`parent::__construct()`の後でないと使用できないので、`parent::__construct()`を呼び出した後に、メソッド追加処理を記述する。

これで使う準備が整った。

### アクセス制御を実行する

下記に、リソースのupdateページでのアクセス制御の例を示す。

```php
class ProductsController extends \Backend\Classes\Controller
{
    public $implement = [
        'Backend.Behaviors.FormController',
        ...
    ];

    public function update($recordId = null, $context = null)
    {
        // オーバライドしている FormController::update メソッドを呼んでおく。
        $this->asExtension('FormController')->update($recordId, $context);

        // 開いたページの操作対象のモデルを取得
        $product = $this->formGetModel();

        if (!$this->user->can('update', $product)) { // <- これ
            // リソースへのアクセス権がないので403ページを表示
            return Response::make(View::make('backend::access_denied'), 403);
        }
        ...
    }
}
```

以上