passbolt/passbolt_api · error · Cake\Http\Exception\BadRequestException
The transfer is not authorized
Error message
The transfer is not authorized
What it means
The server verifies that the permissions submitted for ownership transfer exactly match the set of resources/folders that blocked the delete (compared as sorted aco_foreign_key sets). Any mismatch — missing blocking content, extra content, or stale data — throws BadRequestException 'The transfer is not authorized' (note: no trailing period). Deletion proceeds only when the transfer fully covers the blockers.
Solutions
- Re-run the dry-run delete and rebuild the transfer from the fresh errors.resources/errors.folders listings
- Include exactly one permission entry per blocking aco_foreign_key — no more, no less
- Cover folders as well as resources when passbolt.plugins.folders is enabled
- After a failed attempt, refetch state instead of reusing the old payload
Example fix
// before transferOwners(onlyResourcePermissions); // after const body = (await dryRunDelete(userId)).body; const owners = [...body.errors.resources.sole_owner, ...body.errors.folders.sole_owner] .flatMap(x => x.permissions.filter(p => p.type === 15)); transferOwners(owners);
Defensive patterns
Strategy: retry
Validate before calling
const blocking = new Set([...dryRun.body.errors.resources.sole_owner, ...dryRun.body.errors.folders.sole_owner].flatMap(x => x.permissions.map(p => p.aco_foreign_key)));
const given = new Set(owners.map(o => o.aco_foreign_key));
if (blocking.size !== given.size || ![...blocking].every(c => given.has(c))) throw new Error('transfer must exactly cover blocking content'); Try / catch
try { await transferOwnersAndDelete(payload); } catch (e) { if (e.status === 400 && /transfer is not authorized/.test(e.message)) { const dryRun = await api.deleteUser(id, { dryRun: true }); return transferOwnersAndDelete(buildPayload(dryRun)); } throw e; } Prevention
- Include folders in the transfer when the folders plugin is enabled
- Rebuild the payload from a fresh dry-run on every retry
- Do not split resource and folder transfers across separate delete attempts
When it happens
Trigger: Delete transfer payload covering only some of the solely-owned resources/folders; resource ownership changed by another admin between dry-run and transfer; folders plugin state changed after the dry-run snapshot.
Common situations: Partial offboarding scripts that handle resources but forget folders (folders plugin enabled); retries after a partially failed transfer; cached dry-run results.
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
- The identifier should be a valid UUID.
- The permissions data array keys must be integers.
- The permissions data must be an array.
- You do not have the permission to change a personal tag…
- You do not have the permission to edit shared tags on this…
AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17).
Data as JSON: /api/errors/63e89011bf757989.
Report an issue: GitHub.
Appendix: source
Thrown at src/Controller/Users/UsersDeleteController.php:351
$contentIdBlockingDelete = $this->Permissions
->findSharedAcosByAroIsSoleOwner(PermissionsTable::RESOURCE_ACO, $user->id, ['checkGroupsUsers' => true])
->all()
->extract('aco_foreign_key')
->toArray();
if (Configure::read('passbolt.plugins.folders.enabled')) {
$foldersIdsBlockingDelete = $this->Permissions
->findSharedAcosByAroIsSoleOwner(PermissionsTable::FOLDER_ACO, $user->id, ['checkGroupsUsers' => true])
->all()
->extract('aco_foreign_key')
->toArray();
$contentIdBlockingDelete = array_merge($contentIdBlockingDelete, $foldersIdsBlockingDelete);
}
sort($contentIdBlockingDelete);
// If all the resources that are requiring a change are not satisfied, throw an exception.
if ($contentIdsToUpdate != $contentIdBlockingDelete) {
throw new BadRequestException('The transfer is not authorized');
}
// Update all the permissions given as parameter as long as they are
// relative to a content which blocked the delete process.
$this->Permissions->updateAll([
'type' => Permission::OWNER,
], [
'id IN' => $permissionsIdsToUpdate,
'aco_foreign_key IN' => $contentIdBlockingDelete,
]);
}
/**
* Send email notification
*
* @param \App\Model\Entity\User $deletedUser entity
* @param array $groupIds list of Group entity user was member of
* @return voidView on GitHub (pinned to 31c1bbc10f)