passbolt/passbolt_api · error · ValidationException

Cannot delete group user.

Error message

Cannot delete group user.

What it means

GroupsUsersDeleteService::assertAtLeastOneGroupManager throws this ValidationException when deleting a group user would leave the group with zero group managers (i.e. the deleted user is the only is_admin member). The real reason is attached as a validation error on the entity's is_admin field.

Solutions

  1. Promote another group member to admin (PUT /groups/users/{id} with is_admin=true) before deleting this one.
  2. Delete a different member first if another manager exists.
  3. Check GET /groups/{id} memberships for other is_admin users before calling delete.
  4. If intending to dissolve the group, delete the whole group instead (DELETE /groups/{id}).

Example fix

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

Strategy: validation

Validate before calling

const managers = group.memberships.filter(m => m.is_admin);
if (managers.length === 1 && managers[0].id === targetId) throw new Error('last group manager');

Try / catch

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

Prevention

When it happens

Trigger: DELETE /groups/users/{id} where the target group user is the sole admin of the group ($groupManagersCount === 1 and the user is that admin).

Common situations: Removing the last group manager before another member is promoted, bulk-cleanups of group memberships, deleting memberships via API without checking is_admin distribution.

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/f0bba5e41e283a3f. Report an issue: GitHub.

Appendix: source

Thrown at src/Service/GroupsUsers/GroupsUsersDeleteService.php:114

     * Assert that the group to remove the group user in will have at least one manager after removing the group user.
     *
     * @param \App\Model\Entity\GroupsUser $groupUser The group user to check the group for
     * @return void
     * @throws \App\Error\Exception\ValidationException Cannot delete the last group manager.
     */
    private function assertAtLeastOneGroupManager(GroupsUser $groupUser): void
    {
        if (!$groupUser->is_admin) {
            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 delete group user.', $groupUser);
        }
    }

    /**
     * Delete the secrets for the resources the user lost access after being removed from the group.
     *
     * @param \App\Model\Entity\GroupsUser $groupUser The group user to delete.
     * @return array<\App\Model\Entity\Secret>
     */
    private function deleteLostAccessAssociatedSecrets(GroupsUser $groupUser): array
    {
        $lostAccessSecretsConditions = [
            'user_id' => $groupUser->user_id,
            'resource_id IN' => $this->findLostAccessResourcesIdsQuery($groupUser),
        ];
        /** @var array<\App\Model\Entity\Secret> $lostAccessSecrets */
        $lostAccessSecrets = $this->secretsTable->find()
            ->select(['id', 'resource_id', 'user_id'])

View on GitHub (pinned to 31c1bbc10f)