fix: run course access check before learner user substitution#400
Conversation
There was a problem hiding this comment.
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 aftersetup_masquerade(...), before substituting the localuservariable with a newly fetched learner user. - Updates the masquerade comment to reflect that the fallback behavior continues with
request.userwhen the masqueraded username cannot be loaded.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
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_masqueradealready returns a masqueraded user withmasquerade_settings(andreal_user) populated when masquerading as a specific learner. Re-fetching the learner viaUser.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
There was a problem hiding this comment.
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 withoutmasquerade_settings/real_user. That drops masquerade context for downstream helpers that rely onget_course_masquerade(user, course_key)(e.g.,get_access_expiration_datacomputesmasquerading_expired_courseviais_masquerading_as_specific_student). Sincesetup_masqueradealready returns a masqueraded user withmasquerade_settingsattached (when specific learner masquerade is active), avoid re-fetching here (or explicitly copymasquerade_settings/real_useronto 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)
There was a problem hiding this comment.
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 enablesCourseDurationLimitConfig, 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)
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 freshUser.objects.get()instance of the masqueraded learner. This newly fetched user object does not includemasquerade_settings, causingcheck_course_expiredto 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 returnshasAccess: 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 substitutingrequest.userwith the masqueraded learner. As a result, the access check was performed against a fresh learner object that lackedmasquerade_settings, instead of the originalrequest.user, where the masquerade context had already been established bysetup_masquerade.Fix
Reordered the existing code so that
get_course_or_403()executes before the learner substitution:Ticket: LP-982