{"record":{"id":"4d67d89cf59d52e8","repo":"instructure/canvas-lms","slug":"insufficient-permission-move-outcome-links","errorCode":null,"errorMessage":"Insufficient permission","messagePattern":"Insufficient permission","errorType":"exception","errorClass":"GraphQL::ExecutionError","httpStatus":null,"severity":"error","filePath":"app/graphql/mutations/move_outcome_links.rb","lineNumber":68,"sourceCode":"\n    {\n      errors:,\n      moved_outcome_links: ContentTag.where(id: outcome_links.pluck(:id))\n    }\n  end\n\n  def self.moved_outcome_link_ids_log_entry(_ids, ctx)\n    ctx[:group]\n  end\n\n  private\n\n  def get_group!(input)\n    LearningOutcomeGroup.active.find_by(id: input[:group_id]).tap do |group|\n      raise GraphQL::ExecutionError, I18n.t(\"Group not found\") unless group\n\n      if group.context\n        raise GraphQL::ExecutionError, I18n.t(\"Insufficient permission\") unless\n          group.context.grants_right?(current_user, session, :manage_outcomes)\n      else\n        raise GraphQL::ExecutionError, I18n.t(\"Insufficient permission\") unless\n          Account.site_admin.grants_right?(current_user, session, :manage_global_outcomes)\n      end\n    end\n  end\n\n  def get_outcome_links(input, context)\n    ids = input[:outcome_link_ids].map(&:to_i).uniq\n    links = if context\n              ContentTag.active.learning_outcome_links.where(\n                context:,\n                id: ids\n              )\n            else\n              ContentTag.active.learning_outcome_links.where(id: ids, context_type: \"LearningOutcomeGroup\")\n            end","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/mutations/move_outcome_links.rb#L50-L86","documentation":"In MoveOutcomeLinks#get_group!, when the target outcome group has a context (Course or Account), the current user must hold :manage_outcomes there; otherwise this ExecutionError is raised. It is the authorization gate for moving outcome links within a contextual group.","triggerScenarios":"A user without manage_outcomes rights on the group's Course/Account calling moveOutcomeLinks with that group's group_id.","commonSituations":"Students or teachers (without outcome-management rights) attempting moves; users whose enrollment changed; API tokens of a different user than the UI session expects.","solutions":["Perform the move as a user with manage_outcomes permission on the group's context (typically an admin or outcome manager)","Grant the user the manage_outcomes role in the Course/Account if appropriate","Check which enrollment/role the current user actually has in the target context"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// check permissions client-side before attempting the move\nconst perms = await client.query({ query: CONTEXT_PERMS, variables: { contextId, contextType } });\nif (!perms.data?.context?.permissions?.manageOutcomes) {\n  throw new Error('User lacks manage_outcomes on the group context');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await client.mutate({ mutation: MOVE_OUTCOME_LINKS, variables: { input } });\n} catch (e) {\n  if (e.message.includes('Insufficient permission')) {\n    // hide move actions and show an explanatory message\n  }\n}","preventionTips":["Query the context's permissions before rendering move controls","Re-check permissions when a user's enrollment changes","Ensure API tokens belong to the intended privileged user"],"tags":["graphql","authorization","permissions","outcomes"],"backgroundTag":"permission-denied","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"}