{"record":{"id":"63e89011bf757989","repo":"passbolt/passbolt_api","slug":"the-transfer-is-not-authorized-63e890","errorCode":null,"errorMessage":"The transfer is not authorized","messagePattern":"The transfer is not authorized","errorType":"exception","errorClass":"Cake\\Http\\Exception\\BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Controller/Users/UsersDeleteController.php","lineNumber":351,"sourceCode":"        $contentIdBlockingDelete = $this->Permissions\n            ->findSharedAcosByAroIsSoleOwner(PermissionsTable::RESOURCE_ACO, $user->id, ['checkGroupsUsers' => true])\n            ->all()\n            ->extract('aco_foreign_key')\n            ->toArray();\n\n        if (Configure::read('passbolt.plugins.folders.enabled')) {\n            $foldersIdsBlockingDelete = $this->Permissions\n                ->findSharedAcosByAroIsSoleOwner(PermissionsTable::FOLDER_ACO, $user->id, ['checkGroupsUsers' => true])\n                ->all()\n                ->extract('aco_foreign_key')\n                ->toArray();\n            $contentIdBlockingDelete = array_merge($contentIdBlockingDelete, $foldersIdsBlockingDelete);\n        }\n        sort($contentIdBlockingDelete);\n\n        // If all the resources that are requiring a change are not satisfied, throw an exception.\n        if ($contentIdsToUpdate != $contentIdBlockingDelete) {\n            throw new BadRequestException('The transfer is not authorized');\n        }\n\n        // Update all the permissions given as parameter as long as they are\n        // relative to a content which blocked the delete process.\n        $this->Permissions->updateAll([\n            'type' => Permission::OWNER,\n        ], [\n            'id IN' => $permissionsIdsToUpdate,\n            'aco_foreign_key IN' => $contentIdBlockingDelete,\n        ]);\n    }\n\n    /**\n     * Send email notification\n     *\n     * @param \\App\\Model\\Entity\\User $deletedUser entity\n     * @param array $groupIds list of Group entity user was member of\n     * @return void","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/src/Controller/Users/UsersDeleteController.php#L333-L369","documentation":"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.","triggerScenarios":"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.","commonSituations":"Partial offboarding scripts that handle resources but forget folders (folders plugin enabled); retries after a partially failed transfer; cached dry-run results.","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"],"exampleFix":"// before\ntransferOwners(onlyResourcePermissions);\n// after\nconst body = (await dryRunDelete(userId)).body;\nconst owners = [...body.errors.resources.sole_owner, ...body.errors.folders.sole_owner]\n  .flatMap(x => x.permissions.filter(p => p.type === 15));\ntransferOwners(owners);","handlingStrategy":"retry","validationCode":"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)));\nconst given = new Set(owners.map(o => o.aco_foreign_key));\nif (blocking.size !== given.size || ![...blocking].every(c => given.has(c))) throw new Error('transfer must exactly cover blocking content');","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["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"],"tags":["bad-request","permissions","ownership-transfer"],"backgroundTag":"invalid-state-transition","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}