n8n-io/n8n · error · ForbiddenError

Admin cannot reset password of global owner

Error message

Admin cannot reset password of global owner

What it means

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.

Source

Thrown at packages/cli/src/controllers/users.controller.ts:166

		});
	}

	@Get('/:id/password-reset-link')
	@GlobalScope('user:resetPassword')
	async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {
		const user = await this.userRepository.findOneOrFail({
			where: { id: req.params.id },
			relations: ['role'],
		});
		if (!user) {
			throw new NotFoundError('User not found');
		}

		if (
			req.user.role.slug === GLOBAL_ADMIN_ROLE.slug &&
			user.role.slug === GLOBAL_OWNER_ROLE.slug
		) {
			throw new ForbiddenError('Admin cannot reset password of global owner');
		}

		const link = this.authService.generatePasswordResetUrl(user);
		return { link };
	}

	@Post('/:id/invite-link')
	@GlobalScope('user:generateInviteLink')
	async generateInviteLink(req: AuthenticatedRequest<{ id: string }, {}, {}, {}>, _res: Response) {
		const inviterId = req.user.id;
		const inviteeId = req.params.id;

		const targetUser = await this.userRepository.findOne({ where: { id: inviteeId } });

		if (!targetUser) {
			throw new NotFoundError('User to generate invite link for not found');
		}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Have the global owner reset their own password through the self-service flow instead.
  2. Filter the owner out of the target list before issuing bulk reset calls.
  3. If a true owner password reset is required, follow the documented owner-recovery/runbook rather than the admin endpoint.
Defensive patterns

Strategy: validation

Validate before calling

function canResetPassword(requesterRoleSlug: string, targetRoleSlug: string) {
  return !(requesterRoleSlug === 'global:admin' && targetRoleSlug === 'global:owner');
}
if (!canResetPassword(reqUser.role.slug, target.role.slug)) {
  throw new Error('Owner password must be reset through the owner-recovery flow');
}

Type guard

const isOwner = (slug: string) => slug === 'global:owner';
const isAdmin = (slug: string) => slug === 'global:admin';

Try / catch

try { await fetch(`/rest/users/${id}/password-reset-link`); }
catch (e) { if (e.statusCode === 403) { /* escalate to owner-recovery */ } else throw e; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/c4a25b84a0d56a4a. Report an issue: GitHub.