2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

認可処理であるPolicyのソースコードリーディング(middleware編)

Posted at

ポリシーのソースコードリーディング

がんばりましょう。

Gateファサード

解決されるクラス Illuminate/Auth/Access/Gate.php

ポリシーの登録

ポリシーのマッピングはapp/Providers/AuthServiceProviderに記述します。

keyに関連するモデル、valueにポリシーを記述します。

app/Providers/AuthServiceProvider
protected $policies = [
    // 'App\Model' => 'App\Policies\ModelPolicy',
]

boot()の中で呼ばれているregisterPolicies()で登録していきます。

Illuminate/Foundation/Support/Providers/AuthServiceProvider.php
public function registerPolicies()
    {
        foreach ($this->policies() as $key => $value) {
            Gate::policy($key, $value);
        }
    }

$this->policies()は先程のpoliciesプロパティを返します。

Illuminate/Auth/Access/Gate::policies()が呼ばれます。

Illuminate/Auth/Access/Gate.php
public function policy($class, $policy)
{
    $this->policies[$class] = $policy;

    return $this;
}

Illuminate/Auth/Access/Gatepoliciesプロパティに同じ形で値を格納していきます。

これにてポリシーの登録は終了。

ポリシーをつかう

主なポリシーの使用方法は3つで、

  • User.phpから使う
  • Controllerから使う
  • ミドルウェアで使う

中身は大体同じなので、今回はミドルウェアで使うを例に見ていきます。

想定ルート、ミドルウェア

Route::get('/{book}/{comment}', 'BookController@index')->middleware('can:view,book,comment');

実際にアクセスされたURL

/1/2

bookはルートモデルバインディングで解決され、commentは解決されないものとします。

ここでの注意点は、middleware()内に書いた文字列のbookcommentに当たる部分は必ず同じ名前でURIに記載してください。

たとえば、

Route::get('/{book}/hoge', 'BookController@index')->middleware('can:view,book,comment');

のように、middleware()内にcommentと書いてあるのに、getの第一引数の中に{comment}が含まれていないと怒られます。

ポリシーはリソース(モデル)に対しての認可を提供するので、ルートモデルバインディングで解決されたインスタンスを利用します。(なのでミドルウェアの順番も重要です)

想定ポリシー

protected $polices = [
    Book::class => BookPolicy::class
]

B
BookPolicy::view()が定義されている。

みていこう

まずは'can:view,book,comment'がどのようにミドルウェアに変換されるか見ていきましょう。

ミドルウェアをごにょごにょしているのはここです。

Illuminate/Pipeline/Pipeline.php
protected function carry()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            [$name, $parameters] = $this->parsePipeString($pipe);

            $pipe = $this->getContainer()->make($name);

            $parameters = array_merge([$passable, $stack], $parameters);
        
            $carry = method_exists($pipe, $this->method)
                            ? $pipe->{$this->method}(...$parameters)
                            : $pipe(...$parameters);

            return $this->handleCarry($carry);
        };
    };
}

$pipeには'can:view,book'という文字列が入り、$passableにはRequestクラス、$stackは次のミドルウェアが入っています。

parsePipeString()でミドルウェア名とその他に分けます。

Illuminate/Pipeline/Pipeline.php
protected function parsePipeString($pipe)
{
    [$name, $parameters] = array_pad(explode(':', $pipe, 2), 2, []);

    if (is_string($parameters)) {
        $parameters = explode(',', $parameters);
    }

    return [$name, $parameters];
}

$name = 'can'
$parameters = ['view', 'book', 'comment']

$this->getContainer()->make($name)Illuminate/Auth/Middleware/Authorize.phpインスタンスを生成します。
(文字列とミドルウェアの対応はapp/Http/Kernel.phpに書いてあります。)

$this->methodにはhandleが入っているので、Illuminate/Auth/Middleware/Authorize::handle()が実行されます.

Illuminate/Auth/Middleware/Authorize.php
public function handle($request, Closure $next, $ability, ...$models)
{
    $this->gate->authorize($ability, $this->getGateArguments($request, $models));

    return $next($request);
}

$abilityviewが入り、...$modelsに残りもの(book comment)が渡されます。

まずはgetGateArguments()を見ていきましょう。

Illuminate/Auth/Middleware/Authorize.php
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();
}

コレクションクラスのmap()で一つずつ処理をしていきます。

今回$modelsは文字列なのでgetModel()が呼ばれます。

Illuminate/Auth/Middleware/Authorize.php
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);
    }
}

isClassNameで完全修飾名かどうかを判定しています。完全修飾名だとそのまま返すようです。

完全修飾名出ない場合を見ていきましょう。

Illuminate/Http/Request.php
public function route($param = null, $default = null)
{
    $route = call_user_func($this->getRouteResolver());

    if (is_null($route) || is_null($param)) {
        return $route;
    }

    return $route->parameter($param, $default);
}

$routeには現在のルートの情報を保持するRouteクラスが入ってきます。

Routeクラスのparametersプロパティにはルートモデルバインディングで解決されたモデルなどが入っています。

protected $parameters = [
    'book' => 解決されたBookモデル,
    'comment' => 2,

$route->parameter()は第一引数をkeyに$parametersから値を取得します。

今回のgetGateArguments()の返り値は配列になります。中身は[解決されたBookモデル, 2]です。

$this->gate->authorize('view', [解決されたBookモデル, 2])を見てきます。

Illuminate/Auth/Access/Gate.php
public function authorize($ability, $arguments = [])
{
    return $this->inspect($ability, $arguments)->authorize();
}

inspcetが呼ばれて、

Illuminate/Auth/Access/Gate.php
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が呼ばれ、

Illuminate/Auth/Access/Gate.php
public function raw($ability, $arguments = [])
{
    $arguments = Arr::wrap($arguments);

    $user = $this->resolveUser();

    $result = $this->callBeforeCallbacks(
        $user, $ability, $arguments
    );

    if (is_null($result)) {
        $result = $this->callAuthCallback($user, $ability, $arguments);
    }

    return $this->callAfterCallbacks(
        $user, $ability, $arguments, $result
    );
}

Arr:wrapは配列でないときは配列でラップしてくれるだけです。

$userにはログイン済みのユーザーが入ってきます。

callBeforeCallbacks()callAfterCallbacks()は、それぞれGate::before()Gate::after()で定義したコールバックが呼ばれます。

今回は関係ないのでcallAuthCallback()を見ていきます。

Illuminate/Auth/Access/Gate.php
protected function callAuthCallback($user, $ability, array $arguments)
{
    $callback = $this->resolveAuthCallback($user, $ability, $arguments);

    return $callback($user, ...$arguments);
}

resolveAuthCallbackは...

Illuminate/Auth/Access/Gate.php
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;
    }

    //...
}

$argumentsは[解決されたBookモデル, 2]なので、issetはtrueになり、getPolicyFor()は、

Illuminate/Auth/Access/Gate.php
public function getPolicyFor($class)
{
    if (is_object($class)) {
        $class = get_class($class);
    }

   if (isset($this->policies[$class])) {
       return $this->resolvePolicy($this->policies[$class]);
   }
    
    //...

    foreach ($this->policies as $expected => $policy) {
        if (is_subclass_of($class, $expected)) {
            return $this->resolvePolicy($policy);
        }
    }
}

ポリシーは

protected $polices = [
    Book::class => BookPolicy::class
]

と登録したので、$this->policies[$class]の返り値はBookPolicy::classになります。

resolvePolicyでクラス名からインスタンスを生成します。resolveAuthCallback()に戻ります。

Illuminate/Auth/Access/Gate.php
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;
    }
    
    //...
}

resolvePolicyCallback()はコールバックを返すみたいなのでコールバックが呼ばれるときに見てみることにします。

呼び出し元に戻ります。

Illuminate/Auth/Access/Gate.php
protected function callAuthCallback($user, $ability, array $arguments)
{
    $callback = $this->resolveAuthCallback($user, $ability, $arguments);

    return $callback($user, ...$arguments);
}

コールバックを呼び出してその返り値を返しているようです。案外登場が早かったのでコールバックの中身を見てみましょう。

Illuminate/Auth/Access/Gate.php
protected function resolvePolicyCallback($user, $ability, array $arguments, $policy)
{

    return function () use ($user, $ability, $arguments, $policy) {
        
        $result = $this->callPolicyBefore(
            $policy, $user, $ability, $arguments
        );

        if (! is_null($result)) {
            return $result;
        }

        $method = $this->formatAbilityToMethod($ability);

        return $this->callPolicyMethod($policy, $method, $user, $arguments);
    };
}

$userはログインユーザー、$abilityview$argumentは[解決されたBookモデル, 2]、$policyはさっき解決したBookPolicyインスタンス。

ポリシーにもbefore()がありますが今回は関係ないのでスルー。

Illuminate/Auth/Access/Gate.php
protected function callPolicyMethod($policy, $method, $user, array $arguments)
{
    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);
    }
}

$arguments[0]が文字列ならばここで抹消されます。

最終的に$policy->{$method}($user, ...$arguments)でポリシー内のメソッドが呼ばれます。ポリシーのメソッドはboolを返します。

なので、callAuthCallback()の返り値は、ポリシーの指定されたメソッドの返り値ということになります。

Illuminate/Auth/Access/Gate.php
public function raw($ability, $arguments = [])
{
    $arguments = Arr::wrap($arguments);

    $user = $this->resolveUser();

    if (is_null($result)) {
        $result = $this->callAuthCallback($user, $ability, $arguments);
    }

    return $this->callAfterCallbacks(
        $user, $ability, $arguments, $result
    );
}

afterを指定していないので、callAuthCallback()の返り値がそのまま返ります。

Illuminate/Auth/Access/Gate.php
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();
    }
}

$resultにポリシーのメソッドの返り値が格納されます。

Illuminate/Auth/Access/Response.php
public static function deny($message = null, $code = null)
{
    return new static(false, $message, $code);
}

public static function allow($message = null, $code = null)
{
    return new static(true, $message, $code);
}

Illuminate/Auth/Access/Response.phpを生成して返していますね。

Illuminate/Auth/Access/Gate.php
public function authorize($ability, $arguments = [])
{
    return $this->inspect($ability, $arguments)->authorize();
}

inspct()Illuminate/Auth/Access/Responseを返すので、Illuminate/Auth/Access/Response::authorize()が呼ばれます。

Illuminate/Auth/Access/Response.php
public function authorize()
{
    if ($this->denied()) {
        throw (new AuthorizationException($this->message(), $this->code()))
                    ->setResponse($this);
    }

    return $this;
}

ここでエラーレスポンスを返すか、次のミドルウェアへ処理を移すかの分岐になっていますね。

以上でポリシーの認可は終わりです!

まとめ

ポリシーはリソース(モデル)を利用するGateの糖衣のようなもの。
ルートモデルバインディングを前提としている。
before, afterなどのhookが用意されている。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?