From b6f0e0e376741d6464518c1e67d164e05cdaedde Mon Sep 17 00:00:00 2001 From: Kevyn Suarez Date: Fri, 25 Sep 2026 12:54:19 -0500 Subject: [PATCH] fix: enforce courses.manage_pages_and_resources on ProctoredExamSettingsView GET and POST on this endpoint only required studio read access, so a user who could view a course in Studio could also read or overwrite its proctored exam settings by calling the API directly, bypassing the UI's staff-only enforcement. Add the same authz check used elsewhere for pages-and-resources management, with a write-level legacy fallback when authz is disabled. Closes openedx-authz#443 --- .../rest_api/v1/views/proctoring.py | 13 ++++++++++- .../v1/views/tests/test_proctoring.py | 22 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/proctoring.py b/cms/djangoapps/contentstore/rest_api/v1/views/proctoring.py index 6aa4aa3c17f1..89996cb15ffa 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/proctoring.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/proctoring.py @@ -5,8 +5,9 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from opaque_keys.edx.keys import CourseKey +from openedx_authz.constants.permissions import COURSES_MANAGE_PAGES_AND_RESOURCES from rest_framework import status -from rest_framework.exceptions import NotFound +from rest_framework.exceptions import NotFound, PermissionDenied from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -15,6 +16,8 @@ from cms.djangoapps.contentstore.views.course import get_course_and_check_access from cms.djangoapps.models.settings.course_metadata import CourseMetadata from common.djangoapps.student.auth import check_course_advanced_settings_access +from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission +from openedx.core.djangoapps.authz.decorators import user_has_course_permission from openedx.core.djangoapps.course_apps.toggles import exams_ida_enabled from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes from xmodule.course_block import ( # pylint: disable=wrong-import-order @@ -200,6 +203,14 @@ def _get_and_validate_course_access(user, course_id): f'Course with course_id {course_id} does not exist.' ) + if not user_has_course_permission( + user, + COURSES_MANAGE_PAGES_AND_RESOURCES.identifier, + course_key, + LegacyAuthoringPermission.WRITE, + ): + raise PermissionDenied + return course_block diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_proctoring.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_proctoring.py index eb09d5f4e6da..eaa71ee19d2f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_proctoring.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_proctoring.py @@ -7,7 +7,7 @@ from django.test.utils import override_settings from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag -from openedx_authz.constants.permissions import COURSES_VIEW_ADVANCED_SETTINGS +from openedx_authz.constants.permissions import COURSES_MANAGE_PAGES_AND_RESOURCES, COURSES_VIEW_ADVANCED_SETTINGS from rest_framework import status from rest_framework.test import APITestCase @@ -42,6 +42,26 @@ def test_404_no_course_block(self): "detail": f"Course with course_id {course_id} does not exist." } + @patch.object(core_toggles.AUTHZ_COURSE_AUTHORING_FLAG, 'is_enabled', return_value=True) + @patch('openedx.core.djangoapps.authz.decorators.authz_api.is_user_allowed') + def test_authz_user_allowed(self, mock_is_user_allowed, mock_flag): + """A user holding the authz manage_pages_and_resources permission can access.""" + mock_is_user_allowed.return_value = True + self.client.login(username=self.global_staff.username, password=self.password) + response = self.make_request() + assert response.status_code == status.HTTP_200_OK + called_permission = mock_is_user_allowed.call_args[0][1] + assert called_permission == COURSES_MANAGE_PAGES_AND_RESOURCES.identifier + + @patch.object(core_toggles.AUTHZ_COURSE_AUTHORING_FLAG, 'is_enabled', return_value=True) + @patch('openedx.core.djangoapps.authz.decorators.authz_api.is_user_allowed') + def test_authz_user_not_allowed(self, mock_is_user_allowed, mock_flag): + """Without the authz manage_pages_and_resources permission, access is denied, even for staff.""" + mock_is_user_allowed.return_value = False + self.client.login(username=self.global_staff.username, password=self.password) + response = self.make_request() + assert response.status_code == status.HTTP_403_FORBIDDEN + class ProctoringExamSettingsGetTests( ProctoringExamSettingsTestcase, ModuleStoreTestCase, APITestCase