概要
October管理画面は独自にPermissionという機能ごとのアクセス制御機能を備えているが、リソースごとのアクセス制御機能は提供していない。
そこでPermissionに加えて、LaravelのAuthorization Policyを使いたいと思ったが、残念ながらOctober管理画面ではPolicyをサポートしていなかったので、Policyの使い方を調べたのでメモ。
Policyでのリソースアクセス制御はUserモデル経由、ミドルウェア経由、コントローラヘルパなど、いくつかの方法で制御を実施できるが、今回はUserモデルのみ試している。
後日追記:シンプルなリソースアクセス制御の場合、無理してPolicyを使わずに、モデルイベントのフックとエラーハンドラできれいに実装できた。こちらにそれをまとめている。
使用方法
LaravelはGateとPolicyの2種類を提供しているが、ロジックをまとめるためPolicyを使用する。
ポリシーを作成する
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.php
のproviders
に登録する
'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では下記のような使い方になる。
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に実装されているものを参考にしている。
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ページでのアクセス制御の例を示す。
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);
}
...
}
}
以上