Search code examples
phplaravellaravel-middlewareauthorize

How Laravel implicit bound middleware work?


I have to update a Laravel 8/9 application and I have an eli5 question about how implicitly bound middleware work.

I have a logged in route to update an entity as follows:

Auth::routes(['verify' => true]);

Route::group(['middleware' => ['auth', 'verified']], function () {
  Route::group(['middleware' => ['a.b.c', 'can:update,entity']], function () {
    Route::put('/admin/entity/{entity}', 'App\Http\Controllers\EntityController@updateEntity')->name('entity.updateEntity');
  });
});

The auth, verified and a.b.c middlewares I understand, I think:

  • auth: check the user is logged in (check the auth()->user() return value?). It is a built-in middleware.
  • verified: check the user email has been verified (check the email_verified_at column in the DB?). It is a built-in middleware.
  • a.b.c: check the entity exists and can still be modified (custom middleware). It has been written for our application and I checked the code out.

But the can:update,entity middleware is implicitly bound, it checks the user can update the entity as I understand, but I cannot understand how the magic happens.

I checked the \Illuminate\Auth\Middleware\Authorize source code but it still does not click:

<?php

namespace Illuminate\Auth\Middleware;

use Closure;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Database\Eloquent\Model;

class Authorize
{
    /**
     * The gate instance.
     *
     * @var \Illuminate\Contracts\Auth\Access\Gate
     */
    protected $gate;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Access\Gate  $gate
     * @return void
     */
    public function __construct(Gate $gate)
    {
        $this->gate = $gate;
    }

    /**
     * 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);
    }

    /**
     * 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();
    }

    /**
     * Get the model to authorize.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $model
     * @return \Illuminate\Database\Eloquent\Model|string
     */
    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);
        }
    }

    /**
     * Checks if the given string looks like a fully qualified class name.
     *
     * @param  string  $value
     * @return bool
     */
    protected function isClassName($value)
    {
        return str_contains($value, '\\');
    }
}

Like what are the $gate, $ability?

Can anyone explain to me how it happens under the hood like I'm 5 please?


Solution

  • As pointed out by @Jaquarh, Authorization is usually implemented via Policies, that group authorization logic around a particular model or resource, and that use Gates for single validation objectives.

    So how does that translate to my application?

    1. There is a policy defined for my Entities that is registered in App\Providers\AuthServiceProvider.php in the policies property:
    <?php
    
    namespace App\Providers;
    
    use App\Models\UserEntity;
    use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
    use Illuminate\Support\Facades\Gate;
    use Carbon\Carbon;
    
    class AuthServiceProvider extends ServiceProvider
    {
        /**
         * The policy mappings for the application.
         *
         * @var array<class-string, class-string>
         */
        protected $policies = [
            'App\Models\Entity' => 'App\Policies\EntityPolicy',
        ];
    
        /**
         * Register any authentication / authorization services.
         *
         * @return void
         */
        public function boot()
        {
            $this->registerPolicies();
        }
    }
    
    1. The EntityPolicy policy is defined in App\Policies\EntityPolicy.php and defines what actions can be authorized on Entities for the current User: create, edit and update. ※ Note that all 3 validations are delegated to 2 Gates: can-create-entity and user-has-entity.
    <?php
    
    namespace App\Policies;
    
    use App\Models\Entity;
    use App\Models\User;
    use Illuminate\Auth\Access\HandlesAuthorization;
    use Illuminate\Support\Facades\Gate;
    
    class EntityPolicy
    {
        use HandlesAuthorization;
    
        /**
         * Determine whether the user can create entities.
         *
         * @param  \App\Models\User  $user
         * @return bool
         */
        public function create(User $user)
        {
            return Gate::allows('can-create-entity');
        }
    
        /**
         * Determine whether the user can edit the entity.
         *
         * @param  \App\Models\User  $user
         * @param  \App\Models\Entity  $entity
         * @return mixed
         */
        public function edit(User $user, Entity $entity)
        {
            return Gate::allows('user-has-entity', $entity);
        }
    
        /**
         * Determine whether the user can update the entity.
         *
         * @param  \App\Models\User  $user
         * @param  \App\Models\Entity  $entity
         * @return mixed
         */
        public function update(User $user, Entity $entity)
        {
            return Gate::allows('user-has-entity', $entity);
        }
    }
    
    1. Gates are defined in the boot method of the App\Providers\AuthServiceProvider class using the Gate facade like follows:
    ...
        public function boot()
        {
            $this->registerPolicies();
    
            Gate::define('user-has-entity', function($user, $entity) {
                $userEntity = UserEntity::where('user_id', $user->id)
                    ->where('entity_id', $entity->id)
                    ->first();
                return boolval($userEntity);
            });
            Gate::define('can-create-entity', function($user) {
                $max = config('entities.MAX_AVAILABLE_ENTITIES');
                $now = Carbon::now();
                $current = $user
                            ->entities()
                            ->where('end', '>=', $now)
                            ->count();
                return $current < $max;
            });
        }
    ...
    

    I had read the Laravel documentation page some time ago, but it did not make sense immediately for me, now I think I finally get it and I hope it can help someone else too.