{"record":{"id":"73dedfd58928189b","repo":"instructure/canvas-lms","slug":"insufficient-permission-update-discussion-topic-participant","errorCode":null,"errorMessage":"insufficient permission","messagePattern":"insufficient permission","errorType":"exception","errorClass":"GraphQL::ExecutionError","httpStatus":null,"severity":"error","filePath":"app/graphql/mutations/update_discussion_topic_participant.rb","lineNumber":35,"sourceCode":"# You should have received a copy of the GNU Affero General Public License along\n# with this program. If not, see <http://www.gnu.org/licenses/>.\n#\n\nclass Mutations::UpdateDiscussionTopicParticipant < Mutations::BaseMutation\n  graphql_name \"UpdateDiscussionTopicParticipant\"\n\n  argument :discussion_topic_id, ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func(\"DiscussionEntry\")\n  argument :expanded, Boolean, required: false\n  argument :has_unread_pinned_entry, Boolean, required: false\n  argument :preferred_language, Types::PreferredLanguageType, required: false\n  argument :show_pinned_entries, Boolean, required: false\n  argument :sort_order, Types::DiscussionSortOrderType, required: false\n  argument :summary_enabled, Boolean, required: false\n  field :discussion_topic, Types::DiscussionType, null: false\n\n  def resolve(input:)\n    discussion_topic = DiscussionTopic.find(input[:discussion_topic_id])\n    raise GraphQL::ExecutionError, \"insufficient permission\" unless discussion_topic.grants_right?(current_user, session, :read)\n\n    discussion_topic.update_or_create_participant(current_user:, **input)\n    { discussion_topic: }\n  rescue ActiveRecord::RecordNotFound\n    raise GraphQL::ExecutionError, \"not found\"\n  end\nend\n","sourceCodeStart":17,"sourceCodeEnd":43,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/mutations/update_discussion_topic_participant.rb#L17-L43","documentation":"UpdateDiscussionTopicParticipant#resolve raises \"insufficient permission\" when the topic exists but does not grant :read to the current user; the trailing rescue separately turns RecordNotFound into \"not found\". Participants (read state, entries count, summary prefs) can only be updated on readable topics.","triggerScenarios":"updateDiscussionTopicParticipant with discussion_topic_id of an unpublished/delayed topic for a student, a topic in a course the token user is not enrolled in, or with a nil/invalid session causing grants_right? to fail.","commonSituations":"Background unread-count sync running before enrollment activation; mobile app tokens tied to a stale user; observers restricted by section limitation; announcements in read-only public courses.","solutions":["Pre-check discussion_topic.grants_right?(current_user, session, :read) before the mutation.","Ensure the API token/session belongs to an enrolled, active user.","Delay participant updates until the topic becomes available to that role.","Fix section-limited enrollments if the user should have access."],"exampleFix":"// before\nupdateDiscussionTopicParticipant(input: {discussionTopicId: t, read: true})\n// after\nif (topicPermissions.read) {\n  updateDiscussionTopicParticipant(input: {discussionTopicId: t, read: true})\n}","handlingStrategy":"type-guard","validationCode":"query { discussionTopic(id: $id) { permissions { read } } }\nif (!data.discussionTopic?.permissions?.read) return;","typeGuard":"function canReadTopic(t) { return t?.permissions?.read === true; }","tryCatchPattern":"try {\n  await updateDiscussionTopicParticipant(...);\n} catch (e) {\n  if (['not found', 'insufficient permission'].includes(e.message)) stopParticipantSync();\n  else throw e;\n}","preventionTips":["Check topic availability dates before participant updates","Use the enrolled user's own token for read-state sync jobs","Treat not-found and permission errors identically to avoid leaking topic existence","Re-verify enrollment after course section changes"],"tags":["graphql","permissions","discussions"],"backgroundTag":"insufficient-permissions","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"}