Note: This article is done using Laravel 4 and will not work in the new Laravel 5. See here for the Laravel 5 version: http://josephralph.co.uk/laravel-5-simple-route-access-control/

When using Laravel you will notice how easy and quick it is to authenticate users into your system.

In this article I am going to expand on what Laravel already offers and add in the functionality to allow/disallow route access based on a users permissions.

The End Goal

The final goal of this setup is to enable a form of permission based route access.

The final working routes could look something like below

// A route with only auth.
Route::get('admin', [
    'before' => 'auth',
    'uses' => 'AdminController@index'
    'as' => 'admin'
]);

// A route requiring the 'user-manager' permission.
Route::get('admin/users', [
    'before' => 'auth|permission:user-manager',
    'uses' => 'Admin\UsersController@index',
    'as' => 'admin.user'
]);

// A route requiring either the 'user-manager' OR 'accounts-manager' permission.
Route::get('admin/accounts', [
    'before' => 'auth|permission:user-manager,accounts-manager',
    'uses' => 'Admin\AccountsController@index',
    'as' => 'admin.accounts'
   ]);

// A route requiring 'user-manager' AND 'accounts-manager' permissions.
Route::get('admin/accounts/users', [
    'before' => 'auth|permissions:user-manager,accounts-manager',
    'uses' => 'Admin\AccountsUsersController@index',
    'as' => 'admin.accounts.user'
]);

The first route is just a standard route to the admin dashboard that only requires a user to be logged in.

The second route requires that a user has the user-manager permission and is logged in.

The third route requires that a user has the user-manager or accounts-manager permission. If the user has either of the specified permissions, they are granted access.

The fourth and final route requires that a user has the user-manager and accounts-manager permissions. If the user has one but not the other, they are denied access.

The Database

Now we have a goal, lets set up our database to help accomplish this.

Users Table

Create a users table migration using the following command: php artisan migrate:make create_users_table or any other method you prefer.

Once created, add the below setup to the schema creation. (Feel free to change this to your needs.)

<?php

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

class CreateUsersTable extends Migration {

    /**
     * Run the migration and create the schema.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function(Blueprint $table)
        {
            $table->increments('id');
            $table->string('email')->unique();
            $table->string('password', 60);
            $table->rememberToken();
            $table->timestamps();
        });
    }

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

}

Permissions Table

Create a migration for the permissions table by using the following command: php artisan migrate:make create_permissions_table or any method you prefer.

Add the below to the schema creation.

<?php

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

class CreatePermissionsTable extends Migration {

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

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

}

Creating the Pivot Table

Now, as each user can have many permissions, we need a way to link the permissions to the users.

Lets create the permission_user table now.

<?php

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

class CreatePermissionUserTable extends Migration {

    /**
     * Run the migration and create the schema.
     *
     * @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 migration.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('permission_user');
    }

}

Once you have all of these migrations created, its time to run them. Give the php artisan migrate command a call.

Creating The Models

Now we have our database tables, we can create the required eloquent models and relationships for our users and permissions.

Lets create the following models.

// app/models/User.php
class User extends Eloquent implements UserInterface, RemindableInterface {

    use UserTrait, RemindableTrait;

    protected $table = 'users';

    protected $hidden = array('password', 'remember_token');

    /**
     * A user will belong to many permissions.
     */
    public function permissions()
    {
        return $this->belongsToMany('Permission')->withTimestamps();
    }

}

// app/models/Permission.php
class Permission extends Eloquent {

    /**
     * A permission will have many users.
     */
    public function users()
    {
        return $this->hasMany('User')->withTimestamps();   
    }

}

Add Some Data

Now we have our database structure and models setup, we will need to add some data to this for us to use.

You can do this using Laravel's seeds feature, or by using the php artisan tinker command and adding them manually.

$ php artisan tinker

$userOne = new User;
$userOne->email = 'admin@example.com';
$userOne->password = Hash::make('password'); // Nice and secure!
$userOne->save();

$userTwo = new User;
$userTwo->email = 'post-editor@example.com';
$userTwo->password = Hash::make('password'); // Nice and secure!
$userTwo->save();

$userThree = new User;
$userThree->email = 'content-editor@example.com';
$userThree->password = Hash::make('password'); // Nice and secure!
$userThree->save();

$permission = new Permission;
$permission->name = 'Post Editor';
$permission->slug = 'post-editor';
$permission->save();

$permission = new Permission;
$permission->name = 'Content Editor';
$permission->slug = 'content-editor';
$permission->save();

$userOne->permissions()->sync([1, 2]);
$userOne->save();
$userTwo->permissions()->attach(1);
$userTwo->save();
$userThree->permissions()->attach(2);
$userTwo->save();

Checking for Permissions

Now we have the database, models and test data setup, we can begin to create the functionality.

First we will create some handy methods within our Users model that let us check if a user has a requested permission or permissions.

Lets add the following method to our Users model.

public function hasPermissions($permissions, $requireAll = false)
{
    // Fetch all of the users permission slugs.
    $userPermissions = array_fetch($this->permissions->toArray(), 'slug');

    // Create an empty array to store the required permissions that the user has.
    $hasPermissions = [];

    // Loop through all of the required permissions.
    foreach ((array) $permissions as $permission) {

        // Check if the required permission is in the userPermissions array.
        if (in_array($permission, $userPermissions)) {

            // Add the permission to the array of required permissions that the user has.
            $hasPermissions[] = $permission;
        }
    }

    // If all permissions are required, check that the user has them all.
    if ($requireAll === true) {
        return $hasPermissions == (array) $permissions;
    }

    // If all are not required, check that the user has at least 1.
    return !empty($hasPermissions);
}

Now we have this handy method on our User model we can easily check if a user has a specified permission, one of an array of permissions or all of an array of permissions.

For example:

$user = User::find(1);

// User has post-editor permission.
$user->hasPermissions('post-editor'); // TRUE

// User has content-editor permission.
$user->hasPermissions('content-editor'); // TRUE

// User has post-editor OR content-editor permission.
$user->hasPermissions(['post-editor', 'content-editor']); // TRUE

// User has post-ditor AND content-editor permissions.
$user->hasPermissions(['post-editor', 'content-editor'], true); // TRUE


$user = User::find(2);

// User has post-editor permission.
$user->hasPermissions('post-editor'); // TRUE

// User has content-editor permission.
$user->hasPermissions('content-editor'); // FALSE

// User has post-editor OR content-editor permission.
$user->hasPermissions(['post-editor', 'content-editor']); // TRUE

// User has post-editor AND content-editor permission.
$user->hasPermissions(['post-editor', 'content-editor'], true); // FALSE


$user = User::find(3);

$user = User::find(2);

// User has post-editor permission.
$user->hasPermissions('post-editor'); // FALSE

// User has content-editor permission.
$user->hasPermissions('content-editor'); // FALSE

// User has post-editor OR content-editor permission.
$user->hasPermissions(['post-editor', 'content-editor']); // FALSE

// User has post-editor AND content-editor permission.
$user->hasPermissions(['post-editor', 'content-editor'], true); // FALSE

Creating the Route Filter

Now we have our models setup to check for permissions, we can create the required filters to check for permissions when a route is accessed.

Route::filter('permission', function($route, $request, $value)
{
    $permissions = func_get_args();
    array_shift($permissions);
    array_shift($permissions);

    if (!Auth::user()->hasPermissions($permissions))
    {
        return App::abort(401);
    }
});

Route::filter('permissions', function($route, $request, $value)
{
    $permissions = func_get_args();
    array_shift($permissions);
    array_shift($permissions);

    if (!Auth::user()->hasPermissions($permissions, true))
    {
        return App::abort(401);
    }
});

For both filters, we get all of the function arguments and strip out the $route and $request paramaters, just leaving us with the $value paramater and any other permissions you may have passed to the filter.

Once we have the permissions that are required, we then run the check to make the sure the user has one or all of them. The first filter checks if the user has one of the required permissions (Note there is no true flag on for the $requireAll paramater). On the second filter, we check that the user has all of the permissions (Note there is a tru flag on the $requireAll paramater.).

Putting It All Together

Now we have our filter, we can put it all together similar to the example at the start of this article.

// Create an admin panel that requires auth.
Route::group(['prefix' => 'admin', 'before' => 'auth'], function()
{   
    // No permissions required.
    Route::get('/', function()
    {
        return 'You have reached the admin dashboard.';
    });

    // User must have post-editor OR content-editor permissions.
    Route::get('posts', [
        'before' => 'permission:post-editor,content-editor',
        function() {
            return 'You have reached the posts page.';
        }
    ]);

    // User must have content-editor permission.
    Route::get('media', [
        'before' => 'permission:content-editor',
        function() {
            return 'You have reached the media page.';
        }
    ]);

    // Userm ust have post-editor AND content-editor permissions.
    Route::get('posts/media', [
        'before' => 'permissions:post-editor,content-editor',
        function() {
            return 'You have reached the post media page.';
        }
    ]);
});

Now we have this, we can easily expand it to include the ability to always allow anyone with the permission of admin access to all routes. We would simpilt add a check to the model to always return true if the admin permission is present.

We could also expand the permissions to have parent permission, and allow all child permissions to be overriden by the parent. (For example, you could have a manager permission with content-manager and post-manager child permissions. Then you could allow anyone with the manager permission to access both areas.)

See a working github example here: https://github.com/jralph/article-laravel-permissions

Resources

Final Notes

Feel free to add any improvments or any bugs you have found in the comments below. I look forward to finding out what people think!

© 2017. All Rights Reserved.