RBAC
RBAC has no schema of its own — no roles/permissions tables.
You supply a resolver closure; RBAC only does the check.
Wiring it
final class RBAC extends \Monad\Clarity\Middlewares\RBAC
{
private const ROLE_PERMISSIONS = [
'admin' => ['users.view', 'users.create', 'users.delete'],
'member' => ['users.view'],
];
public function __construct()
{
parent::__construct(
permissionsForUser: static function (string $userId): array {
$role = DB::run('SELECT role FROM users WHERE id = ? LIMIT 1', [$userId])->fetch()['role'] ?? null;
return self::ROLE_PERMISSIONS[$role] ?? [];
},
permissionsForRole: static fn (string $role): array => self::ROLE_PERMISSIONS[$role] ?? [],
);
}
}
Direct checks — callable from anywhere
$rbac->can(string $userId, string $permission): bool
$rbac->canAny(string $userId, array $permissions): bool // at least one
$rbac->canAll(string $userId, array $permissions): bool // every one (empty list => true)
$rbac->roleHasPermission(string $role, string $permission): bool // needs permissionsForRole
if (!$rbac->can($currentUserId, 'users.delete')) {
return Response::json(['error' => 'Forbidden'], 403);
}
Route guards
guard() returns a closure matching the middleware signature — register it directly,
no class-string zero-argument constructor needed since it's already callable:
Route::delete('/users/{id}', [UserController::class, 'destroy'])
->middleware($rbac->guard('users.delete', fn (Request $request) => $request->header('X-User-Id')));
// 403 if the resolved user id is null or lacks the permission; the route's action otherwise.
guard()'s second argument is entirely your responsibility — RBAC has no opinion on how
a request maps to an authenticated user (session cookie, bearer token, or anything else).
Next steps
- Authentication — resolving the authenticated user id that
guard()'s resolver typically reads from a session.