From 2e17fb811774d0c2d9189aa825bebe2b24d31ef7 Mon Sep 17 00:00:00 2001 From: Jillian Date: Thu, 16 Jan 2025 04:02:37 +1030 Subject: [PATCH] fix: allow_to_create_new_org checks org autocreate [FC-0076] (#36094) Updates the StudioHome API's allow_to_create_new_org to require both organization-creation permissions and ORGANIZATION_AUTOCREATE to be enabled. It also adds the list of "allowed organizations for libraries" to the Studio Home API so that the Authoring MFE can use it. (cherry picked from commit b96a3bf249ecc104b4882e67738d06499c1a0245) --- .../rest_api/v1/serializers/home.py | 4 ++ .../contentstore/rest_api/v1/views/home.py | 9 ++- .../rest_api/v1/views/tests/test_home.py | 14 ++++- .../tests/test_content_libraries.py | 58 +++++++++++++++++++ .../djangoapps/content_libraries/views.py | 12 ++++ 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index fa2a651f8a28..2f4eb770afad 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -50,6 +50,10 @@ class StudioHomeSerializer(serializers.Serializer): child=serializers.CharField(), allow_empty=True ) + allowed_organizations_for_libraries = serializers.ListSerializer( + child=serializers.CharField(), + allow_empty=True + ) archived_courses = CourseCommonSerializer(required=False, many=True) can_access_advanced_settings = serializers.BooleanField() can_create_organizations = serializers.BooleanField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index 3de536d78092..2d18a3e0b92e 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView +from organizations import api as org_api from openedx.core.lib.api.view_utils import view_auth_classes from ....utils import get_home_context, get_course_context, get_library_context @@ -51,6 +52,7 @@ def get(self, request: Request): "allow_to_create_new_org": true, "allow_unicode_course_id": false, "allowed_organizations": [], + "allowed_organizations_for_libraries": [], "archived_courses": [], "can_access_advanced_settings": true, "can_create_organizations": true, @@ -80,7 +82,12 @@ def get(self, request: Request): home_context = get_home_context(request, True) home_context.update({ - 'allow_to_create_new_org': settings.FEATURES.get('ENABLE_CREATOR_GROUP', True) and request.user.is_staff, + # 'allow_to_create_new_org' is actually about auto-creating organizations + # (e.g. when creating a course or library), so we add an additional test. + 'allow_to_create_new_org': ( + home_context['can_create_organizations'] and + org_api.is_autocreate_enabled() + ), 'studio_name': settings.STUDIO_NAME, 'studio_short_name': settings.STUDIO_SHORT_NAME, 'studio_request_email': settings.FEATURES.get('STUDIO_REQUEST_EMAIL', ''), diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index 9a3d2c24d8f5..17839a1aa6f6 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -37,9 +37,10 @@ def setUp(self): self.url = reverse("cms.djangoapps.contentstore:v1:home") self.expected_response = { "allow_course_reruns": True, - "allow_to_create_new_org": False, + "allow_to_create_new_org": True, "allow_unicode_course_id": False, "allowed_organizations": [], + "allowed_organizations_for_libraries": [], "archived_courses": [], "can_access_advanced_settings": True, "can_create_organizations": True, @@ -84,6 +85,17 @@ def test_home_page_studio_with_meilisearch_enabled(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(expected_response, response.data) + @override_settings(ORGANIZATIONS_AUTOCREATE=False) + def test_home_page_studio_with_org_autocreate_disabled(self): + """Check response content when Organization autocreate is disabled""" + response = self.client.get(self.url) + + expected_response = self.expected_response + expected_response["allow_to_create_new_org"] = False + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data) + def test_taxonomy_list_link(self): response = self.client.get(self.url) self.assertTrue(response.data['taxonomies_enabled']) diff --git a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py index 34eb7c4f34fc..aaac63375453 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py @@ -8,6 +8,7 @@ import ddt from django.contrib.auth.models import Group +from django.test import override_settings from django.test.client import Client from freezegun import freeze_time from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2 @@ -140,6 +141,63 @@ def test_library_validation(self): 'slug': ['Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or hyphens.'], } + def test_library_org_validation(self): + """ + Staff users can create libraries in any existing or auto-created organization. + """ + assert Organization.objects.filter(short_name='auto-created-org').count() == 0 + self._create_library(slug="auto-created-org-1", title="Library in an auto-created org", org='auto-created-org') + assert Organization.objects.filter(short_name='auto-created-org').count() == 1 + self._create_library(slug="existing-org-1", title="Library in an existing org", org="CL-TEST") + + @patch( + "openedx.core.djangoapps.content_libraries.views.user_can_create_organizations", + ) + @patch( + "openedx.core.djangoapps.content_libraries.views.get_allowed_organizations_for_libraries", + ) + @override_settings(ORGANIZATIONS_AUTOCREATE=False) + def test_library_org_no_autocreate(self, mock_get_allowed_organizations, mock_can_create_organizations): + """ + When org auto-creation is disabled, user must use one of their allowed orgs. + """ + mock_can_create_organizations.return_value = False + mock_get_allowed_organizations.return_value = ["CL-TEST"] + assert Organization.objects.filter(short_name='auto-created-org').count() == 0 + response = self._create_library( + slug="auto-created-org-2", + org="auto-created-org", + title="Library in an auto-created org", + expect_response=400, + ) + assert response == { + 'org': "No such organization 'auto-created-org' found.", + } + + Organization.objects.get_or_create( + short_name="not-allowed-org", + defaults={"name": "Content Libraries Test Org Membership"}, + ) + response = self._create_library( + slug="not-allowed-org", + org="not-allowed-org", + title="Library in an not-allowed org", + expect_response=400, + ) + assert response == { + 'org': "User not allowed to create libraries in 'not-allowed-org'.", + } + assert mock_can_create_organizations.call_count == 1 + assert mock_get_allowed_organizations.call_count == 1 + + self._create_library( + slug="allowed-org-2", + org="CL-TEST", + title="Library in an allowed org", + ) + assert mock_can_create_organizations.call_count == 2 + assert mock_get_allowed_organizations.call_count == 2 + @skip("This endpoint shouldn't support num_blocks and has_unpublished_*.") @patch("openedx.core.djangoapps.content_libraries.views.LibraryRootView.pagination_class.page_size", new=2) def test_list_library(self): diff --git a/openedx/core/djangoapps/content_libraries/views.py b/openedx/core/djangoapps/content_libraries/views.py index 48fe49144194..27bae691de19 100644 --- a/openedx/core/djangoapps/content_libraries/views.py +++ b/openedx/core/djangoapps/content_libraries/views.py @@ -99,6 +99,10 @@ from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet +from cms.djangoapps.contentstore.views.course import ( + get_allowed_organizations_for_libraries, + user_can_create_organizations, +) from openedx.core.djangoapps.content_libraries import api, permissions from openedx.core.djangoapps.content_libraries.serializers import ( ContentLibraryBlockImportTaskCreateSerializer, @@ -271,6 +275,14 @@ def post(self, request): raise ValidationError( # lint-amnesty, pylint: disable=raise-missing-from detail={"org": f"No such organization '{org_name}' found."} ) + # Ensure the user is allowed to create libraries under this org + if not ( + user_can_create_organizations(request.user) or + org_name in get_allowed_organizations_for_libraries(request.user) + ): + raise ValidationError( # lint-amnesty, pylint: disable=raise-missing-from + detail={"org": f"User not allowed to create libraries in '{org_name}'."} + ) org = Organization.objects.get(short_name=org_name) try: