instructure/canvas-lms · error · BadPermissionSettingError

Cannot change locked status on granular permission

Error message

Cannot change locked status on granular permission

What it means

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.

Solutions

  1. Send the locked flag on each individual granular permission instead of the group name (e.g. manage_courses_add, manage_courses_edit).
  2. Remove the :locked key from permission_updates for granular groups and only set it for lockable permissions.
  3. Check Permission.all/permission metadata for disable_locking before sending the locked key.

Example fix

// before
permissions: {manage_courses: {locked: true, explicit: true}}
// after
permissions: {manage_courses_add: {locked: true, explicit: true}, manage_courses_edit: {locked: true, explicit: true}}
Defensive patterns

Strategy: validation

Validate before calling

const granular = ['manage_courses','manage_students'] // check permission metadata
if (updates[perm]?.locked !== undefined && granular.includes(perm)) throw new Error(`set locked on granular sub-permissions of ${perm} individually`)

Try / catch

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 }

Prevention

When it happens

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

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

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/17b651e1a1b53457. Report an issue: GitHub.

Appendix: source

Thrown at app/controllers/role_overrides_controller.rb:667

    grouped_permissions = Hash.new { |h, k| h[k] = [] }.with_indifferent_access
    manageable_permissions.each do |permission_name, permission|
      grouped_permissions[permission_name] << { name: permission_name, disable_locking: permission.key?(:group) }
      if permission.key?(:group)
        current_override = context.role_overrides.where(permission: permission_name, role_id: role.id).first
        grouped_permissions[permission[:group]] << { name: permission_name, disable_locking: false, currently: current_override&.enabled }
      end
    end

    RoleOverride.transaction do
      permissions.each do |permission_or_group_name, permission_updates|
        next if value_to_boolean(permission_updates[:readonly])

        target_permissions = grouped_permissions[permission_or_group_name]
        next if target_permissions.empty?

        if permission_updates.key?(:locked)
          if target_permissions.any? { |permission| permission[:disable_locking] }
            raise BadPermissionSettingError, t("Cannot change locked status on granular permission")
          else
            locked = value_to_boolean(permission_updates[:locked])
          end
        end

        if permission_updates.key?(:enabled) && value_to_boolean(permission_updates[:explicit])
          override = value_to_boolean(permission_updates[:enabled])
        end

        if permission_updates.key? :applies_to_self
          applies_to_self = value_to_boolean(permission_updates[:applies_to_self])
        end

        if permission_updates.key? :applies_to_descendants
          applies_to_descendants = value_to_boolean(permission_updates[:applies_to_descendants])
        end

        if applies_to_descendants == false && applies_to_self == false

View on GitHub (pinned to 1c9f0bb801)