diff --git a/lms/djangoapps/courseware/block_render.py b/lms/djangoapps/courseware/block_render.py index 650b4418b423..392d05832450 100644 --- a/lms/djangoapps/courseware/block_render.py +++ b/lms/djangoapps/courseware/block_render.py @@ -27,6 +27,7 @@ from edx_proctoring.api import get_attempt_status_summary from edx_proctoring.services import ProctoringService from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from edx_rest_framework_extensions.permissions import JwtRestrictedApplication from edx_when.field_data import DateLookupFieldData from eventtracking import tracker from opaque_keys import InvalidKeyError @@ -774,12 +775,19 @@ def handle_xblock_callback(request, course_id, usage_id, handler, suffix=None): ) else: if user_auth_tuple is not None: - request.user, _ = user_auth_tuple + # When using JWT authentication, the second element contains the JWT token. We need it to determine + # whether the application that issued the token is restricted. + request.user, request.auth = user_auth_tuple + # This is verified by the `JwtRestrictedApplication` before it decodes the token. + request.successful_authenticator = authenticator break # NOTE (CCB): Allow anonymous GET calls (e.g. for transcripts). Modifying this view is simpler than updating # the XBlocks to use `handle_xblock_callback_noauth`, which is practically identical to this view. - if request.method != 'GET' and not (request.user and request.user.is_authenticated): + # Block all request types coming from restricted applications. + if ( + request.method != 'GET' and not (request.user and request.user.is_authenticated) + ) or JwtRestrictedApplication().has_permission(request, None): # type: ignore return HttpResponseForbidden('Unauthenticated') request.user.known = request.user.is_authenticated diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 59a9551b772b..2fc727623541 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -699,7 +699,7 @@ def _ora_assessment_to_assignment( date_config_type = block_data.get_xblock_field(ora_block, 'date_config_type', 'manual') assignment_type = block_data.get_xblock_field(ora_block, 'format', None) block_title = block_data.get_xblock_field(ora_block, 'title', _('Open Response Assessment')) - course_key = block_data.root_block_usage_key + block_key = block_data.root_block_usage_key # Steps with no "due" date, like staff or training, should not show up here assessment_step_due = assessment.get('start') @@ -712,7 +712,7 @@ def _ora_assessment_to_assignment( extra_info = None elif date_config_type == 'course_end': assessment_start = None - assessment_due = block_data.get_xblock_field(course_key, 'end') + assessment_due = block_data.get_xblock_field(block_key, 'end') extra_info = None else: assessment_start, assessment_due = None, None @@ -746,7 +746,7 @@ def _ora_assessment_to_assignment( now = datetime.now(pytz.UTC) assignment_released = not assessment_start or assessment_start < now if assignment_released: - url = reverse('jump_to', args=[course_key, ora_block]) + url = reverse('jump_to', args=[block_key.course_key, ora_block]) past_due = not complete and assessment_due and assessment_due < now first_component_block_id = str(ora_block) diff --git a/lms/djangoapps/courseware/tests/test_block_render.py b/lms/djangoapps/courseware/tests/test_block_render.py index 80827afa632c..753672ae76e2 100644 --- a/lms/djangoapps/courseware/tests/test_block_render.py +++ b/lms/djangoapps/courseware/tests/test_block_render.py @@ -91,7 +91,7 @@ from lms.djangoapps.lms_xblock.field_data import LmsFieldData from openedx.core.djangoapps.credit.api import set_credit_requirement_status, set_credit_requirements from openedx.core.djangoapps.credit.models import CreditCourse -from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user +from openedx.core.djangoapps.oauth_dispatch.jwt import _create_jwt, create_jwt_for_user from openedx.core.djangoapps.oauth_dispatch.tests.factories import AccessTokenFactory, ApplicationFactory from openedx.core.lib.courses import course_image_url from openedx.core.lib.gating import api as gating_api @@ -383,6 +383,26 @@ def test_jwt_authentication(self): response = self.client.post(dispatch_url, {}, **headers) assert 200 == response.status_code + def test_jwt_authentication_with_restricted_application(self): + """Test that the XBlock endpoint disallows JWT authentication with restricted applications.""" + + def _mock_create_restricted_jwt(*args, **kwargs): + """Pass an additional argument to `_create_jwt` without modifying the signature of `create_jwt_for_user`.""" + kwargs['is_restricted'] = True + return _create_jwt(*args, **kwargs) + + with patch('openedx.core.djangoapps.oauth_dispatch.jwt._create_jwt', _mock_create_restricted_jwt): + token = create_jwt_for_user(self.mock_user) + + dispatch_url = self._get_dispatch_url() + headers = {'HTTP_AUTHORIZATION': 'JWT ' + token} + + response = self.client.get(dispatch_url, {}, **headers) + assert 403 == response.status_code + + response = self.client.post(dispatch_url, {}, **headers) + assert 403 == response.status_code + def test_missing_position_handler(self): """ Test that sending POST request without or invalid position argument don't raise server error diff --git a/openedx/features/survey_report/tests/test_query_methods.py b/openedx/features/survey_report/tests/test_query_methods.py index fd1a41f504f5..9a4b6431ed59 100644 --- a/openedx/features/survey_report/tests/test_query_methods.py +++ b/openedx/features/survey_report/tests/test_query_methods.py @@ -45,7 +45,7 @@ def test_get_unique_courses_offered(self): """ Test that get_unique_courses_offered returns the correct number of courses. """ - course_overview = CourseOverviewFactory.create(id=self.first_course.id, start="2019-01-01", end="2024-01-01") + course_overview = CourseOverviewFactory.create(id=self.first_course.id, start="2019-01-01", end="9999-01-01") CourseEnrollmentFactory.create(user=self.user, course_id=course_overview.id) CourseEnrollmentFactory.create(user=self.user1, course_id=course_overview.id) CourseEnrollmentFactory.create(user=self.user2, course_id=course_overview.id)