Skip to content

Commit 2678891

Browse files
authored
feat: add authz permission to search_reindex endpoint (#38348)
1 parent a74534c commit 2678891

7 files changed

Lines changed: 105 additions & 11 deletions

File tree

cms/djangoapps/contentstore/views/course.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
COURSES_MANAGE_COURSE_UPDATES,
3535
COURSES_MANAGE_GROUP_CONFIGURATIONS,
3636
COURSES_MANAGE_PAGES_AND_RESOURCES,
37+
COURSES_PUBLISH_COURSE_CONTENT,
3738
COURSES_VIEW_COURSE,
3839
COURSES_VIEW_COURSE_UPDATES,
3940
COURSES_VIEW_PAGES_AND_RESOURCES,
@@ -56,7 +57,6 @@
5657
from common.djangoapps.course_action_state.models import CourseRerunState, CourseRerunUIStateManager
5758
from common.djangoapps.edxmako.shortcuts import render_to_response
5859
from common.djangoapps.student.auth import (
59-
has_course_author_access,
6060
has_studio_advanced_settings_access,
6161
has_studio_read_access,
6262
has_studio_write_access,
@@ -191,7 +191,12 @@ def reindex_course_and_check_access(course_key, user):
191191
"""
192192
Internal method used to restart indexing on a course.
193193
"""
194-
if not has_course_author_access(user, course_key):
194+
if not user_has_course_permission(
195+
user=user,
196+
authz_permission=COURSES_PUBLISH_COURSE_CONTENT.identifier,
197+
course_key=course_key,
198+
legacy_permission=LegacyAuthoringPermission.WRITE
199+
):
195200
raise PermissionDenied()
196201
return CoursewareSearchIndexer.do_course_reindex(modulestore(), course_key)
197202

@@ -367,10 +372,13 @@ def course_search_index_handler(request, course_key_string):
367372
html: return status of indexing task
368373
json: return status of indexing task
369374
"""
370-
# Only global staff (PMs) are able to index courses
371-
if not GlobalStaff().has_user(request.user):
372-
raise PermissionDenied()
373375
course_key = CourseKey.from_string(course_key_string)
376+
is_authz_enabled = core_toggles.AUTHZ_COURSE_AUTHORING_FLAG.is_enabled(course_key)
377+
if not is_authz_enabled and not GlobalStaff().has_user(request.user):
378+
# When AuthZ is disabled, restrict to global staff (legacy behavior).
379+
# When AuthZ is enabled, access control is enforced by the AuthZ layer,
380+
# which includes staff/superuser checks and course-level permissions.
381+
raise PermissionDenied()
374382
content_type = request.META.get('CONTENT_TYPE', None)
375383
if content_type is None:
376384
content_type = "application/json; charset=utf-8"

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

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
import pytz
1212
from django.core.exceptions import PermissionDenied
1313
from django.utils.translation import gettext as _
14+
from openedx_authz.constants.roles import COURSE_STAFF
1415
from search.api import perform_search
1516

1617
from cms.djangoapps.contentstore.courseware_index import CoursewareSearchIndexer, SearchIndexingError
17-
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
18+
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase
1819
from cms.djangoapps.contentstore.utils import reverse_course_url, reverse_usage_url
1920
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import VisibilityState, create_xblock_info
2021
from common.djangoapps.student.tests.factories import UserFactory
22+
from openedx.core.djangoapps.authz.tests.mixins import CourseAuthoringAuthzTestMixin
2123
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
2224
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
2325
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
@@ -541,3 +543,86 @@ def test_indexing_no_item(self, mock_get_course):
541543
# Start manual reindex and check error in response
542544
with self.assertRaises(SearchIndexingError): # noqa: PT027
543545
CoursewareSearchIndexer.do_course_reindex(modulestore(), self.course.id)
546+
547+
548+
class TestCourseReIndexAuthz(CourseAuthoringAuthzTestMixin, CourseTestCase):
549+
"""
550+
AuthZ-based tests for course reindex.
551+
"""
552+
553+
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
554+
SUCCESSFUL_RESPONSE = _("Course has been successfully reindexed.")
555+
ENABLED_SIGNALS = ['course_published']
556+
557+
@mock.patch(
558+
'cms.djangoapps.contentstore.signals.handlers.transaction.on_commit',
559+
new=mock.Mock(side_effect=lambda func: func()),
560+
)
561+
def setUp(self):
562+
super().setUp()
563+
564+
self.url = reverse_course_url('course_search_index_handler', self.course.id)
565+
566+
self.non_staff_client = AjaxEnabledTestClient()
567+
self.non_staff_user, self.non_staff_password = self.create_non_staff_user()
568+
self.non_staff_client.login(username=self.non_staff_user.username, password=self.non_staff_password)
569+
570+
self.course.start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc)
571+
modulestore().update_item(self.course, self.user.id)
572+
573+
self.chapter = BlockFactory.create(
574+
parent_location=self.course.location,
575+
category='chapter',
576+
display_name="Week 1"
577+
)
578+
self.sequential = BlockFactory.create(
579+
parent_location=self.chapter.location,
580+
category='sequential',
581+
display_name="Lesson 1"
582+
)
583+
self.vertical = BlockFactory.create(
584+
parent_location=self.sequential.location,
585+
category='vertical',
586+
display_name='Subsection 1'
587+
)
588+
self.video = BlockFactory.create(
589+
parent_location=self.vertical.location,
590+
category="video",
591+
display_name="My Video"
592+
)
593+
self.html = BlockFactory.create(
594+
parent_location=self.vertical.location,
595+
category="html",
596+
display_name="My HTML",
597+
data="<div>This is my unique HTML content</div>",
598+
)
599+
600+
def test_staff_user_can_reindex(self):
601+
""" Verify that staff user can reindex the course. """
602+
603+
response = self.client.get(self.url, HTTP_ACCEPT='application/json')
604+
605+
assert self.user.is_staff
606+
assert response.status_code == 200
607+
assert self.SUCCESSFUL_RESPONSE in response.content.decode()
608+
609+
def test_non_staff_user_cannot_reindex(self):
610+
""" Verify that non-staff user without course authoring permissions cannot reindex the course. """
611+
response = self.non_staff_client.get(self.url, HTTP_ACCEPT='application/json')
612+
613+
assert not self.non_staff_user.is_staff
614+
assert response.status_code == 403
615+
616+
def test_non_staff_user_can_reindex(self):
617+
""" Verify that non-staff user with course authoring permissions can reindex the course. """
618+
619+
# Grant access helper
620+
self.add_user_to_role_in_course(
621+
self.non_staff_user,
622+
COURSE_STAFF.external_key,
623+
self.course.id
624+
)
625+
response = self.non_staff_client.get(self.url, HTTP_ACCEPT='application/json')
626+
assert not self.non_staff_user.is_staff
627+
assert response.status_code == 200
628+
assert self.SUCCESSFUL_RESPONSE in response.content.decode()

common/djangoapps/student/auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@ def _has_content_creator_access(user, org):
256256
"""
257257
if settings.FEATURES.get('DISABLE_COURSE_CREATION', False):
258258
return False
259-
org_scope_key = f"course-v1:{org}+*"
259+
# Using Org scope. e.g. "course-v1:{org}+*"
260+
org_scope_key = authz_api.OrgCourseOverviewGlobData.build_external_key(org)
260261

261262
return authz_api.is_user_allowed(
262263
user.username,

requirements/edx/base.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ openedx-atlas==0.7.0
828828
# enterprise-integrated-channels
829829
# openedx-authz
830830
# openedx-forum
831-
openedx-authz==1.5.0
831+
openedx-authz==1.11.0
832832
# via -r requirements/edx/kernel.in
833833
openedx-calc==5.0.0
834834
# via

requirements/edx/development.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1377,7 +1377,7 @@ openedx-atlas==0.7.0
13771377
# enterprise-integrated-channels
13781378
# openedx-authz
13791379
# openedx-forum
1380-
openedx-authz==1.5.0
1380+
openedx-authz==1.11.0
13811381
# via
13821382
# -r requirements/edx/doc.txt
13831383
# -r requirements/edx/testing.txt

requirements/edx/doc.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,7 @@ openedx-atlas==0.7.0
10051005
# enterprise-integrated-channels
10061006
# openedx-authz
10071007
# openedx-forum
1008-
openedx-authz==1.5.0
1008+
openedx-authz==1.11.0
10091009
# via -r requirements/edx/base.txt
10101010
openedx-calc==5.0.0
10111011
# via

requirements/edx/testing.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1052,7 +1052,7 @@ openedx-atlas==0.7.0
10521052
# enterprise-integrated-channels
10531053
# openedx-authz
10541054
# openedx-forum
1055-
openedx-authz==1.5.0
1055+
openedx-authz==1.11.0
10561056
# via -r requirements/edx/base.txt
10571057
openedx-calc==5.0.0
10581058
# via

0 commit comments

Comments
 (0)