{"record":{"id":"3ba4ae44904d05c1","repo":"RocketChat/Rocket.Chat","slug":"error-role-protected","errorCode":"error-role-protected","errorMessage":"Role is protected","messagePattern":"Role is protected","errorType":"exception","errorClass":"MeteorError","httpStatus":null,"severity":"error","filePath":"apps/meteor/ee/server/lib/roles/updateRole.ts","lineNumber":24,"sourceCode":"import { notifyOnRoleChangedById } from '../../../../server/lib/notifyListener';\n\ntype UpdateRoleOptions = {\n\tbroadcastUpdate?: boolean;\n};\n\nexport const updateRole = async (\n\troleId: IRole['_id'],\n\troleData: Omit<IRole, '_id' | '_updatedAt'>,\n\toptions: UpdateRoleOptions = {},\n): Promise<IRole> => {\n\tconst role = await Roles.findOneById(roleId);\n\n\tif (!role) {\n\t\tthrow new MeteorError('error-invalid-roleId', 'This role does not exist');\n\t}\n\n\tif (role.protected && ((roleData.name && roleData.name !== role.name) || (roleData.scope && roleData.scope !== role.scope))) {\n\t\tthrow new MeteorError('error-role-protected', 'Role is protected');\n\t}\n\n\tif (roleData.name) {\n\t\tconst otherRole = await Roles.findOneByName(roleData.name, { projection: { _id: 1 } });\n\t\tif (otherRole && otherRole._id !== role._id) {\n\t\t\tthrow new MeteorError('error-duplicate-role-names-not-allowed', 'Role name already exists');\n\t\t}\n\t} else {\n\t\troleData.name = role.name;\n\t}\n\n\tif (roleData.scope) {\n\t\tif (!isValidRoleScope(roleData.scope)) {\n\t\t\tthrow new MeteorError('error-invalid-scope', 'Invalid scope');\n\t\t}\n\t} else {\n\t\troleData.scope = role.scope;\n\t}","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/ee/server/lib/roles/updateRole.ts#L6-L42","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait updateRole(adminRoleId, { name: 'administrator', scope: 'Users', description: d }); // throws error-role-protected\n\n// after\nawait updateRole(adminRoleId, { description: d }); // name/scope are immutable on protected roles","handlingStrategy":"validation","validationCode":"const role = await Roles.findOneById(roleId);\nconst changesIdentity = role?.protected && ((roleData.name && roleData.name !== role.name) || (roleData.scope && roleData.scope !== role.scope));\nif (changesIdentity) {\n\t// strip name/scope from the payload or reject the edit before calling updateRole\n}","typeGuard":"const isProtectedRole = (role: Pick<IRole, 'protected'> | null | undefined): boolean => Boolean(role?.protected);","tryCatchPattern":"try {\n\tawait updateRole(roleId, roleData);\n} catch (e: any) {\n\tif (e?.error === 'error-role-protected') { surface('Name and scope of protected roles cannot be changed'); return; }\n\tthrow e;\n}","preventionTips":["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."],"tags":["roles","permissions","protected-resource","immutability","enterprise"],"backgroundTag":"protected-resource-modification","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}