Skip to content

fix: run course access check before learner user substitution#400

Merged
Alam-2U merged 1 commit into
release-ulmofrom
LP-982
Jul 23, 2026
Merged

fix: run course access check before learner user substitution#400
Alam-2U merged 1 commit into
release-ulmofrom
LP-982

Conversation

@Alam-2U

@Alam-2U Alam-2U commented Jul 23, 2026

Copy link
Copy Markdown

Summary

Fixes a regression where staff using "View as Specific Learner" on a learner with expired audit access would encounter a generic "An unexpected error occurred" page instead of the expected course outline.

Problem

When masquerading as a learner, the outline view replaces request.user (the staff user) with a fresh User.objects.get() instance of the masqueraded learner. This newly fetched user object does not include masquerade_settings, causing check_course_expired to lose the masquerade context and incorrectly return a 403 "Access expired" response.

The frontend (frontend-app-learning) suppresses this 403 and relies on the metadata endpoint to determine access. Since the metadata endpoint evaluates access using the original staff user, it returns hasAccess: true. As a result, the frontend attempts to render the course outline with an empty outline model, leading to the generic error page.

Root Cause

get_course_or_403() was executed after substituting request.user with the masqueraded learner. As a result, the access check was performed against a fresh learner object that lacked masquerade_settings, instead of the original request.user, where the masquerade context had already been established by setup_masquerade.

Fix

Reordered the existing code so that get_course_or_403() executes before the learner substitution:

# Access check runs first — request.user still has masquerade_settings
course = get_course_or_403(request.user, "load", course_key, ...)

# Replace the user only after access validation so downstream visibility
# checks continue to use the masqueraded learner's permissions.
if user_is_masquerading and masquerade_object.role == "student":
    user = User.objects.get(username=masquerade_object.user_name)

Ticket: LP-982

Copilot AI review requested due to automatic review settings July 23, 2026 09:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a regression in the Course Home Outline endpoint where staff using “View as Specific Learner” could hit a generic error page when the target learner’s audit access had expired, due to an access check being evaluated against a freshly-fetched learner User instance lacking masquerade context.

Changes:

  • Moves the get_course_or_403(...) access check to run immediately after setup_masquerade(...), before substituting the local user variable with a newly fetched learner user.
  • Updates the masquerade comment to reflect that the fallback behavior continues with request.user when the masqueraded username cannot be loaded.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread lms/djangoapps/course_home_api/outline/views.py
Copilot AI review requested due to automatic review settings July 23, 2026 11:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

lms/djangoapps/course_home_api/outline/views.py:213

  • setup_masquerade already returns a masqueraded user with masquerade_settings (and real_user) populated when masquerading as a specific learner. Re-fetching the learner via User.objects.get(...) creates a fresh user instance without those attributes, which can reintroduce the original regression for downstream masquerade-aware checks (e.g., get_access_expiration_data / check_course_expired).
        if user_is_masquerading and masquerade_object.role == 'student':
            try:
                User = get_user_model()
                # If the masqueraded user does not exist, we will continue with request.user.
                username = masquerade_object.user_name

Copilot AI review requested due to automatic review settings July 23, 2026 12:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

lms/djangoapps/course_home_api/outline/views.py:214

  • The User.objects.get(username=...) re-fetch creates a new user instance without masquerade_settings/real_user. That drops masquerade context for downstream helpers that rely on get_course_masquerade(user, course_key) (e.g., get_access_expiration_data computes masquerading_expired_course via is_masquerading_as_specific_student). Since setup_masquerade already returns a masqueraded user with masquerade_settings attached (when specific learner masquerade is active), avoid re-fetching here (or explicitly copy masquerade_settings/real_user onto the fetched instance).
            try:
                User = get_user_model()
                # If the masqueraded user does not exist, we will continue with request.user.
                username = masquerade_object.user_name
                user = User.objects.get(username=username)

Copilot AI review requested due to automatic review settings July 23, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

lms/djangoapps/course_home_api/outline/views.py:206

  • This change fixes a masquerade/access regression path, but there is no test covering the specific scenario described in the PR (staff using “View as Specific Learner” for a learner with expired audit access). Given existing coverage for this endpoint in lms/djangoapps/course_home_api/outline/tests/test_view.py, consider adding a regression test that enables CourseDurationLimitConfig, sets up an audit enrollment past expiration, enables masquerade to that learner, and asserts the outline endpoint returns 200 (and appropriate access-expiration metadata) rather than denying access.
        course = get_course_or_403(request.user, 'load', course_key, check_if_enrolled=False)

Comment thread lms/djangoapps/course_home_api/outline/views.py
@Alam-2U
Alam-2U merged commit 2ecdc89 into release-ulmo Jul 23, 2026
65 checks passed
@Alam-2U
Alam-2U deleted the LP-982 branch July 23, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants