Skip to content

Commit d629d22

Browse files
wgu-taylor-payneKiro
andauthored
feat: use granular permissions on course updates if authz authoring flag set (#38198)
Co-authored-by: Kiro <[email protected]>
1 parent 7c86626 commit d629d22

2 files changed

Lines changed: 138 additions & 2 deletions

File tree

cms/djangoapps/contentstore/views/course.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
has_studio_advanced_settings_access,
5757
is_content_creator,
5858
)
59+
from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission
60+
from openedx.core.djangoapps.authz.decorators import user_has_course_permission
61+
from openedx_authz.constants.permissions import COURSES_MANAGE_COURSE_UPDATES, COURSES_VIEW_COURSE_UPDATES
5962
from common.djangoapps.student.roles import (
6063
CourseInstructorRole,
6164
CourseStaffRole,
@@ -1100,8 +1103,14 @@ def course_info_update_handler(request, course_key_string, provided_id=None):
11001103
if provided_id == '':
11011104
provided_id = None
11021105

1103-
# check that logged in user has permissions to this item (GET shouldn't require this level?)
1104-
if not has_studio_write_access(request.user, usage_key.course_key):
1106+
if request.method == 'GET':
1107+
authz_perm = COURSES_VIEW_COURSE_UPDATES.identifier
1108+
legacy_perm = LegacyAuthoringPermission.READ
1109+
else:
1110+
authz_perm = COURSES_MANAGE_COURSE_UPDATES.identifier
1111+
legacy_perm = LegacyAuthoringPermission.WRITE
1112+
1113+
if not user_has_course_permission(request.user, authz_perm, usage_key.course_key, legacy_perm):
11051114
raise PermissionDenied()
11061115

11071116
if request.method == 'GET':

cms/djangoapps/contentstore/views/tests/test_course_updates.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44

55

66
import json
7+
78
from opaque_keys.edx.keys import UsageKey
9+
from openedx_authz.constants.roles import COURSE_AUDITOR, COURSE_STAFF
810

911
from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase
12+
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
1013
from cms.djangoapps.contentstore.utils import reverse_course_url, reverse_usage_url
14+
from common.djangoapps.student.tests.factories import UserFactory
15+
from openedx.core.djangoapps.authz.tests.mixins import CourseAuthzTestMixin
1116
from openedx.core.lib.xblock_utils import get_course_update_items
1217
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
1318

@@ -312,3 +317,125 @@ def test_course_update_id(self):
312317
course_updates = modulestore().get_item(updates_location)
313318
del course_updates.items[0]["status"]
314319
self.assertEqual(course_updates.items, [{'date': update_date, 'content': update_content, 'id': 2}])
320+
321+
322+
class CourseUpdateAuthzTest(CourseAuthzTestMixin, CourseTestCase):
323+
"""
324+
Tests course_info_update_handler authorization using openedx-authz.
325+
"""
326+
327+
authz_roles_to_assign = [COURSE_STAFF.external_key]
328+
329+
@property
330+
def course_key(self):
331+
return self.course.id
332+
333+
def create_update_url(self, provided_id=None):
334+
kwargs = {'provided_id': str(provided_id)} if provided_id else None
335+
return reverse_course_url('course_info_update_handler', self.course.id, kwargs=kwargs)
336+
337+
def setUp(self):
338+
super().setUp()
339+
self.auditor_user = UserFactory()
340+
self.add_user_to_role(self.auditor_user, COURSE_AUDITOR.external_key)
341+
342+
self.staff_client = self._make_client_for_user(self.authorized_user)
343+
self.auditor_client = self._make_client_for_user(self.auditor_user)
344+
self.unauthorized_client = self._make_client_for_user(self.unauthorized_user)
345+
346+
def _make_client_for_user(self, user):
347+
client = AjaxEnabledTestClient()
348+
client.login(username=user.username, password='Password1234')
349+
return client
350+
351+
def _create_update(self, client):
352+
return client.ajax_post(
353+
self.create_update_url(),
354+
{'content': 'Test update', 'date': 'January 1, 2026'},
355+
)
356+
357+
# -- Authorized user (course_staff): full access --
358+
359+
def test_authorized_user_can_get(self):
360+
resp = self.staff_client.get_json(self.create_update_url())
361+
self.assertEqual(resp.status_code, 200)
362+
363+
def test_authorized_user_can_post(self):
364+
resp = self._create_update(self.staff_client)
365+
self.assertEqual(resp.status_code, 200)
366+
367+
def test_authorized_user_can_put(self):
368+
update_id = self._create_update(self.staff_client).json()['id']
369+
resp = self.staff_client.ajax_post(
370+
self.create_update_url(update_id),
371+
{'content': 'Updated', 'date': 'January 2, 2026'},
372+
HTTP_X_HTTP_METHOD_OVERRIDE="PUT",
373+
REQUEST_METHOD="POST",
374+
)
375+
self.assertEqual(resp.status_code, 200)
376+
377+
def test_authorized_user_can_delete(self):
378+
update_id = self._create_update(self.staff_client).json()['id']
379+
resp = self.staff_client.delete(self.create_update_url(update_id))
380+
self.assertEqual(resp.status_code, 200)
381+
382+
# -- View-only user (course_auditor): GET only --
383+
384+
def test_auditor_can_get(self):
385+
resp = self.auditor_client.get_json(self.create_update_url())
386+
self.assertEqual(resp.status_code, 200)
387+
388+
def test_auditor_cannot_post(self):
389+
resp = self._create_update(self.auditor_client)
390+
self.assertEqual(resp.status_code, 403)
391+
392+
def test_auditor_cannot_put(self):
393+
update_id = self._create_update(self.staff_client).json()['id']
394+
resp = self.auditor_client.ajax_post(
395+
self.create_update_url(update_id),
396+
{'content': 'Test', 'date': 'January 2, 2026'},
397+
HTTP_X_HTTP_METHOD_OVERRIDE="PUT",
398+
REQUEST_METHOD="POST",
399+
)
400+
self.assertEqual(resp.status_code, 403)
401+
402+
def test_auditor_cannot_delete(self):
403+
update_id = self._create_update(self.staff_client).json()['id']
404+
resp = self.auditor_client.delete(self.create_update_url(update_id))
405+
self.assertEqual(resp.status_code, 403)
406+
407+
# -- Unauthorized user: no access --
408+
409+
def test_unauthorized_user_cannot_get(self):
410+
resp = self.unauthorized_client.get_json(self.create_update_url())
411+
self.assertEqual(resp.status_code, 403)
412+
413+
def test_unauthorized_user_cannot_post(self):
414+
resp = self._create_update(self.unauthorized_client)
415+
self.assertEqual(resp.status_code, 403)
416+
417+
# -- Staff/superuser without authz role: access via enforcer admin check --
418+
419+
def test_django_staff_without_role_can_get(self):
420+
staff_user = UserFactory(is_staff=True)
421+
client = self._make_client_for_user(staff_user)
422+
resp = client.get_json(self.create_update_url())
423+
self.assertEqual(resp.status_code, 200)
424+
425+
def test_django_staff_without_role_can_post(self):
426+
staff_user = UserFactory(is_staff=True)
427+
client = self._make_client_for_user(staff_user)
428+
resp = self._create_update(client)
429+
self.assertEqual(resp.status_code, 200)
430+
431+
def test_superuser_without_role_can_get(self):
432+
superuser = UserFactory(is_superuser=True)
433+
client = self._make_client_for_user(superuser)
434+
resp = client.get_json(self.create_update_url())
435+
self.assertEqual(resp.status_code, 200)
436+
437+
def test_superuser_without_role_can_post(self):
438+
superuser = UserFactory(is_superuser=True)
439+
client = self._make_client_for_user(superuser)
440+
resp = self._create_update(client)
441+
self.assertEqual(resp.status_code, 200)

0 commit comments

Comments
 (0)