instructure/canvas-lms · error · GraphQL::ExecutionError

You do not have permission to view this course.

Error message

You do not have permission to view this course.

What it means

discussions_scope in discussions_connection_interface.rb raises this GraphQL::ExecutionError when the current user is not allowed to view discussions on behalf of the requested scoped user. The permission check delegates to current_user.can_current_user_view_as_user(course, scoped_user), which fails unless the viewer is the scoped user or an admin permitted to 'view as' that user in the course. It is a masquerade-style authorization guard, not a general read-permission check.

Solutions

  1. Remove the user_id argument (or pass the current user's own id) so the scope targets the caller itself.
  2. Log in as, or use a token for, the user whose discussions you need.
  3. Use an account-level admin token with 'View as' permission over the target user.
  4. Check can_current_user_view_as_user(course, scoped_user) client-side before adding user_id to the query.
  5. If user_id was intentionally passed but the user is missing, note that a nil scoped_user already returns DiscussionTopic.none — only valid user_ids are permission-checked.

Example fix

// before
query { course(id: 1) { discussionsConnection(userId: 42) { nodes { id } } } }
// after
query { course(id: 1) { discussionsConnection { nodes { id } } } } // omit userId, or pass your own id
Defensive patterns

Strategy: validation

Validate before calling

// before sending the query
const canViewAs = (course, targetUserId, currentUserId, isAdmin) =>
  targetUserId === undefined || targetUserId === currentUserId || isAdmin
if (!canViewAs(course, variables.userId, currentUser.id, currentUser.admin))
  throw new Error("user_id not viewable by current user; omit userId or use admin token")

Type guard

const isOwnScope = (v) => v.userId === undefined || v.userId === currentUser.id

Prevention

When it happens

Trigger: Querying legacyNode/course.discussionsConnection (discussions_connection) with a user_id argument for a user the caller is not allowed to view as — e.g. a student supplying another student's user_id, or a teacher without 'view as' rights targeting an admin.

Common situations: Developers testing the GraphQL API with a hard-coded user_id belonging to a different account; tooling that caches one user's token while querying another user's discussions; admins operating in a sub-account where 'View as' is restricted.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

Thrown at app/graphql/interfaces/discussions_connection_interface.rb:46

    argument :search_term, String, <<~MD, required: false
      only return discussions whose title matches this search term
    MD
    argument :is_announcement, Boolean, <<~MD, required: false
      only return discussions that are announcements (true) or
      regular discussions (false). If not provided, returns both.
    MD
  end

  def discussions_scope(course, user_id = nil, search_term = nil, is_announcement = nil)
    scoped_user = user_id.nil? ? current_user : User.find_by(id: user_id)

    # If user_id was provided but user not found, return no discussions
    return DiscussionTopic.none if user_id.present? && scoped_user.nil?

    # Check if current user has permission to view discussions as the scoped user
    unless current_user.can_current_user_view_as_user(course, scoped_user)
      # Current user lacks permissions to view as the scoped user
      raise GraphQL::ExecutionError, "You do not have permission to view this course."
    end

    discussions = if is_announcement == true
                    # For announcements, use the same logic as /courses/X/announcements page
                    # Start with active_announcements scope (just workflow_state <> 'deleted')
                    course.active_announcements
                  else
                    course.discussion_topics.active
                  end

    # Apply announcement filter for non-announcement-specific queries
    if is_announcement == false
      discussions = discussions.where(type: ["DiscussionTopic", nil])
    end

    # Apply search term filter if provided
    if search_term.present?
      discussions = discussions.where(DiscussionTopic.wildcard(:title, search_term))

View on GitHub (pinned to 1c9f0bb801)