{"record":{"id":"c4a25b84a0d56a4a","repo":"n8n-io/n8n","slug":"admin-cannot-reset-password-of-global-owner","errorCode":null,"errorMessage":"Admin cannot reset password of global owner","messagePattern":"Admin cannot reset password of global owner","errorType":"exception","errorClass":"ForbiddenError","httpStatus":403,"severity":"error","filePath":"packages/cli/src/controllers/users.controller.ts","lineNumber":166,"sourceCode":"\t\t});\n\t}\n\n\t@Get('/:id/password-reset-link')\n\t@GlobalScope('user:resetPassword')\n\tasync getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {\n\t\tconst user = await this.userRepository.findOneOrFail({\n\t\t\twhere: { id: req.params.id },\n\t\t\trelations: ['role'],\n\t\t});\n\t\tif (!user) {\n\t\t\tthrow new NotFoundError('User not found');\n\t\t}\n\n\t\tif (\n\t\t\treq.user.role.slug === GLOBAL_ADMIN_ROLE.slug &&\n\t\t\tuser.role.slug === GLOBAL_OWNER_ROLE.slug\n\t\t) {\n\t\t\tthrow new ForbiddenError('Admin cannot reset password of global owner');\n\t\t}\n\n\t\tconst link = this.authService.generatePasswordResetUrl(user);\n\t\treturn { link };\n\t}\n\n\t@Post('/:id/invite-link')\n\t@GlobalScope('user:generateInviteLink')\n\tasync generateInviteLink(req: AuthenticatedRequest<{ id: string }, {}, {}, {}>, _res: Response) {\n\t\tconst inviterId = req.user.id;\n\t\tconst inviteeId = req.params.id;\n\n\t\tconst targetUser = await this.userRepository.findOne({ where: { id: inviteeId } });\n\n\t\tif (!targetUser) {\n\t\t\tthrow new NotFoundError('User to generate invite link for not found');\n\t\t}\n","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/cli/src/controllers/users.controller.ts#L148-L184","documentation":"Returned by GET /users/:id/password-reset-link when the requesting user holds the global admin role (GLOBAL_ADMIN_ROLE.slug) but the target user is the instance/global owner (GLOBAL_OWNER_ROLE.slug). This is a deliberate authorization guard: admins may reset passwords for anyone except the highest-privilege owner. HTTP 403.","triggerScenarios":"A global admin (not the owner) calls GET /users/<owner-id>/password-reset-link. The role-slug comparison matches GLOBAL_ADMIN_ROLE for the requester and GLOBAL_OWNER_ROLE for the target.","commonSituations":"Admin console listing where the owner is visually indistinguishable from other admins; scripted bulk reset that iterates all user ids including the owner; RBAC misconfiguration promoting someone to admin who then tries owner operations.","solutions":["Have the global owner reset their own password through the self-service flow instead.","Filter the owner out of the target list before issuing bulk reset calls.","If a true owner password reset is required, follow the documented owner-recovery/runbook rather than the admin endpoint."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"function canResetPassword(requesterRoleSlug: string, targetRoleSlug: string) {\n  return !(requesterRoleSlug === 'global:admin' && targetRoleSlug === 'global:owner');\n}\nif (!canResetPassword(reqUser.role.slug, target.role.slug)) {\n  throw new Error('Owner password must be reset through the owner-recovery flow');\n}","typeGuard":"const isOwner = (slug: string) => slug === 'global:owner';\nconst isAdmin = (slug: string) => slug === 'global:admin';","tryCatchPattern":"try { await fetch(`/rest/users/${id}/password-reset-link`); }\ncatch (e) { if (e.statusCode === 403) { /* escalate to owner-recovery */ } else throw e; }","preventionTips":["Filter global:owner out of admin-driven reset lists.","Surface role badges in the UI so admins see owner rows distinctly.","Document the owner-password recovery runbook separately."],"tags":["users","password-reset","rbac","forbidden","authorization","owner","admin"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}