This is a continuation from a previous article, found here: http://josephralph.co.uk/laravel-simple-route-based-access-control/

This article will cover a slightly more advanced and cleaner approach to creating the route filters.

The End Goal

The goal of this article is to explain how to improve our previous filters and how to add in the ability to provide an override for the permissions. (eg, The admin permission overrides all others.)

Setup

To proceed with the following steps, you will need to have some sort of autoloading setup for your custom php classes.

I personally do this by using PSR-4 autoloading within the composer.json file.

"autoload": {
    "classmap": [
        ...
    ],
    "psr-4": {
        "Acme\\": "app/src"
    }
},

This means that all classes under the Acme namespace can be found within the app/src folder.

Note: You may need to run a composer dump-autoload -o for the classes to register.

Route Filters

Upon browsing Laravel's documents you may have noticed the Route Filters section. Towards the end of this section a small mention is given about using class based filters. We will be using this to provide our filtering from now on.

Using our previous filters, change the filters to the following:

Route::filter('permission', 'Acme\Filters\PermissionFilter');
Route::filter('permissions', 'Acme\Filters\PermissionFilter');

The Filter Class

Now we have our route filters created, it's time to make them function correctly. To do this, we can use the following class.

Create a new class called PermissionFilter in your application. I have created it in the PSR-4 loaded directory app/src/filters, giving the class the namespace of Acme\Filters.

<?php namespace Acme\Filters;

use Illuminate\Routing\Route;
use Auth;
use App;

class PermissionFilter {

    /**
     * Run the filter with an instance of the route.
     */
    public function filter(Route $route)
    {
        // Get the all filters and paramaters from the route.
        $this->filters = $route->beforeFilters();

        // Only retrieve the paramaters from the requested permission filter.
        $permissions = $this->filters[$this->getFilterName()];

        // Check for access and return 401 if not successful.
        if (!$this->hasAccess($permissions))
        {
            return App::abort(401);
        }
    }

    /**
     * Return the name of the permission filter being run.
     *
     * @return string
     */
    private function getFilterName()
    {
        if (array_key_exists('permissions', $this->filters))
        {
            return 'permissions';
        }

        return 'permission';
    }

    /**
     * Work out if the filter should require all permissions
     * to be present on the user or not.
     *
     * @return boolean
     */
    private function shouldRequireAll()
    {
        switch ($this->getFilterName())
        {
            // If we are using the permissions filter, true.
            case 'permissions':
                return true;
            // Otherwise, false.
            case 'permission':
            default:
                return false;
        }
    }

    /**
     * Check if a user has access or not.
     *
     * @return boolean
     */
    private function hasAccess($permissions)
    {
        return Auth::user()
                ->hasPermissions($permissions, $this->shouldRequireAll());
    }

}

In the above class we have the main filter method that gets passed the $route instance by Laravel. We use this instance to retrieve the paramaters passed to our filter. (You may have noticed that we used func_get_args() in the last article. Using laravel's route object is much cleaner.)

This main filter function gets all of the filters and paramaters that were applied to the route and then retrieves the paramaters that are only related to our permission(s) call. It does this by using the getFilterName method, which just simply checks for either permissions or permission in the filter array.

The function then proceeds to check that the user has these permissions, using the hasAccess() method.

We pass in the permissions we have obtained from the filter array.

The hasAccess() function does the same as our pervious filters, it calls the hasPermissions($permissions, $requireAll) method on our User model that we created in the previous article.

This time, instead of passing in a static true or false, we pass in a method call that works out if we should be requiring them all or not based on the filter name. (If we use the permissions filter, we require all, if we use the permission filter, we do not.)

With this done, our filter is now fully functioning and provides the same features as our filters from last time, but is easier to maintain and easier to adapt. It also avoids having huge closures in our filters.php file.

Overrides

Now we have our class setup, lets have a look at expanding it a little more to allow specified permissions to override all others. (eg, We can have an admin permission that overrides all other permissions.)

Config

First off, we need somewhere to store what the override permission should be. Lets do this in a config file within app/config. You could do this in a database table or any way you want. Just change the code to work for you.

Below is a copy of the config file I am using in app/config/permissions.php.

<?php

return [

    'override' => [

        // Set an array or string of permissions that
        // can override others.
        'permissions' => 'admin',

        // If true, all permissions must be present for
        // the override to take place.
        'require_all' => false

    ],

];

In this file, we are saying that a user must have the admin permission to be able to override any other permissions.

We could change the permissions to an array ('permissions' => ['admin', 'manager']) and set the require_all option to true to only override permissions if the user has the admin and manager permissions.

Filter Class

Now we have the config setup, we can use this in our filter class.

Lets add a new canOverride method.

...
/**
 * This function should return true if the user can override
 * and false if not. 
 *
 * The implementation can be apated to use a database, it's up to you.
 */
public function canOverride()
{    
    // Remember to add 'use Config;' to your class!
    $options = Config::get('permissions');

    if (isset($options['override'])
        && isset($options['override']['require_all'])
        && isset($options['override']['permissions']))
    {
        return Auth::user()->hasPermissions(
            $options['override']['permissions'], 
            $options['override']['require_all']
        );
    }

    return false;
}
...

Now lets use this in our filter method.

...
public function filter(Route $route)
{
    $this->filters = $route->beforeFilters();
    $permissions = $this->filters[$this->getFilterName()];

    // Note we now call '$this->canOverride()' below.
    if (!$this->canOverride() && !$this->hasPermissions($permissions))
    {
        return App::abort(401);
    }
}
...

Now, any users that have the admin role will be allowed access to all routes running this filter, regardless of the admin role being present when the filter is run or not.

Final Notes

Now we have a much cleaner and easier to use filter for our permissions.

We could expand on this further by creating a service provider to inject the config into the class instead of calling Config::get('permissions'); within the class. We could also add the filters them selves to the service provider instead of keeping them in the filters.php file.

As usual, feel free to let me know what you think of the article and if you find any bugs or have any questions.

Resources

© 2017. All Rights Reserved.