Skip to content

Commit 1599b80

Browse files
fix: respect masqueraded learner permissions when computing subsection show_grades in Progress Tab API (#38025)
This PR fixes grade-visibility behavior in the Progress Tab API when staff users are masquerading as learners. Previously, subsection show_grades was computed using the real requester’s staff access, which could expose grades that the masqueraded learner should not see. Now, show_grades respects the masqueraded user’s permissions so the API response matches the learner view. Co-authored-by: Peter Pinch <pdpinch@mit.edu>
1 parent 05f51e8 commit 1599b80

File tree

2 files changed

+51
-7
lines changed

2 files changed

+51
-7
lines changed

lms/djangoapps/course_home_api/progress/tests/test_views.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,49 @@ def test_masquerade(self):
117117
self.update_masquerade(username=verified_user.username)
118118
assert self.client.get(self.url).data.get('enrollment_mode') == 'verified'
119119

120+
def test_masquerade_uses_masqueraded_permissions_for_show_grades(self):
121+
"""
122+
Test that when a staff user is masquerading as a verified learner,
123+
the grade visibility for subsections with show_correctness='past_due' is
124+
determined based on the verified learner's permissions, not the staff user's
125+
permissions.
126+
"""
127+
subsection_name = 'Masquerade grade visibility subsection'
128+
chapter = BlockFactory(parent=self.course, category='chapter')
129+
subsection = BlockFactory(
130+
parent=chapter,
131+
category='sequential',
132+
display_name=subsection_name,
133+
graded=True,
134+
due=now() + timedelta(days=30),
135+
show_correctness='past_due',
136+
)
137+
vertical = BlockFactory(parent=subsection, category='vertical', graded=True)
138+
BlockFactory(parent=vertical, category='problem', graded=True)
139+
140+
verified_user = UserFactory(is_staff=False)
141+
CourseEnrollment.enroll(verified_user, self.course.id, CourseMode.VERIFIED)
142+
143+
self.switch_to_staff() # needed for masquerade
144+
145+
def get_subsection_show_grades(response):
146+
for chapter in response.data['section_scores']:
147+
for subsection in chapter['subsections']:
148+
if subsection['display_name'] == subsection_name:
149+
return subsection['show_grades']
150+
assert False, f'Subsection {subsection_name} not found in section_scores'
151+
152+
# Staff can see grades even when show_correctness is `past_due` and the due date has not passed.
153+
response = self.client.get(self.url)
154+
assert response.status_code == 200
155+
assert get_subsection_show_grades(response) is True
156+
157+
# When masquerading, grade visibility should follow the masqueraded learner permissions.
158+
self.update_masquerade(username=verified_user.username)
159+
response = self.client.get(self.url)
160+
assert response.status_code == 200
161+
assert get_subsection_show_grades(response) is False
162+
120163
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
121164
def test_has_scheduled_content_data(self):
122165
CourseEnrollment.enroll(self.user, self.course.id)

lms/djangoapps/course_home_api/progress/views.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def _visible_section_scores(self, course_grade):
191191
visible_chapters.append({**chapter, "sections": filtered_sections})
192192
return visible_chapters
193193

194-
def get(self, request, *args, **kwargs):
194+
def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements
195195
course_key_string = kwargs.get('course_key_string')
196196
course_key = CourseKey.from_string(course_key_string)
197197
student_id = kwargs.get('student_id')
@@ -203,9 +203,10 @@ def get(self, request, *args, **kwargs):
203203
monitoring_utils.set_custom_attribute('course_id', course_key_string)
204204
monitoring_utils.set_custom_attribute('user_id', request.user.id)
205205
monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff)
206-
is_staff = bool(has_access(request.user, 'staff', course_key))
206+
requester_has_staff_access = bool(has_access(request.user, 'staff', course_key))
207207

208-
student = self._get_student_user(request, course_key, student_id, is_staff)
208+
student = self._get_student_user(request, course_key, student_id, requester_has_staff_access)
209+
learner_has_staff_access = bool(has_access(student, 'staff', course_key))
209210
username = get_enterprise_learner_generic_name(request) or student.username
210211

211212
course = get_course_or_403(student, 'load', course_key, check_if_enrolled=False)
@@ -214,7 +215,7 @@ def get(self, request, *args, **kwargs):
214215
enrollment = CourseEnrollment.get_enrollment(student, course_key)
215216
enrollment_mode = getattr(enrollment, 'mode', None)
216217

217-
if not (enrollment and enrollment.is_active) and not is_staff:
218+
if not (enrollment and enrollment.is_active) and not requester_has_staff_access:
218219
return Response('User not enrolled.', status=401)
219220

220221
# The block structure is used for both the course_grade and has_scheduled content fields
@@ -223,7 +224,7 @@ def get(self, request, *args, **kwargs):
223224
course_grade = CourseGradeFactory().read(student, collected_block_structure=collected_block_structure)
224225

225226
# recalculate course grade from visible grades (stored grade was calculated over all grades, visible or not)
226-
course_grade.update(visible_grades_only=True, has_staff_access=is_staff)
227+
course_grade.update(visible_grades_only=True, has_staff_access=learner_has_staff_access)
227228

228229
# Get has_scheduled_content data
229230
transformers = BlockStructureTransformers()
@@ -265,7 +266,7 @@ def get(self, request, *args, **kwargs):
265266
assignment_type_grade_summary = aggregate_assignment_type_grade_summary(
266267
course_grade,
267268
grading_policy,
268-
has_staff_access=is_staff,
269+
has_staff_access=learner_has_staff_access,
269270
)
270271

271272
# Filter out section scores to only have those that are visible to the user
@@ -291,7 +292,7 @@ def get(self, request, *args, **kwargs):
291292
'final_grades': assignment_type_grade_summary["final_grades"],
292293
}
293294
context = self.get_serializer_context()
294-
context['staff_access'] = is_staff
295+
context['staff_access'] = learner_has_staff_access
295296
context['course_blocks'] = course_blocks
296297
context['course_key'] = course_key
297298
# course_overview and enrollment will be used by VerifiedModeSerializer

0 commit comments

Comments
 (0)