{"record":{"id":"17b651e1a1b53457","repo":"instructure/canvas-lms","slug":"cannot-change-locked-status-on-granular-permission","errorCode":null,"errorMessage":"Cannot change locked status on granular permission","messagePattern":"Cannot change locked status on granular permission","errorType":"exception","errorClass":"BadPermissionSettingError","httpStatus":400,"severity":"error","filePath":"app/controllers/role_overrides_controller.rb","lineNumber":667,"sourceCode":"    grouped_permissions = Hash.new { |h, k| h[k] = [] }.with_indifferent_access\n    manageable_permissions.each do |permission_name, permission|\n      grouped_permissions[permission_name] << { name: permission_name, disable_locking: permission.key?(:group) }\n      if permission.key?(:group)\n        current_override = context.role_overrides.where(permission: permission_name, role_id: role.id).first\n        grouped_permissions[permission[:group]] << { name: permission_name, disable_locking: false, currently: current_override&.enabled }\n      end\n    end\n\n    RoleOverride.transaction do\n      permissions.each do |permission_or_group_name, permission_updates|\n        next if value_to_boolean(permission_updates[:readonly])\n\n        target_permissions = grouped_permissions[permission_or_group_name]\n        next if target_permissions.empty?\n\n        if permission_updates.key?(:locked)\n          if target_permissions.any? { |permission| permission[:disable_locking] }\n            raise BadPermissionSettingError, t(\"Cannot change locked status on granular permission\")\n          else\n            locked = value_to_boolean(permission_updates[:locked])\n          end\n        end\n\n        if permission_updates.key?(:enabled) && value_to_boolean(permission_updates[:explicit])\n          override = value_to_boolean(permission_updates[:enabled])\n        end\n\n        if permission_updates.key? :applies_to_self\n          applies_to_self = value_to_boolean(permission_updates[:applies_to_self])\n        end\n\n        if permission_updates.key? :applies_to_descendants\n          applies_to_descendants = value_to_boolean(permission_updates[:applies_to_descendants])\n        end\n\n        if applies_to_descendants == false && applies_to_self == false","sourceCodeStart":649,"sourceCodeEnd":685,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/controllers/role_overrides_controller.rb#L649-L685","documentation":"Canvas permissions can be granular (e.g. a group like :manage_courses splits into :manage_courses_add/:manage_courses_edit). For such granular permissions, locking the whole group's locked state is disallowed, so set_permissions_for raises BadPermissionSettingError when the :locked key is sent for a group whose members have disable_locking.","triggerScenarios":"Calling role update or add_role with permissions like {manage_courses: {locked: true}} where manage_courses is a permission group flagged with disable_locking on its member permissions.","commonSituations":"UI or SIS automation sending a single 'locked' flag for a grouped permission; scripts copied from non-granular permission updates; API clients unaware of the granular-permission refactor.","solutions":["Send the locked flag on each individual granular permission instead of the group name (e.g. manage_courses_add, manage_courses_edit).","Remove the :locked key from permission_updates for granular groups and only set it for lockable permissions.","Check Permission.all/permission metadata for disable_locking before sending the locked key."],"exampleFix":"// before\npermissions: {manage_courses: {locked: true, explicit: true}}\n// after\npermissions: {manage_courses_add: {locked: true, explicit: true}, manage_courses_edit: {locked: true, explicit: true}}","handlingStrategy":"validation","validationCode":"const granular = ['manage_courses','manage_students'] // check permission metadata\nif (updates[perm]?.locked !== undefined && granular.includes(perm)) throw new Error(`set locked on granular sub-permissions of ${perm} individually`)","typeGuard":null,"tryCatchPattern":"try { await api.updateRole(roleId, body) } catch (e) { if (e.message.includes('Cannot change locked status')) { /* fall back to per-sub-permission updates */ } else throw e }","preventionTips":["Consult Canvas permission metadata for disable_locking before sending :locked","Apply locked flags per granular sub-permission","Mirror what the Canvas permissions UI sends"],"tags":["permissions","api","validation"],"backgroundTag":"invalid-argument-value","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}