0
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-05-15

概要

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

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

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

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

使用方法

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

ポリシーについてのLaravel公式ドキュメント

ポリシーを作成する

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

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

<?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 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.phpproviders に登録する

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の他にServiceProviderAuthServiceProviderも登録する。これがないとGateファサードが使用できないからだ。

Userモデルを拡張する

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

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

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::$userModelBackend\Models\Userにハードコードされている。

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

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

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->userparent::__construct()の後でないと使用できないので、parent::__construct()を呼び出した後に、メソッド追加処理を記述する。

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

アクセス制御を実行する

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

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);
        }
        ...
    }
}

以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0