Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

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

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

Expand All @@ -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):
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):

@mariajgrimaldi mariajgrimaldi Aug 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the edit (POST) in group_configurations_list_handler ever tested?

"""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
21 changes: 20 additions & 1 deletion cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Comment on lines +1934 to +1937

@mariajgrimaldi mariajgrimaldi Aug 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to block this PR but I'm a bit concerned about adding more and more code branches when checking for permissions. I understand it's best to be surgical about the changes to not break other unrelated code sections but I'm not sure about the maintainability long-term. At least we should ensure that all the branches are thoroughly tested. Can we review testing and make sure we are?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
return redirect(get_group_configurations_url(course_key))
Expand Down
Loading