instructure/canvas-lms · error · Assignment::GradeError

User not authorized to give final provisional grades

Error message

User not authorized to give final provisional grades

What it means

Submission#find_or_create_provisional_grade! creates or updates a ModeratedGrading::ProvisionalGrade for a scorer. When attrs[:final] is true, the scorer must hold moderation permission on the assignment (assignment.permits_moderation?); otherwise Assignment::GradeError 'User not authorized to give final provisional grades' is raised. Final provisional grades are reserved for the assignment's moderator(s).

Solutions

  1. Ensure the scorer has the moderator role / 'Select final grade' permission on the assignment before passing final: true.
  2. Call with final: false for provisional (non-final) grades from non-moderator graders.
  3. Check assignment.permits_moderation?(scorer) client-side or in the controller and return a 401/403 before invoking the method.
  4. If the user should be a moderator, grant them the appropriate role (e.g. add as moderator on the moderated assignment / enroll with grading permissions).

Example fix

// before
submission.find_or_create_provisional_grade!(scorer, { final: true, score: 92 })
# => Assignment::GradeError for non-moderator

// after
if assignment.permits_moderation?(scorer)
  submission.find_or_create_provisional_grade!(scorer, { final: true, score: 92 })
else
  submission.find_or_create_provisional_grade!(scorer, { final: false, score: 92 })
end
Defensive patterns

Strategy: validation

Validate before calling

unless assignment.permits_moderation?(scorer)
  return render json: { error: 'not authorized for final provisional grades' }, status: :forbidden
end

Type guard

def can_give_final_provisional_grade?(assignment, user)
  assignment.permits_moderation?(user)
end

Try / catch

begin
  submission.find_or_create_provisional_grade!(scorer, final: true, score: score)
rescue Assignment::GradeError => e
  render json: { error: e.message }, status: :unauthorized
end

Prevention

When it happens

Trigger: Call submission.find_or_create_provisional_grade!(scorer, { final: true, ... }) where scorer is a TA/provisional grader without moderation rights (not the final grader / moderator, no 'Moderate Grades' permission on the course/assignment).

Common situations: API or UI request from a provisional grader (non-moderator TA) attempting to post a final grade during moderated grading; custom integrations passing final: true for any scorer; permission changes removing the moderator role after grades were being posted.

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/845c58acf698014f. Report an issue: GitHub.

Appendix: source

Thrown at app/models/submission.rb:2550

             pgs.detect { |pg2| !pg2.final && pg2.scorer_id == scorer.id }
           end
         elsif final
           provisional_grades.final.first
         else
           provisional_grades.not_final.where(scorer_id: scorer).first
         end

    if default_to_null_grade && pg.nil?
      ModeratedGrading::NullProvisionalGrade.new(self, scorer.id, final)
    else
      pg
    end
  end

  def find_or_create_provisional_grade!(scorer, attrs = {})
    ModeratedGrading::ProvisionalGrade.unique_constraint_retry do
      if attrs[:final] && !assignment.permits_moderation?(scorer)
        raise Assignment::GradeError, "User not authorized to give final provisional grades"
      end

      pg = find_existing_provisional_grade(scorer, attrs[:final]) || provisional_grades.build
      pg = update_provisional_grade(pg, scorer, attrs)
      pg.save! if attrs[:force_save] || pg.new_record? || pg.changed?
      pg
    end
  end

  def find_existing_provisional_grade(scorer, final)
    final ? provisional_grades.final.first : provisional_grades.not_final.find_by(scorer:)
  end

  def moderated_grading_allow_list(current_user = user, loaded_attachments: nil)
    return nil unless assignment.moderated_grading? && current_user.present?

    moderation_allow_list_for_user(current_user).map(&:moderated_grading_ids)
  end

View on GitHub (pinned to 1c9f0bb801)