n8n-io/n8n · error · ForbiddenError

Admin cannot change role on global owner

Error message

Admin cannot change role on global owner

What it means

Returned by PATCH /users/:id/role when the requester's role.slug is GLOBAL_ADMIN_ROLE.slug AND the target's role.slug is GLOBAL_OWNER_ROLE.slug. Admins are not permitted to act on the global owner's role. Message is the NO_ADMIN_ON_OWNER constant. HTTP 403.

Source

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

			UsersController.ERROR_MESSAGES.CHANGE_ROLE;

		if (req.user.id === id) {
			throw new ForbiddenError(CANNOT_CHANGE_OWN_ROLE);
		}

		const targetUser = await this.userRepository.findOne({
			where: { id },
			relations: ['role'],
		});
		if (targetUser === null) {
			throw new NotFoundError(NO_USER);
		}

		if (
			req.user.role.slug === GLOBAL_ADMIN_ROLE.slug &&
			targetUser.role.slug === GLOBAL_OWNER_ROLE.slug
		) {
			throw new ForbiddenError(NO_ADMIN_ON_OWNER);
		}

		if (
			req.user.role.slug === GLOBAL_OWNER_ROLE.slug &&
			targetUser.role.slug === GLOBAL_OWNER_ROLE.slug
		) {
			throw new ForbiddenError(NO_OWNER_ON_OWNER);
		}

		await this.userService.changeUserRole(targetUser, payload);

		this.eventService.emit('user-changed-role', {
			userId: req.user.id,
			targetUserId: targetUser.id,
			targetUserNewRole: payload.newRoleName,
			publicApi: false,
		});

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Only the current global owner (or the provisioning system) may reassign the owner role; use that path.
  2. Filter the owner out of any admin-driven role-batch operation.
  3. Surface a UI hint that the owner role cannot be changed by admins.
Defensive patterns

Strategy: validation

Validate before calling

function canChangeRole(requesterSlug: string, targetSlug: string) {
  return !(requesterSlug === 'global:admin' && targetSlug === 'global:owner');
}
if (!canChangeRole(reqUser.role.slug, target.role.slug)) {
  throw new Error('Admins cannot change the owner role');
}

Type guard

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

Try / catch

try { await fetch(`/rest/users/${id}/role`, { method: 'PATCH', body }); }
catch (e) { if (e.statusCode === 403 && /change role on global owner/.test(e.message)) { /* route to owner */ } else throw e; }

Prevention

When it happens

Trigger: A global admin calls PATCH /users/<owner-id>/role; both role-slug comparisons match.

Common situations: Admin console where the owner is listed alongside admins; scripted role sync that includes the owner id; misconfiguration granting admin rights to a service account that then targets the owner.

Related errors


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