Note: This was written for Laravel 5.0, not 5.1. I will be updating this article for 5.1 when I can.

When using Laravel 4, creating a simple user access control was rather easy using route filters. With Laravel 5, things have changed with the addition of middleware.

Adding a middleware to provide access control is just as simple.

See the related GitHub repo here: https://github.com/jralph/article-laravel5-permissions

The End Goal

The end goal of this article is to provide you with the information I have used to create a basic access control using middleware.

In the end, we should be able to do something like this to control our route access.

// Allow users with the permission "access" to see the page.
Route::get('/test', [
    'middleware' => ['auth', 'permissions.required'],
    'permissions' => 'access',
    'uses' => 'MyController@myAction'
]);

// Allow users with "access" OR "admin" to see the page.
Route::get('/test', [
    'middleware' => ['auth', 'permissions.required'],
    'permissions' => ['access', 'admin'],
    'uses' => 'MyController@myAction'
]);

// Allow users with "access" AND "admin" to see the page.
Route::get('/test', [
    'middleware' => ['auth', 'permissions.required'],
    'permissions' => ['access', 'admin'],
    'permissions_require_all' => true,
    'uses' => 'MyController@myAction'
]);

When a user has been denied access to a page, they will be presented with a http 401 error.

The Database

Now we have a goal, we are almost ready to create the code, but first we need some tables to store our data.

We will be using the following tables:

  • users: This table stores all of the user data.
  • permissions: This table stores all of the permissions available.
  • permission_user: This table stores the link between users and permissions.

The User Table

By default, Laravel 5 comes with a migration already setup. For simplicity, we will just use this, but feel free to use your own.

The Permissions Table

Generate a new migration by running php artisan make:migration create_permissions_table and add the following code to it.

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePermissionsTable extends Migration {

      /**
      * Run the migrations.
      *
      * @return void
      */
      public function up()
      {
          Schema::create('permissions', function(Blueprint $table)
          {
              $table->increments('id');
              $table->string('name');
              $table->string('slug')->unique();
              $table->text('description')->nullable();
              $table->timestamps();
          });
      }

      /**
      * Reverse the migrations.
      *
      * @return void
      */
      public function down()
      {
          Schema::drop('permissions');
      }

}

The Permission User Table

Generate a new migration by running php artisan make:migration create_permission_user_table and add the following code to it.

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePermissionUserTable extends Migration {

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('permission_user', function(Blueprint $table)
        {
            $table->increments('id');
            $table->integer('permission_id');
            $table->integer('user_id');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('permission_user');
    }

}

Running The Migrations

Now we have some migrations created for our system, we can run them using php artisan migrate.

Models

By default, Laravel 5 comes with a User model, so we do not have to create it. We will need to add the permissions method though.

Add the following to the App\User model.

public function permissions()
{
    return $this->belongsToMany('App\Permission')->withTimestamps();
}

Now we need a Permission model. Run the command php artisan make:model Permission to generate one now.

Add the following code to the App\Permission model.

public function users()
{
    return $this->hasMany('App\User')->withTimestamps();
}

Now we have our models setup and ready to go, we can add some test data.

Test Data

You may also want to add some test data to use. This can be done by creating a few seeds or by using artisan's tinker ability.

// Using "php artisan tinker"

// Create a new user.
$user = new App\User;
$user->name = 'Joe';
$user->email = 'Bloggs';
$user->password = Hash::make('password');
$user->save();

// Create a new permission.
$permission = new App\Permission;
$permission->name = 'Admin';
$permission->slug = 'admin';
$permission->description = 'This is the admin permission.';
$permission->save();

// Sync the permission with the users permissions.
$user->permissions()->sync([1]);

The above code, when run using php artisan tinker will add a user called Joe with the password password. It will also create a permission called Admin. We will then assign the permission to the user.

The Middleware

Now we have our database and test data setup, we can create the middleware required to check for permissions.

Lets call our middleware permissions.required or App\Http\Middleware\PermissionsRequiredMiddleware.

We can generate this using the following artisan command.

php artisan make:middleware PermissionsRequiredMiddleware

In this middleware we will be processing the following things:

  • Check for a logged in user.
  • Get the current route.
  • Get the routes actions.
  • Fetch the permissions action.
  • Fetch the user permissions.
  • Sort both sets of permissions.
  • Check if a user must have all permissions or just one.
  • Return a 401 error if a user does not have permission.

First off, we need to check if a user is logged in. To do this, we can use the $request object that is passed through to the middleware's handle method.

public function handle($request, Closure $next)
{
    // Check if a user is logged in.
    if (!$user = $request->user())
    {
        return $next($request);
    }
}

You will notice that we do not bother throwing an error here, this is because this functionality can be performed by Laravel's auth middleware. You could easily add this functionality if required by changing the return $next($request); into return abort(401);.

Next we need to get the current route and the permissions we told the route to require, and check if we actually have permissions to check against.

This can be done by accessing the route through the $request object, then accessing the permissions action through that.

public function handle($request, Closure $next)
{
    // Previous Code

    // Get the current route.
    $route = $request->route();

    // Get the current route actions.
    $actions = $route->getAction();

    // Check if we have any permissions to check the user has.
    if (!$permissions = isset($actions['permissions']) ? $actions['permissions'] : null)
    {
        // No permissions to check, allow access.
        return $next($request);
    }
}

Now we know that a user is logged in and we have permissions to check, we can proceed with getting the users permissions and making sure they match.

public function handle($request, Closure $next)
   {
    // Previous Code

    // Fetch all of the matching user permissions.
    $userPermissions = array_fetch($user->permissions()->whereIn('slug', (array) $permissions)->get()->toArray(), 'slug');

    // Turn the permissions we require into an array.
    $permissions = (array) $permissions;

    // Check if we require all permissions, or just one.
    if (isset($actions['permissions_require_all']))
    {
        // If user has EVERY permission required.
        if (count($permissions) == count($userPermissions))
        {    
            // Access is granted.
            return $next($request);
        }
    } else {
        // If the user has the permission.
        if (count($userPermissions) >= 1)
        {
            // Access is granted and the rest of the permissions are ignored.
            return $next($request);
        }
    }

    // If we reach this far, the user does not have the required permissions.
    return abort(401);
}

In the above code we first fetch the slug of every user permission. This is done by using a helper provided by Laravel. The array_fetch function. This allows us to fetch a specific key from all items in an array, in our case, the slug key.

Then we make sure that the permissions that have been required are in the form of an array. This is done so that we can pass in a string for just one permission, or an array for multiple.

Once done, we check if the user requires all of the permissions or not.

If the user requires all of the permissions, we check that both the $permissions array and the $userPermissions array contain the same number of elements. If so, we allow the request to continue.

If the user does not require all of the permissions, we check that the count of $userPermissions is greater than or equal to 1. If so, we allow the request to continue.

After this, we just simply abort. If a user has made it this far and not been allowed acess, they do not have the permissions.

The Full Middleware Code

<?php namespace App\Http\Middleware;

use Closure;

class PermissionsRequiredMiddleware {

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {    
        // Check if a user is logged in.
        if (!$user = $request->user())
        {
            return $next($request);
        }

        // Get the current route.
        $route = $request->route();

        // Get the current route actions.
        $actions = $route->getAction();

        // Check if we have any permissions to check the user has.
        if (!$permissions = isset($actions['permissons']) ? $actions['permissons'] : null)
        {    
            // No permissions to check, allow access.
            return $next($request);
        }

        // Fetch all of the matching user permissions.
        $userPermissions = array_fetch($user->permissions()->whereIn('slug', (array) $permissions)->get()->toArray(), 'slug');

        // Turn the permissions we require into an array.
        $permissions = (array) $permissions;

        // Check if we require all permissions, or just one.
        if (isset($actions['permissions_require_all']))
        {
            // If user has EVERY permission required.
            if (count($permissions) == count($userPermissions))
            {    
                // Access is granted.
                return $next($request);
            }
        } else {
            // If the user has the permission.
            if (count($userPermissions) >= 1)
            {
                // Access is granted and the rest of the permissions are ignored.
                return $next($request);
            }
        }

        // If we reach this far, the user does not have the required permissions.
        return abort(401);
    }

}

Now we have our middleware created, we just need to add it to the app/http/Kernel.php file and we are ready to go.

In app/http/Kernel.php add the following line to the $routeMiddleware paramater.

'permissions.required' => 'App\Http\Middleware\PermissionsRequiredMiddleware',

Note: Do not add this middleware to the $middleware parameter as this will make it apply to every route. This will also cause the middleware to break as the $request->route() is not available when the global middleware run. If you want to apply this to all routes, just add the middlware to a route group.

Closing Notes

Now we have a very simple route permissions system setup and ready to go. This could easily be expanded on to provide more comprehensive options and abilities quite easily.

You may also want to note that we use the actions of the route to provide the ability to specify permissions. Do take care with this as if we accidently overwrite one of the actions that Laravel requries, it could cause issues with our application.

© 2024. All Rights Reserved.