Skip to content

Commit 6e50d3b

Browse files
dwong2708wgu-taylor-payne
authored andcommitted
feat: apply authz decorator to quality and validation views
1 parent 22a1f92 commit 6e50d3b

5 files changed

Lines changed: 157 additions & 5 deletions

File tree

cms/djangoapps/contentstore/api/tests/test_quality.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
Tests for the course import API views
33
"""
44

5-
5+
from rest_framework.test import APIClient
66
from rest_framework import status
7+
from openedx_authz.constants.roles import COURSE_STAFF, COURSE_DATA_RESEARCHER
78

9+
from common.djangoapps.student.tests.factories import UserFactory
10+
from openedx.core.djangoapps.authz.tests.mixins import CourseAuthzTestMixin
811
from .base import BaseCourseViewTest
912

1013

@@ -67,3 +70,63 @@ def test_student_fails(self):
6770
self.client.login(username=self.student.username, password=self.password)
6871
resp = self.client.get(self.get_url(self.course_key))
6972
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
73+
74+
75+
class CourseQualityAuthzTest(CourseAuthzTestMixin, BaseCourseViewTest):
76+
"""
77+
Tests Course Quality API authorization using openedx-authz.
78+
The endpoint uses COURSES_VIEW_COURSE permission.
79+
"""
80+
81+
view_name = "courses_api:course_quality"
82+
authz_roles_to_assign = [COURSE_STAFF.external_key]
83+
84+
def test_authorized_user_can_access(self):
85+
"""User with COURSE_STAFF role can access."""
86+
resp = self.authorized_client.get(self.get_url(self.course_key))
87+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
88+
89+
def test_unauthorized_user_cannot_access(self):
90+
"""User without role cannot access."""
91+
resp = self.unauthorized_client.get(self.get_url(self.course_key))
92+
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
93+
94+
def test_role_scoped_to_course(self):
95+
"""Authorization should only apply to the assigned course."""
96+
other_course = self.store.create_course("OtherOrg", "OtherCourse", "Run", self.staff.id)
97+
98+
resp = self.authorized_client.get(self.get_url(other_course.id))
99+
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
100+
101+
def test_staff_user_allowed_via_legacy(self):
102+
"""
103+
Staff users should still pass through legacy fallback.
104+
"""
105+
self.client.login(username=self.staff.username, password=self.password)
106+
107+
resp = self.client.get(self.get_url(self.course_key))
108+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
109+
110+
def test_superuser_allowed(self):
111+
"""Superusers should always be allowed."""
112+
superuser = UserFactory(is_superuser=True)
113+
114+
client = APIClient()
115+
client.force_authenticate(user=superuser)
116+
117+
resp = client.get(self.get_url(self.course_key))
118+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
119+
120+
def test_non_staff_user_cannot_access(self):
121+
"""
122+
User without permissions should be denied.
123+
This case validates that a non-staff user cannot access even
124+
if they have course author access to the course.
125+
"""
126+
non_staff_user = UserFactory()
127+
non_staff_client = APIClient()
128+
self.add_user_to_role(non_staff_user, COURSE_DATA_RESEARCHER.external_key)
129+
non_staff_client.force_authenticate(user=non_staff_user)
130+
131+
resp = non_staff_client.get(self.get_url(self.course_key))
132+
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)

cms/djangoapps/contentstore/api/tests/test_validation.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@
77

88
import ddt
99
import factory
10+
1011
from django.conf import settings
1112
from django.contrib.auth import get_user_model
1213
from django.test.utils import override_settings
1314
from django.urls import reverse
1415
from rest_framework import status
1516
from rest_framework.test import APITestCase
17+
from rest_framework.test import APIClient
18+
from openedx.core.djangoapps.authz.tests.mixins import CourseAuthzTestMixin
19+
from openedx_authz.constants.roles import COURSE_STAFF, COURSE_DATA_RESEARCHER
1620

1721
from common.djangoapps.course_modes.models import CourseMode
1822
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
1923
from common.djangoapps.student.tests.factories import StaffFactory, UserFactory
2024
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
2125
from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory
26+
from cms.djangoapps.contentstore.api.tests.base import BaseCourseViewTest
2227

2328
User = get_user_model()
2429

@@ -272,3 +277,81 @@ def test_list_ready_to_update_reference_success(self, mock_block, mock_auth):
272277
{'usage_key': str(self.block2.location)},
273278
])
274279
mock_auth.assert_called_once()
280+
281+
282+
class CourseValidationAuthzTest(CourseAuthzTestMixin, BaseCourseViewTest):
283+
"""
284+
Tests Course Validation API authorization using openedx-authz.
285+
The endpoint uses COURSES_VIEW_COURSE permission.
286+
"""
287+
288+
view_name = "courses_api:course_validation"
289+
authz_roles_to_assign = [COURSE_STAFF.external_key]
290+
291+
def test_authorized_user_can_access(self):
292+
"""
293+
User with COURSE_STAFF role should be allowed via AuthZ.
294+
"""
295+
resp = self.authorized_client.get(self.get_url(self.course_key))
296+
297+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
298+
299+
def test_unauthorized_user_cannot_access(self):
300+
"""
301+
User without permissions should be denied.
302+
"""
303+
resp = self.unauthorized_client.get(self.get_url(self.course_key))
304+
305+
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
306+
307+
def test_role_scoped_to_course(self):
308+
"""
309+
Authorization should only apply to the assigned course scope.
310+
"""
311+
other_course = self.store.create_course(
312+
"OtherOrg",
313+
"OtherCourse",
314+
"Run",
315+
self.staff.id,
316+
)
317+
318+
resp = self.authorized_client.get(self.get_url(other_course.id))
319+
320+
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
321+
322+
def test_staff_user_allowed_via_legacy(self):
323+
"""
324+
Course staff should pass through legacy fallback when AuthZ denies.
325+
"""
326+
self.client.login(username=self.staff.username, password=self.password)
327+
328+
resp = self.client.get(self.get_url(self.course_key))
329+
330+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
331+
332+
def test_superuser_allowed(self):
333+
"""
334+
Superusers should always be allowed through legacy fallback.
335+
"""
336+
superuser = UserFactory(is_superuser=True)
337+
338+
client = APIClient()
339+
client.force_authenticate(user=superuser)
340+
341+
resp = client.get(self.get_url(self.course_key))
342+
343+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
344+
345+
def test_non_staff_user_cannot_access(self):
346+
"""
347+
User without permissions should be denied.
348+
This case validates that a non-staff user cannot access even
349+
if they have course author access to the course.
350+
"""
351+
non_staff_user = UserFactory()
352+
non_staff_client = APIClient()
353+
self.add_user_to_role(non_staff_user, COURSE_DATA_RESEARCHER.external_key)
354+
non_staff_client.force_authenticate(user=non_staff_user)
355+
356+
resp = non_staff_client.get(self.get_url(self.course_key))
357+
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)

cms/djangoapps/contentstore/api/views/course_quality.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
from edxval.api import get_course_videos_qset
77
from rest_framework.generics import GenericAPIView
88
from rest_framework.response import Response
9+
from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission
910
from scipy import stats
11+
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE
1012

1113
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
1214
from openedx.core.lib.cache_utils import request_cached
1315
from openedx.core.lib.graph_traversals import traverse_pre_order
16+
from openedx.core.djangoapps.authz.decorators import authz_permission_required
1417
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
1518

16-
from .utils import course_author_access_required, get_bool_param
19+
from .utils import get_bool_param
1720

1821
log = logging.getLogger(__name__)
1922

@@ -82,7 +85,7 @@ class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView):
8285
# does not specify a serializer class.
8386
swagger_schema = None
8487

85-
@course_author_access_required
88+
@authz_permission_required(COURSES_VIEW_COURSE.identifier, LegacyAuthoringPermission.READ)
8689
def get(self, request, course_key):
8790
"""
8891
Returns validation information for the given course.

cms/djangoapps/contentstore/api/views/course_validation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from rest_framework import serializers, status
1010
from rest_framework.generics import GenericAPIView
1111
from rest_framework.response import Response
12+
from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission
1213
from user_tasks.models import UserTaskStatus
1314
from user_tasks.views import StatusViewSet
15+
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE
1416

1517
from cms.djangoapps.contentstore.course_info_model import get_course_updates
1618
from cms.djangoapps.contentstore.tasks import migrate_course_legacy_library_blocks_to_item_bank
@@ -19,6 +21,7 @@
1921
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
2022
from openedx.core.lib.api.serializers import StatusSerializerWithUuid
2123
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
24+
from openedx.core.djangoapps.authz.decorators import authz_permission_required
2225
from xmodule.course_metadata_utils import DEFAULT_GRADING_POLICY # lint-amnesty, pylint: disable=wrong-import-order
2326
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
2427

@@ -80,7 +83,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
8083
# does not specify a serializer class.
8184
swagger_schema = None
8285

83-
@course_author_access_required
86+
@authz_permission_required(COURSES_VIEW_COURSE.identifier, LegacyAuthoringPermission.READ)
8487
def get(self, request, course_key):
8588
"""
8689
Returns validation information for the given course.

cms/djangoapps/contentstore/api/views/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def course_author_access_required(view):
120120
Usage::
121121
@course_author_access_required
122122
def my_view(request, course_key):
123-
# Some functionality ...
123+
# Some functionality...
124124
"""
125125
def _wrapper_view(self, request, course_id, *args, **kwargs):
126126
"""

0 commit comments

Comments
 (0)