passbolt/passbolt_api · error · ValidationException

Cannot update group user.

Error message

Cannot update group user.

What it means

GroupsUsersUpdateService::assertAtLeastOneGroupManager throws this ValidationException when updating a group user (e.g. demoting is_admin to false) would leave the group without any group manager. The detailed message is set on the entity's is_admin field as at_least_one_group_manager.

Solutions

  1. Promote another member to is_admin=true first, then demote this user.
  2. Omit is_admin from the update payload if the demotion is unintended.
  3. Verify group manager count via GET /groups/{id} before patching.
  4. If the goal is ownership transfer, do it in one planned sequence: promote, then demote.

Example fix

// before
await passbolt.updateGroupUser(soleManagerId, { is_admin: false });
// after
await passbolt.updateGroupUser(otherMemberId, { is_admin: true });
await passbolt.updateGroupUser(soleManagerId, { is_admin: false });
Defensive patterns

Strategy: validation

Validate before calling

const managers = group.memberships.filter(m => m.is_admin);
if (payload.is_admin === false && managers.length === 1 && managers[0].id === id) return reject('last group manager');

Try / catch

try { await updateGroupUser(id, payload); } catch (e) { if (e.body?.errors?.is_admin?.at_least_one_group_manager) {...} }

Prevention

When it happens

Trigger: PUT /groups/users/{id} with data setting is_admin=false on the only group manager of the group (admin count after change would be 0).

Common situations: Demoting the sole manager to a normal member, scripts syncing group roles that flip is_admin without checking counts, UI edits in groups with a single manager.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/69bafc86df86d856. Report an issue: GitHub.

Appendix: source

Thrown at src/Service/GroupsUsers/GroupsUsersUpdateService.php:84

     * @throws \App\Error\Exception\ValidationException Cannot delete the last group manager.
     */
    private function assertAtLeastOneGroupManager(GroupsUser $groupUser, array $data): void
    {
        $isAdmin = Hash::get($data, 'is_admin');
        if ($isAdmin || is_null($isAdmin)) {
            return;
        }
        if ($groupUser->is_admin === $isAdmin) {
            return;
        }

        $groupManagersCount = $this->groupsUsersTable->findByGroupIdAndIsAdmin($groupUser->group_id, true)
            ->all()
            ->count();

        if ($groupManagersCount === 1) {
            $groupUser->setError('is_admin', ['at_least_one_group_manager' => 'Cannot delete the last group manager.']);
            throw new ValidationException('Cannot update group user.', $groupUser);
        }
    }

    /**
     * Patch the group user with the data to update.
     *
     * @param \App\Utility\UserAccessControl $uac The user at the origin of the operation
     * @param \App\Model\Entity\GroupsUser $groupUser The group user to update.
     * @param array $data The date to use to patch the group user
     * @return \App\Model\Entity\GroupsUser
     */
    private function patchGroupUserEntity(UserAccessControl $uac, GroupsUser $groupUser, array $data): GroupsUser
    {
        $patchEntityOptions = ['accessibleFields' => ['is_admin' => true, 'modified_by' => true]];
        $groupUser = $this->groupsUsersTable->patchEntity($groupUser, $data, $patchEntityOptions);
        if ($groupUser->hasErrors()) {
            $this->handleValidationErrors($groupUser);
        }

View on GitHub (pinned to 31c1bbc10f)