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.