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.

PHP
Route::middleware(['auth', 'super_admin'])->group(function () {
    Route::get('/system/health', HealthController::class);
});
Situation Browser request JSON request
Not logged in Redirect to login_url with an error message 401 {"message":"Unauthenticated."}
Logged in, not super admin Redirect to dashboard_url 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.

PHP
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):

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):

PHP
'permission' => \App\Http\Middleware\HasPermission::class,
'role'       => \App\Http\Middleware\HasRole::class,

Use it:

PHP
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.

PHP
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);
    }
}
PHP
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:

PHP
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);
    }
}
PHP
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:

PHP
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 auth before 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.