diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/group_configurations.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/group_configurations.py index fefeac8e748e..7b7fe132beca 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/group_configurations.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/group_configurations.py @@ -57,3 +57,4 @@ class CourseGroupConfigurationsSerializer(serializers.Serializer): ) should_show_enrollment_track = serializers.BooleanField() should_show_experiment_groups = serializers.BooleanField() + can_manage = serializers.BooleanField(default=False, read_only=True) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/group_configurations.py b/cms/djangoapps/contentstore/rest_api/v1/views/group_configurations.py index 4de2dd8a16b7..f6373d3febaa 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/group_configurations.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/group_configurations.py @@ -2,7 +2,7 @@ import edx_api_doc_tools as apidocs from opaque_keys.edx.keys import CourseKey -from openedx_authz.constants.permissions import COURSES_MANAGE_GROUP_CONFIGURATIONS +from openedx_authz.constants.permissions import COURSES_MANAGE_GROUP_CONFIGURATIONS, COURSES_VIEW_GROUP_CONFIGURATIONS from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -10,7 +10,7 @@ from cms.djangoapps.contentstore.rest_api.v1.serializers import CourseGroupConfigurationsSerializer from cms.djangoapps.contentstore.utils import get_group_configurations_context from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission -from openedx.core.djangoapps.authz.decorators import authz_permission_required +from openedx.core.djangoapps.authz.decorators import authz_permission_required, 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 @@ -36,7 +36,7 @@ class CourseGroupConfigurationsView(DeveloperErrorViewMixin, APIView): ) @verify_course_exists() @authz_permission_required( - authz_permission=COURSES_MANAGE_GROUP_CONFIGURATIONS.identifier, + authz_permission=COURSES_VIEW_GROUP_CONFIGURATIONS.identifier, legacy_permission=LegacyAuthoringPermission.READ ) def get(self, request: Request, course_key: CourseKey): @@ -144,5 +144,11 @@ def get(self, request: Request, course_key: CourseKey): with store.bulk_operations(course_key): course = modulestore().get_course(course_key) group_configurations_context = get_group_configurations_context(course, store) + group_configurations_context['can_manage'] = user_has_course_permission( + request.user, + COURSES_MANAGE_GROUP_CONFIGURATIONS.identifier, + course_key, + LegacyAuthoringPermission.WRITE + ) serializer = CourseGroupConfigurationsSerializer(group_configurations_context) return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_group_configurations.py index 3db602dc42f3..21152920d7c1 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_group_configurations.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_group_configurations.py @@ -2,7 +2,7 @@ Unit tests for the course's setting group configuration. """ from django.urls import reverse -from openedx_authz.constants.roles import COURSE_DATA_RESEARCHER, COURSE_STAFF +from openedx_authz.constants.roles import COURSE_AUDITOR, COURSE_DATA_RESEARCHER, COURSE_EDITOR, COURSE_STAFF from rest_framework import status from rest_framework.test import APIClient @@ -111,3 +111,31 @@ def test_non_staff_user_cannot_access(self): resp = non_staff_client.get(self.get_url(self.course_key)) self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) # noqa: PT009 + + def test_staff_has_can_manage_true(self): + """User with COURSE_STAFF role gets can_manage=True in response.""" + resp = self.authorized_client.get(self.get_url(self.course_key)) + assert resp.status_code == status.HTTP_200_OK + assert resp.data["can_manage"] is True + + def test_editor_can_view_group_configurations(self): + """User with COURSE_EDITOR role can view group configurations (has view_group_configurations).""" + editor_user = UserFactory() + editor_client = APIClient() + self.add_user_to_role(editor_user, COURSE_EDITOR.external_key) + editor_client.force_authenticate(user=editor_user) + + resp = editor_client.get(self.get_url(self.course_key)) + assert resp.status_code == status.HTTP_200_OK + assert resp.data["can_manage"] is True # editor has manage_group_configurations + + def test_auditor_can_view_group_configurations(self): + """User with COURSE_AUDITOR role can view group configurations (has view_group_configurations).""" + auditor_user = UserFactory() + auditor_client = APIClient() + self.add_user_to_role(auditor_user, COURSE_AUDITOR.external_key) + auditor_client.force_authenticate(user=auditor_user) + + resp = auditor_client.get(self.get_url(self.course_key)) + assert resp.status_code == status.HTTP_200_OK + assert resp.data["can_manage"] is False # auditor has view only diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 65bdeb0bc883..b4c8a24fe898 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -42,6 +42,7 @@ COURSES_PUBLISH_COURSE_CONTENT, COURSES_VIEW_COURSE, COURSES_VIEW_COURSE_UPDATES, + COURSES_VIEW_GROUP_CONFIGURATIONS, COURSES_VIEW_PAGES_AND_RESOURCES, ) from organizations.api import add_organization_course, ensure_organization @@ -185,6 +186,21 @@ def get_course_and_check_manage_group_configurations_access(course_key, user, de return _get_course_block(course_key, depth) +def get_course_and_check_view_group_configurations_access(course_key, user, depth=0): + """ + Function used to validate read permission and return a course block + for group configurations list/detail GET requests. + """ + if not user_has_course_permission( + user=user, + authz_permission=COURSES_VIEW_GROUP_CONFIGURATIONS.identifier, + course_key=course_key, + legacy_permission=LegacyAuthoringPermission.READ + ): + raise PermissionDenied() + return _get_course_block(course_key, depth) + + def reindex_course_and_check_access(course_key, user): """ Internal method used to restart indexing on a course. @@ -1915,7 +1931,10 @@ def group_configurations_list_handler(request, course_key_string): course_key = CourseKey.from_string(course_key_string) store = modulestore() with store.bulk_operations(course_key): - course = get_course_and_check_manage_group_configurations_access(course_key, request.user) + if request.method == 'GET': + course = get_course_and_check_view_group_configurations_access(course_key, request.user) + else: + course = get_course_and_check_manage_group_configurations_access(course_key, request.user) if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): return redirect(get_group_configurations_url(course_key))