RocketChat/Rocket.Chat · error · MeteorError
error-role-protected
error-role-protected
Error message
Role is protected
What it means
updateRole refuses identity changes on protected roles: when role.protected is true and the update carries a different name (roleData.name !== role.name) or a different scope (roleData.scope !== role.scope), it throws MeteorError('error-role-protected', 'Role is protected'). Sending the same name/scope back is safe, and description/mandatory2fa remain editable - the guard only fires on an actual change to a protected role's name or scope.
Solutions
- Send only the fields you intend to change; for protected roles limit updates to description and mandatory2fa.
- If you need a different name or scope, create a new custom role and migrate assignments to it instead of mutating the protected one.
- Trim/normalize name and scope fields in payloads so unchanged values compare equal and do not look like edit attempts.
Example fix
// before
await updateRole(adminRoleId, { name: 'administrator', scope: 'Users', description: d }); // throws error-role-protected
// after
await updateRole(adminRoleId, { description: d }); // name/scope are immutable on protected roles Defensive patterns
Strategy: validation
Validate before calling
const role = await Roles.findOneById(roleId);
const changesIdentity = role?.protected && ((roleData.name && roleData.name !== role.name) || (roleData.scope && roleData.scope !== role.scope));
if (changesIdentity) {
// strip name/scope from the payload or reject the edit before calling updateRole
} Type guard
const isProtectedRole = (role: Pick<IRole, 'protected'> | null | undefined): boolean => Boolean(role?.protected);
Try / catch
try {
await updateRole(roleId, roleData);
} catch (e: any) {
if (e?.error === 'error-role-protected') { surface('Name and scope of protected roles cannot be changed'); return; }
throw e;
} Prevention
- Send only changed fields on role updates, not the whole role object.
- Mark name/scope inputs read-only in UIs when the loaded role has protected: true.
- Want a different name or scope? Create a new custom role and migrate assignments.
When it happens
Trigger: Renaming a protected built-in role (e.g. 'admin') or flipping its scope between 'Users' and 'Subscriptions'. Full-object PUTs from UI forms only trip the guard when the name/scope actually differs - e.g. a trimmed or whitespace-mangled value.
Common situations: Forms that send the entire role object back with subtly altered name/scope; scripts trying to rename built-ins to free the name for a custom role; attempts to convert a subscription-scoped built-in to user scope.
Related errors
- error-action-not-allowed
- error-duplicate-role-names-not-allowed
- error-duplicate-role-names-not-allowed
- error-invalid-roleId
- error-invalid-scope
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/3ba4ae44904d05c1.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/ee/server/lib/roles/updateRole.ts:24
import { notifyOnRoleChangedById } from '../../../../server/lib/notifyListener';
type UpdateRoleOptions = {
broadcastUpdate?: boolean;
};
export const updateRole = async (
roleId: IRole['_id'],
roleData: Omit<IRole, '_id' | '_updatedAt'>,
options: UpdateRoleOptions = {},
): Promise<IRole> => {
const role = await Roles.findOneById(roleId);
if (!role) {
throw new MeteorError('error-invalid-roleId', 'This role does not exist');
}
if (role.protected && ((roleData.name && roleData.name !== role.name) || (roleData.scope && roleData.scope !== role.scope))) {
throw new MeteorError('error-role-protected', 'Role is protected');
}
if (roleData.name) {
const otherRole = await Roles.findOneByName(roleData.name, { projection: { _id: 1 } });
if (otherRole && otherRole._id !== role._id) {
throw new MeteorError('error-duplicate-role-names-not-allowed', 'Role name already exists');
}
} else {
roleData.name = role.name;
}
if (roleData.scope) {
if (!isValidRoleScope(roleData.scope)) {
throw new MeteorError('error-invalid-scope', 'Invalid scope');
}
} else {
roleData.scope = role.scope;
}View on GitHub (pinned to b2c16d5842)