Using SystemRoles in Your App
Protecting Routes with Middleware
On this page
SystemRoles ships one middleware, super_admin, which protects its own admin area. To protect your routes, add one of the middleware below. Middleware is the best first line of defence: a route that forgets a check in its controller is still covered.
super_admin (included)#
Registered automatically as the super_admin alias. Lets the request through only when the logged-in user has the super admin role.
Route::middleware(['auth', 'super_admin'])->group(function () {
Route::get('/system/health', HealthController::class);
});| Situation | Browser request | JSON request |
|---|---|---|
| Not logged in | Redirect to login_ with an error message |
401 {"message":"Unauthenticated. |
| Logged in, not super admin | Redirect to dashboard_ with "Access Denied" |
403 {"message":"Access denied. |
| Super admin | Continues | Continues |
permission:<name> (recipe)#
Checks one or more permissions. The user passes if their role has any of them.
namespace App\Http\Middleware;
use BabeRuka\SystemRoles\Services\SystemRolesService;
use Closure;
use Illuminate\Http\Request;
class HasPermission
{
public function handle(Request $request, Closure $next, string ...$permissions)
{
$user = $request->user();
if (! $user) {
return $request->expectsJson()
? response()->json(['message' => 'Unauthenticated.'], 401)
: redirect()->guest(route('login'));
}
$roleId = SystemRolesService::roleIdForUser($user->getKey());
foreach ($permissions as $permission) {
if (SystemRolesService::roleHas($roleId, $permission)) {
return $next($request);
}
}
abort(403, 'You do not have permission to do this.');
}
}Register it.
Laravel 11 and 12 (bootstrap/app.php):
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'permission' => \App\Http\Middleware\HasPermission::class,
'role' => \App\Http\Middleware\HasRole::class,
]);
})Laravel 9 and 10 (app/Http/Kernel.php, $middlewareAliases or $routeMiddleware):
'permission' => \App\Http\Middleware\HasPermission::class,
'role' => \App\Http\Middleware\HasRole::class,Use it:
Route::middleware(['auth', 'permission:invoice.view'])->group(function () {
Route::get('/invoices', [InvoiceController::class, 'index']);
Route::post('/invoices/{invoice}/approve', [InvoiceController::class, 'approve'])
->middleware('permission:invoice.approve');
});
// Any of several permissions
Route::get('/reports', ReportController::class)->middleware('permission:report.view,report.export');role:<guard> (recipe)#
Checks the role itself, by guard name. Useful for coarse areas, such as a staff area.
namespace App\Http\Middleware;
use BabeRuka\SystemRoles\Models\SystemRoles;
use BabeRuka\SystemRoles\Services\SystemRolesService;
use Closure;
use Illuminate\Http\Request;
class HasRole
{
public function handle(Request $request, Closure $next, string ...$roles)
{
$roleId = SystemRolesService::roleIdForUser(optional($request->user())->getKey());
if (SystemRolesService::isSuperAdminRole($roleId)) {
return $next($request);
}
$guard = SystemRoles::whereKey($roleId)->value('role_guard_name');
abort_unless($guard && in_array($guard, $roles, true), 403);
return $next($request);
}
}Route::middleware(['auth', 'role:admin,manager'])->prefix('staff')->group(function () {
// ...
});Prefer permission: over role: where you can. Permissions can be changed on screen. Role lists in route files need a code change.
Deriving the permission from the route#
For large apps, instead of writing a permission on each route, build it from the route name. A route named invoices.update checks the permission invoices.update:
class RoutePermission
{
public function handle(Request $request, Closure $next)
{
$name = $request->route()?->getName();
if (! $name) {
abort(403, 'Unnamed routes are not allowed in this area.');
}
$roleId = SystemRolesService::roleIdForUser(optional($request->user())->getKey());
abort_unless(SystemRolesService::roleHas($roleId, $name), 403);
return $next($request);
}
}Route::middleware(['auth', 'route.permission'])->prefix('admin')->name('admin.')->group(function () {
Route::resource('invoices', InvoiceController::class); // admin.invoices.index, .store, .update, ...
});Seed one permission per route name (see Permissions → Doing it in code).
Roll out safely: audit mode#
Switching on checks across an existing app can lock people out. Add an "audit" switch so the middleware logs what it would deny before it denies anything:
if (! $allowed) {
logger()->channel('daily')->info('permission would-deny', [
'user' => $request->user()?->getKey(),
'role' => $roleId,
'perm' => $permission,
'path' => $request->path(),
]);
if (config('app.permissions_enforce', false)) {
abort(403);
}
}Run in audit mode for a few days, fix the matrix using the log, then turn on enforcement.
Tips#
- Always put
authbefore these middleware. - Apply them to route groups, not globally, so login, registration and public pages stay open.
- Check on the server even if you hide buttons in Blade. Hidden buttons are not security.
- Write a feature test per role for your most sensitive routes.