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.