BookStackApp/BookStack · error · UserUpdateException

errors.role_cannot_remove_only_admin

Error message

errors.role_cannot_remove_only_admin

What it means

UserRepo::setUserRoles() throws UserUpdateException when a role update would demote the last admin: demotingLastAdmin() detects that the target user currently holds the admin role, is the only member of it, and the new role array no longer contains it. This prevents locking the instance out of administration; the exception redirects back to the user's edit URL with errors.role_cannot_remove_only_admin.

Source

Thrown at app/Users/UserRepo.php:312

        $adminRole = Role::getSystemRole('admin');
        if ($adminRole->users()->count() > 1) {
            return false;
        }

        return true;
    }

    /**
     * Set the assigned user roles via an array of role IDs.
     *
     * @throws UserUpdateException
     */
    protected function setUserRoles(User $user, array $roles): void
    {
        $roles = array_filter(array_values($roles));

        if ($this->demotingLastAdmin($user, $roles)) {
            throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());
        }

        $user->roles()->sync($roles);
    }

    /**
     * Check if the given user is the last admin and their new roles no longer
     * contain the admin role.
     */
    protected function demotingLastAdmin(User $user, array $newRoles): bool
    {
        if ($this->isOnlyAdmin($user)) {
            $adminRole = Role::getSystemRole('admin');
            if (!in_array(strval($adminRole->id), $newRoles)) {
                return true;
            }
        }

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Promote a second user to Admin before demoting the last one
  2. Include the admin role id in the roles array for the sole admin, or skip that user in bulk updates
  3. Catch UserUpdateException and redirect with a friendly message instead of a raw error
  4. Check demotingLastAdmin/isOnlyAdmin logic before calling setUserRoles

Example fix

// before
$userRepo->setUserRoles($lastAdmin, [$editorRoleId]);
// after
if (!$userRepo->isOnlyAdmin($lastAdmin)) {
    $userRepo->setUserRoles($lastAdmin, [$editorRoleId]);
}
Defensive patterns

Strategy: validation

Validate before calling

// Verify the new role set keeps admin coverage
$adminRoleId = \BookStack\Access\Role::getSystemRole('admin')->id;
$willLoseAdmin = $user->hasRole($adminRoleId)
    && !in_array($adminRoleId, $newRoleIds)
    && \BookStack\Access\Role::getSystemRole('admin')->users()->count() === 1;
if ($willLoseAdmin) { return back()->withErrors('Cannot remove the only admin.'); }
$userRepo->setUserRoles($user, $newRoleIds);

Try / catch

try {
    $userRepo->setUserRoles($user, $roles);
} catch (\BookStack\Exceptions\UserUpdateException $e) {
    return redirect($user->getEditUrl())->with('error', $e->getMessage());
}

Prevention

When it happens

Trigger: updateWithoutActivity/createWithoutActivity called with a roles array that omits the admin role for the sole admin user; UI role checkboxes unchecked for the last admin.

Common situations: Admin removing their own admin role; bulk role sync via API that replaces role sets wholesale; scripts importing users with role lists that drop the admin grant.

Related errors


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/825878e1d2bc4852. Report an issue: GitHub.