diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_details.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_details.py index ac021f378a43..9d93e9914be0 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/course_details.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_details.py @@ -3,12 +3,18 @@ import edx_api_doc_tools as apidocs from django.core.exceptions import ValidationError from opaque_keys.edx.keys import CourseKey +from openedx_authz.constants.permissions import ( + COURSES_EDIT_DETAILS, + COURSES_EDIT_SCHEDULE, + COURSES_VIEW_SCHEDULE_AND_DETAILS, +) from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -from common.djangoapps.student.auth import has_studio_read_access from common.djangoapps.util.json_request import JsonResponseBadRequest +from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission +from openedx.core.djangoapps.authz.decorators import user_has_course_permission from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes from xmodule.modulestore.django import modulestore @@ -17,6 +23,62 @@ from ..serializers import CourseDetailsSerializer +def _classify_update(payload: dict, course_key: CourseKey) -> tuple[bool, bool]: + """ + Determine whether the payload is updating schedule fields, detail fields, or both + for the course identified by course_key. + + Returns: + (is_schedule_update, is_details_update) + """ + schedule_fields = frozenset({"start_date", "end_date", "enrollment_start", "enrollment_end"}) + + course_details = CourseDetails.fetch(course_key) + + is_schedule_update = False + is_details_update = False + + serializer = CourseDetailsSerializer() + + for field, payload_value in payload.items(): + # Early exit for efficiency + if is_schedule_update and is_details_update: + break + + # Ignore unknown fields if needed + if field not in serializer.fields: + continue + + current_value = getattr(course_details, field, None) + + # Check schedule fields + if field in schedule_fields: + if is_schedule_update: + # Already classified as schedule update, no need to check again + continue + try: + # Convert payload value to internal value for accurate comparison + # on date fields + if payload_value is not None: + payload_value = serializer.fields[field].to_internal_value(payload_value) + except ValidationError as exc: + raise ValidationError( + f"Invalid date format for field {field}: {payload_value}" + ) from exc + + if payload_value != current_value: + is_schedule_update = True + else: + # Any non-schedule field counts as details update + if is_details_update: + # Already classified as details update, no need to check again + continue + if payload_value != current_value: + is_details_update = True + + return is_schedule_update, is_details_update + + @view_auth_classes(is_authenticated=True) class CourseDetailsView(DeveloperErrorViewMixin, APIView): """ @@ -99,7 +161,12 @@ def get(self, request: Request, course_id: str): ``` """ course_key = CourseKey.from_string(course_id) - if not has_studio_read_access(request.user, course_key): + if not user_has_course_permission( + request.user, + COURSES_VIEW_SCHEDULE_AND_DETAILS.identifier, + course_key, + LegacyAuthoringPermission.READ + ): self.permission_denied(request) course_details = CourseDetails.fetch(course_key) @@ -142,7 +209,26 @@ def put(self, request: Request, course_id: str): along with all the course's details similar to a ``GET`` request. """ course_key = CourseKey.from_string(course_id) - if not has_studio_read_access(request.user, course_key): + is_schedule_update, is_details_update = _classify_update(request.data, course_key) + + if not is_schedule_update and not is_details_update: + # No updatable fields provided in the request + is_details_update = True # To trigger permission check and return 403 if user cannot edit details + + if is_schedule_update and not user_has_course_permission( + request.user, + COURSES_EDIT_SCHEDULE.identifier, + course_key, + LegacyAuthoringPermission.READ + ): + self.permission_denied(request) + + if is_details_update and not user_has_course_permission( + request.user, + COURSES_EDIT_DETAILS.identifier, + course_key, + LegacyAuthoringPermission.READ + ): self.permission_denied(request) course_block = modulestore().get_course(course_key) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/settings.py b/cms/djangoapps/contentstore/rest_api/v1/views/settings.py index 6c74d832dafc..d5f24fe81ddb 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/settings.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/settings.py @@ -3,12 +3,14 @@ import edx_api_doc_tools as apidocs from django.conf import settings from opaque_keys.edx.keys import CourseKey +from openedx_authz.constants.permissions import COURSES_VIEW_COURSE from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -from common.djangoapps.student.auth import has_studio_read_access from lms.djangoapps.certificates.api import can_show_certificate_available_date_field +from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission +from openedx.core.djangoapps.authz.decorators import user_has_course_permission from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes from xmodule.modulestore.django import modulestore @@ -99,7 +101,12 @@ def get(self, request: Request, course_id: str): ``` """ course_key = CourseKey.from_string(course_id) - if not has_studio_read_access(request.user, course_key): + if not user_has_course_permission( + request.user, + COURSES_VIEW_COURSE.identifier, + course_key, + LegacyAuthoringPermission.READ + ): self.permission_denied(request) with modulestore().bulk_operations(course_key): diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_details.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_details.py index cbc2fdc98c3f..40006dee56db 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_details.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_details.py @@ -6,9 +6,13 @@ import ddt from django.urls import reverse +from openedx_authz.constants.roles import COURSE_EDITOR, COURSE_STAFF from rest_framework import status +from rest_framework.test import APIClient +from cms.djangoapps.contentstore.rest_api.v1.views.course_details import _classify_update from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from openedx.core.djangoapps.authz.tests.mixins import CourseAuthoringAuthzTestMixin from ...mixins import PermissionAccessMixin @@ -41,7 +45,13 @@ def test_put_permissions_unauthorized(self): Test that an error is returned if the user is unauthorised. """ client, _ = self.create_non_staff_authed_user_client() - response = client.put(self.url) + pre_requisite_course_keys = [str(self.course.id), "invalid_key"] + request_data = {"pre_requisite_courses": pre_requisite_course_keys} + response = client.put( + path=self.url, + data=json.dumps(request_data), + content_type="application/json", + ) error = self.get_and_check_developer_response(response) self.assertEqual(error, "You do not have permission to perform this action.") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -111,3 +121,450 @@ def test_put_course_details(self): content_type="application/json", ) self.assertEqual(response.status_code, status.HTTP_200_OK) + + +@ddt.ddt +class CourseDetailsAuthzViewTest(CourseAuthoringAuthzTestMixin, CourseTestCase): + """ + Tests for CourseDetailsView using AuthZ permissions. + """ + + def setUp(self): + super().setUp() + self.url = reverse( + "cms.djangoapps.contentstore:v1:course_details", + kwargs={"course_id": self.course.id}, + ) + self.request_data = { + "about_sidebar_html": "", + "banner_image_name": "images_course_image.jpg", + "banner_image_asset_path": "/asset-v1:edX+E2E-101+course+type@asset+block@images_course_image.jpg", + "certificate_available_date": "2029-01-02T00:00:00Z", + "certificates_display_behavior": "end", + "course_id": "E2E-101", + "course_image_asset_path": "/static/studio/images/pencils.jpg", + "course_image_name": "bar_course_image_name", + "description": "foo_description", + "duration": "", + "effort": None, + "end_date": "2023-08-01T01:30:00Z", + "enrollment_end": "2023-05-30T01:00:00Z", + "enrollment_start": "2023-05-29T01:00:00Z", + "entrance_exam_enabled": "", + "entrance_exam_id": "", + "entrance_exam_minimum_score_pct": "50", + "intro_video": None, + "language": "creative-commons: ver=4.0 BY NC ND", + "learning_info": ["foo", "bar"], + "license": "creative-commons: ver=4.0 BY NC ND", + "org": "edX", + "overview": '', + "pre_requisite_courses": [], + "run": "course", + "self_paced": None, + "short_description": "", + "start_date": "2023-06-01T01:30:00Z", + "subtitle": "", + "syllabus": None, + "title": "", + "video_thumbnail_image_asset_path": "/asset-v1:edX+E2E-101+course+type@asset+block@images_course_image.jpg", + "video_thumbnail_image_name": "images_course_image.jpg", + "instructor_info": { + "instructors": [ + { + "name": "foo bar", + "title": "title", + "organization": "org", + "image": "image", + "bio": "", + } + ] + }, + } + + def test_put_permissions_unauthenticated(self): + """ + Test that an error is returned in the absence of auth credentials. + """ + client = APIClient() # no auth + response = client.put(self.url) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_put_permissions_unauthorized(self): + """ + Test that an error is returned if the user is unauthorised. + """ + response = self.unauthorized_client.put( + path=self.url, + data=json.dumps(self.request_data), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_get_course_details_authorized(self): + """ + Authorized user with COURSE_EDITOR role can access course details. + """ + self.add_user_to_role_in_course( + self.authorized_user, + COURSE_EDITOR.external_key, + self.course.id + ) + + response = self.authorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_get_course_details_unauthorized(self): + """ + Unauthorized user should receive 403. + """ + response = self.unauthorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_get_course_details_staff_user(self): + """ + Django staff user should bypass AuthZ and access course details. + """ + response = self.staff_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_get_course_details_super_user(self): + """ + Superuser should bypass AuthZ and access course details. + """ + response = self.super_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @ddt.data( + # No changes + ({}, (False, False)), + ( + {"certificates_display_behavior": "end"}, # same value as existing course detail + (False, False), + ), + + # Schedule-only fields + ({"start_date": "2023-01-01"}, (True, False)), + ({"end_date": "2023-02-01"}, (True, False)), + ({"enrollment_start": "2023-01-01"}, (True, False)), + ({"enrollment_end": "2023-01-10"}, (True, False)), + + # Details-only fields + ({"title": "New Title"}, (False, True)), + ({"description": "New description"}, (False, True)), + ({"short_description": "Short"}, (False, True)), + ({"overview": "
HTML
"}, (False, True)), + + # Mixed fields + ( + {"title": "New Title", "start_date": "2023-01-01"}, + (True, True) + ), + + # Non-updatable / irrelevant fields + ({"random_field": "value"}, (False, False)), + ) + @ddt.unpack + def test_classify_update(self, payload, expected): + result = _classify_update(payload, self.course.id) + self.assertEqual(result, expected) + + def test_classyfy_update_with_get_request(self): + """ + GET request with no changes should not be classified as schedule or details update. + """ + # Get the current status of the course details to use + # as the basis for the update request + self.add_user_to_role_in_course( + self.authorized_user, + COURSE_EDITOR.external_key, + self.course.id + ) + response = self.authorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + current_course_details = response.json() + # This field is flagged as a details update because of a type mismatch: + # the GET response returns an invalid string, while the stored value has a different type. + # As a result, the equality check fails even though the values are logically the same. + current_course_details["certificates_display_behavior"] = "end" + + expected = (False, False) + result = _classify_update(current_course_details, self.course.id) + self.assertEqual(result, expected) + + def test_course_editor_can_edit_course_details(self): + """ + User with COURSE_EDITOR role can update course details. + COURSE_EDITOR does not have permission to edit schedule fields. + """ + + self.add_user_to_role_in_course( + self.authorized_user, + COURSE_EDITOR.external_key, + self.course.id + ) + + # Get the current status of the course details to use + # as the basis for the update request + response = self.authorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + current_course_details = response.json() + # This field is flagged as a details update because of a type mismatch: + # the GET response returns an invalid string, while the stored value has a different type. + # As a result, the equality check fails even though the values are logically the same. + current_course_details["certificates_display_behavior"] = "end" + + # Update the course details with new values, + # keeping schedule fields the same to ensure we are only + # testing edit details permission + current_course_details["title"] = "Updated Title" + + response = self.authorized_client.put( + path=self.url, + data=json.dumps(current_course_details), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_course_staff_can_edit_course_schedule(self): + """ + User with COURSE_STAFF role can update course schedule. + Only COURSE_STAFF and COURSE_ADMIN can edit schedule related fields. + """ + self.add_user_to_role_in_course( + self.authorized_user, + COURSE_STAFF.external_key, + self.course.id + ) + + # Get the current status of the course details to use + # as the basis for the update request + response = self.authorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + current_course_details = response.json() + # This field is flagged as a details update because of a type mismatch: + # the GET response returns an invalid string, while the stored value has a different type. + # As a result, the equality check fails even though the values are logically the same. + current_course_details["certificates_display_behavior"] = "end" + + # Update the course details with new values, + # changing schedule fields to ensure we are only + # testing edit schedule permission + current_course_details["end_date"] = "2023-08-01T01:30:00Z" + + response = self.authorized_client.put( + path=self.url, + data=json.dumps(current_course_details), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_course_editor_cannot_edit_course_schedule(self): + """ + User with COURSE_EDITOR role cannot update course schedule. + Only COURSE_STAFF and COURSE_ADMIN can edit schedule-related fields. + """ + self.add_user_to_role_in_course( + self.authorized_user, + COURSE_EDITOR.external_key, + self.course.id + ) + + # Get the current status of the course details to use + # as the basis for the update request + response = self.authorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + current_course_details = response.json() + # This field is flagged as a details update because of a type mismatch: + # the GET response returns an invalid string, while the stored value has a different type. + # As a result, the equality check fails even though the values are logically the same. + current_course_details["certificates_display_behavior"] = "end" + + # Update the course details with new values, + # changing schedule fields to ensure we are only + # testing edit schedule permission + current_course_details["end_date"] = "2023-08-01T01:30:00Z" + + response = self.authorized_client.put( + path=self.url, + data=json.dumps(current_course_details), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_course_staff_can_edit_course_schedule_and_details(self): + """ + User with COURSE_STAFF role can update course + schedule and details. + """ + self.add_user_to_role_in_course( + self.authorized_user, + COURSE_STAFF.external_key, + self.course.id + ) + + # Get the current status of the course details to use + # as the basis for the update request + response = self.authorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + current_course_details = response.json() + # This field is flagged as a details update because of a type mismatch: + # the GET response returns an invalid string, while the stored value has a different type. + # As a result, the equality check fails even though the values are logically the same. + current_course_details["certificates_display_behavior"] = "end" + + # Update the course details with new values, + # changing schedule and details fields to ensure user + # has permission to edit both + current_course_details["end_date"] = "2023-08-01T01:30:00Z" + current_course_details["title"] = "Updated Title" + + response = self.authorized_client.put( + path=self.url, + data=json.dumps(current_course_details), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_course_editor_cannot_edit_course_schedule_and_details(self): + """ + User with COURSE_EDITOR role cannot update course + schedule or course details. + """ + self.add_user_to_role_in_course( + self.authorized_user, + COURSE_EDITOR.external_key, + self.course.id + ) + + # Get the current status of the course details to use + # as the basis for the update request + response = self.authorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + current_course_details = response.json() + # This field is flagged as a details update because of a type mismatch: + # the GET response returns an invalid string, while the stored value has a different type. + # As a result, the equality check fails even though the values are logically the same. + current_course_details["certificates_display_behavior"] = "end" + + # Update the course details with new values, + # changing schedule and details fields to ensure user + # has permission to edit both + current_course_details["end_date"] = "2023-08-01T01:30:00Z" + current_course_details["title"] = "Updated Title" + + response = self.authorized_client.put( + path=self.url, + data=json.dumps(current_course_details), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_unauthorized_user_cannot_edit_with_any_change_on_the_payload(self): + """ + An unauthorized user should receive 403 even if the payload contains + no changes that do not require edit permissions. + """ + self.add_user_to_role_in_course( + self.authorized_user, + COURSE_EDITOR.external_key, + self.course.id + ) + + # Get the current status of the course details to use + # as the basis for the update request + response = self.authorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + current_course_details = response.json() + # This field is flagged as a details update because of a type mismatch: + # the GET response returns an invalid string, while the stored value has a different type. + # As a result, the equality check fails even though the values are logically the same. + current_course_details["certificates_display_behavior"] = "end" + + # Update the course details with the same values. + response = self.unauthorized_client.put( + path=self.url, + data=json.dumps(current_course_details), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_put_user_without_role_then_added_can_update(self): + """ + Validate dynamic role assignment works for PUT. + """ + # Initially unauthorized + response = self.unauthorized_client.put( + path=self.url, + data=json.dumps(self.request_data), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Assign role dynamically + self.add_user_to_role_in_course( + self.unauthorized_user, + COURSE_STAFF.external_key, + self.course.id + ) + + response = self.unauthorized_client.put( + path=self.url, + data=json.dumps(self.request_data), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @patch.dict("django.conf.settings.FEATURES", {"ENABLE_PREREQUISITE_COURSES": True}) + def test_put_invalid_pre_requisite_course_with_authz(self): + """ + Ensure validation still applies under AuthZ. + """ + self.add_user_to_role_in_course( + self.authorized_user, + COURSE_EDITOR.external_key, + self.course.id + ) + + pre_requisite_course_keys = [str(self.course.id), "invalid_key"] + request_data = {"pre_requisite_courses": pre_requisite_course_keys} + + response = self.authorized_client.put( + path=self.url, + data=json.dumps(request_data), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json()["error"], "Invalid prerequisite course key") + + def test_staff_user_can_update_without_authz_role(self): + """ + Django staff user should bypass AuthZ. + """ + response = self.staff_client.put( + path=self.url, + data=json.dumps(self.request_data), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_superuser_can_update_without_authz_role(self): + """ + Superuser should bypass AuthZ. + """ + response = self.super_client.put( + path=self.url, + data=json.dumps(self.request_data), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_settings.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_settings.py index 15b0992fdf1a..50bceaa16377 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_settings.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_settings.py @@ -6,11 +6,13 @@ import ddt from django.conf import settings from django.urls import reverse +from openedx_authz.constants.roles import COURSE_EDITOR from rest_framework import status from cms.djangoapps.contentstore.tests.utils import CourseTestCase from cms.djangoapps.contentstore.utils import get_proctored_exam_settings_url from common.djangoapps.util.course import get_link_for_about_page +from openedx.core.djangoapps.authz.tests.mixins import CourseAuthoringAuthzTestMixin from openedx.core.djangoapps.credit.tests.factories import CreditCourseFactory from ...mixins import PermissionAccessMixin @@ -85,3 +87,78 @@ def test_prerequisite_courses_enabled_setting(self): response = self.client.get(self.url) self.assertIn("possible_pre_requisite_courses", response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) + + +@ddt.ddt +class CourseSettingsAuthzViewTest(CourseAuthoringAuthzTestMixin, CourseTestCase): + """ + Tests for CourseSettingsView using AuthZ permissions. + """ + + def setUp(self): + super().setUp() + self.url = reverse( + "cms.djangoapps.contentstore:v1:course_settings", + kwargs={"course_id": self.course.id}, + ) + + def test_authorized_user_can_access_course_settings(self): + """Authorized user with COURSE_EDITOR role can access course settings.""" + self.add_user_to_role_in_course(self.authorized_user, COURSE_EDITOR.external_key, self.course.id) + response = self.authorized_client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("course_display_name", response.data) + + def test_unauthorized_user_cannot_access_course_settings(self): + """Unauthorized user should receive 403.""" + response = self.unauthorized_client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_user_without_role_then_added_can_access(self): + """ + Validate dynamic role assignment works as expected. + """ + # Initially unauthorized + response = self.unauthorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Assign role dynamically + self.add_user_to_role_in_course( + self.unauthorized_user, + COURSE_EDITOR.external_key, + self.course.id + ) + + response = self.unauthorized_client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @patch.dict("django.conf.settings.FEATURES", {"ENABLE_CREDIT_ELIGIBILITY": True}) + def test_credit_eligibility_setting_with_authz(self): + """ + Ensure feature flags still affect response under AuthZ. + """ + _ = CreditCourseFactory(course_key=self.course.id, enabled=True) + + self.add_user_to_role_in_course(self.authorized_user, COURSE_EDITOR.external_key, self.course.id) + response = self.authorized_client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("credit_requirements", response.data) + self.assertTrue(response.data["is_credit_course"]) + + def test_staff_user_can_access_without_authz_role(self): + """Django staff user should access course settings without AuthZ role.""" + + response = self.staff_client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("course_display_name", response.data) + + def test_superuser_can_access_without_authz_role(self): + """Superuser should access course settings without AuthZ role.""" + response = self.super_client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("course_display_name", response.data) diff --git a/openedx/core/djangoapps/authz/tests/mixins.py b/openedx/core/djangoapps/authz/tests/mixins.py index 39360c49fb29..c6385115693f 100644 --- a/openedx/core/djangoapps/authz/tests/mixins.py +++ b/openedx/core/djangoapps/authz/tests/mixins.py @@ -55,6 +55,14 @@ def setUp(self): self.unauthorized_client = APIClient() self.unauthorized_client.force_authenticate(user=self.unauthorized_user) + self.super_user = UserFactory(is_superuser=True, password=self.password) + self.super_client = APIClient() + self.super_client.force_authenticate(user=self.super_user) + + self.staff_user = UserFactory(is_staff=True, password=self.password) + self.staff_client = APIClient() + self.staff_client.force_authenticate(user=self.staff_user) + def tearDown(self): super().tearDown() AuthzEnforcer.get_enforcer().clear_policy()