LaravelのauthorizeResourceとその認可について調べた
こんにちは。
今日は、LaravelのControllerなどで呼び出すことが出来るauthorizeResource
メソッドについて調べてみました。
今回のバージョン
途中までは7.xのものを見ていたのですが、Gate辺りから8.xのソースを見てます。
処理を見てみる
実際のメソッドを見てみましょう。
/**
* Authorize a resource action based on the incoming request.
*
* @param string $model
* @param string|null $parameter
* @param array $options
* @param \Illuminate\Http\Request|null $request
* @return void
*/
public function authorizeResource($model, $parameter = null, array $options = [], $request = null)
{
$parameter = $parameter ?: Str::snake(class_basename($model)); // パラメータが指定されてなかったらクラス名をスネークケースで設定
$middleware = [];
foreach ($this->resourceAbilityMap() as $method => $ability) { // (1)
$modelName = in_array($method, $this->resourceMethodsWithoutModels()) ? $model : $parameter; // (2)
$middleware["can:{$ability},{$modelName}"][] = $method; // (3)
}
foreach ($middleware as $middlewareName => $methods) {
$this->middleware($middlewareName, $options)->only($methods);
}
}
いくつかよくわからない部分があったので抜き出してみます。
(1)の部分に関して
foreach ($this->resourceAbilityMap() as $method => $ability) {
resourceAbilityMapはコントローラのメソッド名=>アビリティ名
で配列が定義されています。
protected function resourceAbilityMap()
{
return [
'index' => 'viewAny',
'show' => 'view',
'create' => 'create',
'store' => 'create',
'edit' => 'update',
'update' => 'update',
'destroy' => 'delete',
];
}
アビリティ名はGate::define
で指定するものと、Policyで指定するものがあります。
大抵の場合はPolicyでの定義だと思います。
(2)の部分
$modelName = in_array($method, $this->resourceMethodsWithoutModels()) ? $model : $parameter;
protected function resourceMethodsWithoutModels()
{
return ['index', 'create', 'store'];
}
どうやら、メソッド名がindex
,create
,store
の場合だけモデルを指定するようです。
resouceMethodsWithoutModelsという名前なので、resourceのメソッドでmodelを使わないものだった場合は、$model
を利用し、そうでない場合は $parameter
を利用します。
この時点ではなぜこの場合だけこうするのかわかっていません。(先にネタバレしておくとroute parameterがつくかどうかでモデルクラスかモデルオブジェクトで判断するかが変わるため)
(3)の部分
$middleware["can:{$ability},{$modelName}"][] = $method;
middlewareに設定するためのデータを指定しているようです。
これまでの流れから、
"can:viewAny,App\Model\Post" => ["index"]
"can:view,post" => ["show"]
"can:create,App\Model\Post" => ["create", "store"]
"can:update,post" => ["edit", "update"]
"can:delete,post" => ["delete"]
となりますね。
$this->middleware($middlewareName, $options)->only($methods);
こちらの把握も大体できましたね。
これからは、実際に実行されるmiddlewareの方を見てみます。
canに関して
can
はIlluminate\Auth\Middleware\Authorize
クラスにて使用されます。
定義はApp\Http\Kernel
の$routeMiddleware
に存在します。
'can' => \Illuminate\Auth\Middleware\Authorize::class,
Authorizeクラスの実行handleを見てみましょう。
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $ability
* @param array|null ...$models
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function handle($request, Closure $next, $ability, ...$models)
{
$this->gate->authorize($ability, $this->getGateArguments($request, $models));
return $next($request);
}
$this->gate->authorize
を見る前に引数で使われているgetGateArguments
を見ます。
/**
* Get the arguments parameter for the gate.
*
* @param \Illuminate\Http\Request $request
* @param array|null $models
* @return \Illuminate\Database\Eloquent\Model|array|string
*/
protected function getGateArguments($request, $models)
{
if (is_null($models)) {
return [];
}
return collect($models)->map(function ($model) use ($request) {
return $model instanceof Model ? $model : $this->getModel($request, $model);
})->all();
}
getModel
メソッドも見ましょう。
protected function getModel($request, $model)
{
if ($this->isClassName($model)) { // \が存在したらクラスと判定している
return trim($model);
} else {
return $request->route($model, null) ?:
((preg_match("/^['\"](.*)['\"]$/", trim($model), $matches)) ? $matches[1] : null);
}
}
modelクラスのほうはわかりやすいので、後者をちゃんと確認します。
ちなみに、post
という文字列が$modelに入ってるとしましょう。(当初parameterとして指定した文字列です)
$request->route($model, null)
はシンタックスシュガーであるため、これは$request->route()->parameter($model, null)
と訳すことができます。
すでに取得される値は、route-model-binding後の結果の値になっているので、ちゃんとしていればmodelオブジェクトが返されると思います。
以下ドキュメントの内容です。パラメータと同じ名前でメソッドパラメータ(+タイプヒント)が指定されていた場合にReflectionを経由して暗黙のバインディングが発生することと、ModelクラスのgetRouteKeyName
メソッドで自動的に引っ張れること、事前にRoute::model
やRoute::bind
で返却値を調整できることを覚えておくと良いでしょう。
ということで
$this->gate->authorize($ability, $this->getGateArguments($request, $models));
は
$this->gate->authorize($ability, [Postクラスオブジェクトまたはパラメータの値]);
とみなすことができます。
Gateのauthorizeメソッドは何をやっている?
では、$this-gate->authorize
メソッドを見ていきます。
public function authorize($ability, $arguments = [])
{
return $this->inspect($ability, $arguments)->authorize();
}
どうやらinspectというメソッドを通したあとにauthorizeメソッドを呼び出すようです。
なお、通したあとのauthorizeメソッドは拒否されたかどうかを確認して、拒否されてたら例外をスローするようになっているので、inspectメソッドがわかれば全容がわかりそうです。
public function inspect($ability, $arguments = [])
{
try {
$result = $this->raw($ability, $arguments);
if ($result instanceof Response) {
return $result;
}
return $result ? Response::allow() : Response::deny();
} catch (AuthorizationException $e) {
return $e->toResponse();
}
}
rawメソッド以外はときにそれといって一般的なことをやってるのでrawを見てみます。
public function raw($ability, $arguments = [])
{
$arguments = Arr::wrap($arguments); // 必ずarrayが返されるようにする
$user = $this->resolveUser(); // アクセスしてきた認証ユーザーを取得する
// Gate::beforeで定義したコールバック関数が実行される
$result = $this->callBeforeCallbacks(
$user, $ability, $arguments
);
if (is_null($result)) { // Gate::beforeのチェックで通らなかったら
$result = $this->callAuthCallback($user, $ability, $arguments);
}
return tap($this->callAfterCallbacks( // Gate::afterを通したあとに
$user, $ability, $arguments, $result
), function ($result) use ($user, $ability, $arguments) {
$this->dispatchGateEvaluatedEvent($user, $ability, $arguments, $result);
});
}
どうやらメインとなってる処理は$this->callAuthCallback
のようですので見てみます。
protected function callAuthCallback($user, $ability, array $arguments)
{
$callback = $this->resolveAuthCallback($user, $ability, $arguments);
return $callback($user, ...$arguments);
}
resolveAuthCallbackを見ます。
結構掘り進んでしまったので忘れているかもですが、$abilityにはviewAnyやviewやcreateやdeleteといったauthorizeResourceで定義された値が入っていると今回はみなします。
argumentsには対象となっているモデルオブジェクトが入ってきてるだろうと考えます。(うまくやれなかったときはパラメータの値がそのまま入ってきています)
protected function resolveAuthCallback($user, $ability, array $arguments)
{
// 今回はこの部分だけです(ポリシークラスが存在する場合)
if (isset($arguments[0]) &&
! is_null($policy = $this->getPolicyFor($arguments[0])) &&
$callback = $this->resolvePolicyCallback($user, $ability, $arguments, $policy)) {
return $callback;
}
// Gate::define(アビリティ名, 文字列)で書かれた場合。雰囲気的にも、対象のクラスのメソッドを叩くように指定するパターンでしょうね
if (isset($this->stringCallbacks[$ability])) {
[$class, $method] = Str::parseCallback($this->stringCallbacks[$ability]);
if ($this->canBeCalledWithUser($user, $class, $method ?: '__invoke')) {
return $this->abilities[$ability];
}
}
// こっちはdefineの際にコールバックで書かれた場合でしょうね
if (isset($this->abilities[$ability]) &&
$this->canBeCalledWithUser($user, $this->abilities[$ability])) {
return $this->abilities[$ability];
}
return function () {
//
};
}
結構重要そうなメソッドが出てきました。
$this->getPolicyFor
メソッドを見てみます。
public function getPolicyFor($class)
{
if (is_object($class)) {
$class = get_class($class);
}
if (! is_string($class)) {
return;
}
if (isset($this->policies[$class])) { // Policyクラスで対応付けされている場合
return $this->resolvePolicy($this->policies[$class]);
}
// app/Policies/クラス名Policyで作られていた場合に自動でポリシーを見に行く
// この取得ルールはGate::guessPolicyNamesUsingで上書き可能
foreach ($this->guessPolicyName($class) as $guessedPolicy) {
if (class_exists($guessedPolicy)) {
return $this->resolvePolicy($guessedPolicy);
}
}
// クラス名のPolicyが無かったときに、そのサブクラスのポリシーがあった場合はそれを見る
// 主にはinterfeceの実装としてPolicyを定義していたときとかだろうか
foreach ($this->policies as $expected => $policy) {
if (is_subclass_of($class, $expected)) {
return $this->resolvePolicy($policy);
}
}
}
基本的に問題なければresolvePolicyに行ってるのでそちらを見ましょう。ここにはクラスの完全修飾名か、おそらくコールバックのオブジェクトが渡されるようです。
public function resolvePolicy($class)
{
return $this->container->make($class);
}
ということで、単にgetPolicyForは対象のポリシーオブジェクトがあればそれを取得するメソッドのようです。
getPolicyForのあとの
$callback = $this->resolvePolicyCallback($user, $ability, $arguments, $policy)
を見てみます。
protected function resolvePolicyCallback($user, $ability, array $arguments, $policy)
{
// 対象のアビリティメソッドが呼び出し可能であるか
if (! is_callable([$policy, $this->formatAbilityToMethod($ability)])) {
return false;
}
return function () use ($user, $ability, $arguments, $policy) {
// Policyクラスにbeforeメソッドがあったらそれを発火
$result = $this->callPolicyBefore(
$policy, $user, $ability, $arguments
);
if (! is_null($result)) {
return $result;
}
// 実行するメソッド名を整形
$method = $this->formatAbilityToMethod($ability);
// ポリシーのメソッドを実行する
return $this->callPolicyMethod($policy, $method, $user, $arguments);
};
}
protected function callPolicyMethod($policy, $method, $user, array $arguments)
{
// arguments[0]はルートパラメータのバインドがされずにただの値の文字列で渡っている場合が
// あるためここで取り除く(通常はモデルクラスの対象オブジェクトが入ってます)
// 例:posts/{post}と書いてあったとして、postというパラメータが解決できなければ
// 結果としてその{post}の部分の値が入ってしまう
// 一覧や新規作成ページには行けるけど、詳細や編集に行けない場合とかがそうっぽい?
if (isset($arguments[0]) && is_string($arguments[0])) {
array_shift($arguments);
}
if (! is_callable([$policy, $method])) {
return;
}
if ($this->canBeCalledWithUser($user, $policy, $method)) {
return $policy->{$method}($user, ...$arguments);
}
}
やっと実際にポリシークラスのメソッドを叩くところがありました。
Illuminate/Auth/Access/Gate.php
のresolvePolicyCallback
メソッドということで、Gateクラスの規模の大きさも感じつつなソースリーディングとなりました。
Gateクラスの辺りからは実際の認可処理のコードなのでそれほど読む必要は無かったのですが興味本位で読んだ感じですね。
最後に
ソースを読んだこともあって、認可に関する処理が深まりました。
良かったと思います。
認可、うまく使っていきましょう。
Discussion