diff --git a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py index b819d504ffc2..bd83edf4137d 100644 --- a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py +++ b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py @@ -14,6 +14,7 @@ from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, parse_json from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole +from common.djangoapps.student.models import CourseAccessRole from common.djangoapps.student.tests.factories import UserFactory from common.djangoapps.util.organizations_helpers import add_organization, get_course_organizations from xmodule.course_module import CourseFields @@ -34,6 +35,9 @@ def setUp(self): """ super(TestCourseListing, self).setUp() # create and log in a staff user. + self.admin_user = UserFactory(is_staff=True) + self.admin_client = AjaxEnabledTestClient() + self.admin_client.login(username=self.admin_user.username, password='test') # create and log in a non-staff user self.user = UserFactory() self.factory = RequestFactory() @@ -64,6 +68,7 @@ def tearDown(self): Reverse the setup """ self.client.logout() + self.admin_client.logout() ModuleStoreTestCase.tearDown(self) @patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True}) @@ -176,3 +181,81 @@ def test_course_creation_with_org_in_system(self, store): course_orgs = get_course_organizations(new_course_key) self.assertEqual(len(course_orgs), 1) self.assertEqual(course_orgs[0]['short_name'], 'orgX') + + @patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True}) + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) + def test_course_creation_when_user_not_in_org(self, store): + """ + Tests course creation with restriction and user not registered in CourseAccessRole. + """ + with modulestore().default_store(store): + response = self.client.ajax_post(self.course_create_rerun_url, { + 'org': 'TestorgX', + 'number': 'CS101', + 'display_name': 'Course with web certs enabled', + 'run': '2021_T1' + }) + self.assertEqual(response.status_code, 400) + data = parse_json(response) + self.assertEqual( + data["error"], + 'User does not have the permission to create courses in this organization' + ) + + @patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True}) + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) + def test_course_creation_when_user_in_org(self, store): + """ + Tests course creation with restriction and user registered as staff. + """ + staff_role = 'staff' + CourseAccessRole.objects.create( + org='TestorgX', role=staff_role, user=self.user + ) + with modulestore().default_store(store): + response = self.client.ajax_post(self.course_create_rerun_url, { + 'org': 'TestorgX', + 'number': 'CS101', + 'display_name': 'Course with web certs enabled', + 'run': '2021_T1' + }) + self.assertEqual(response.status_code, 200) + + @patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True}) + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) + def test_course_creation_when_user_in_org_with_non_access_role(self, store): + """ + Tests course creation with restriction and user registered as role who doesn't have the access. + """ + staff_role = 'finance_admin' + CourseAccessRole.objects.create( + org='Stark', role=staff_role, user=self.user + ) + with modulestore().default_store(store): + response = self.client.ajax_post(self.course_create_rerun_url, { + 'org': 'Stark', + 'number': 'AV101', + 'display_name': 'Build Iron Man Suit', + 'run': '2021_T1' + }) + self.assertEqual(response.status_code, 400) + data = parse_json(response) + self.assertEqual( + data["error"], + 'User does not have the permission to create courses in this organization' + ) + + @patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True}) + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) + def test_course_creation_when_user_is_global_staff(self, store): + """ + Tests course creation with restriction and user is global staff. + """ + with modulestore().default_store(store): + response = self.admin_client.ajax_post(self.course_create_rerun_url, { + 'org': 'Oscorp', + 'number': 'SP101', + 'display_name': 'Making better web', + 'run': '2021_T1' + }) + self.assertEqual(response.status_code, 200) diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py index f38776491659..52ed6fa2a368 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -26,6 +26,7 @@ OrgLibraryUserRole, OrgStaffRole ) +from common.djangoapps.student.models import CourseAccessRole from common.djangoapps.student.tests.factories import UserFactory from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from xmodule.modulestore import ModuleStoreEnum @@ -829,6 +830,77 @@ def _get_settings_html(): self.assertNotIn('admin_lib_2', non_staff_settings_html) + @patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True}) + def test_library_creation_when_user_is_global_staff(self): + """ + Tests course creation with restriction and user is global staff. + """ + self._login_as_staff_user() + response = self.client.ajax_post(LIBRARY_REST_URL, { + 'org': 'Oscorp', + 'library': 'CentralLibrary', + 'display_name': 'Making better web', + }) + self.assertEqual(response.status_code, 200) + + @patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True}) + def test_library_creation_with_normaL_user_with_no_role(self): + """ + Tests course creation with restriction and user is not a global staff. + """ + self._login_as_non_staff_user() + response = self.client.ajax_post(LIBRARY_REST_URL, { + 'org': 'Stark', + 'library': 'AvengerLibrary', + 'display_name': 'Alien Science', + }) + self.assertEqual(response.status_code, 400) + data = parse_json(response) + self.assertEqual( + data["ErrMsg"], + "User does not have the permission to create library in this organization" + ) + + @patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True}) + def test_library_creation_with_normaL_user_with_non_access_role(self): + """ + Tests course creation with restriction and user doesn't have access role for org. + """ + staff_role = "finance_admin" + self._login_as_non_staff_user() + CourseAccessRole.objects.create( + org='Stark', role=staff_role, user=self.non_staff_user + ) + response = self.client.ajax_post(LIBRARY_REST_URL, { + 'org': 'Stark', + 'library': 'AvengerLibrary', + 'display_name': 'Alien Science', + }) + self.assertEqual(response.status_code, 400) + data = parse_json(response) + self.assertEqual( + data["ErrMsg"], + "User does not have the permission to create library in this organization" + ) + + @patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True}) + def test_library_creation_with_normaL_user_with_role(self): + """ + Tests course creation with restriction and user has role access. + """ + staff_role = "instructor" + self._login_as_non_staff_user() + CourseAccessRole.objects.create( + org='Stark', role=staff_role, user=self.non_staff_user + ) + response = self.client.ajax_post(LIBRARY_REST_URL, { + 'org': 'Stark', + 'library': 'AvengerLibrary', + 'display_name': 'Alien Science', + }) + self.assertEqual(response.status_code, 200) + + @ddt.ddt @override_settings(SEARCH_ENGINE=None) class TestOverrides(LibraryTestCase): diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index afb4b7c9558f..4183ccc6425a 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -831,6 +831,20 @@ def _create_or_rerun_course(request): status=400 ) + # Allow user to create the course only if they belong to the organisation + # This flag doesn't apply to Global Staff and Superusers + if settings.FEATURES.get('RESTRICT_COURSE_CREATION_TO_ORG_ROLES', False): + has_org_permission = has_studio_write_access(request.user, None, org) + if not has_org_permission: + log.exception( + "User does not have the permission to create course in this organization." + "User: {} Org: {} Course: {}".format(request.user.id, org, course) + ) + return JsonResponse( + {'error': _('User does not have the permission to create courses in this organization')}, + status=400 + ) + fields = {'start': start} if display_name is not None: fields['display_name'] = display_name diff --git a/cms/djangoapps/contentstore/views/library.py b/cms/djangoapps/contentstore/views/library.py index 0b9a38543a07..8cb496d4e1a5 100644 --- a/cms/djangoapps/contentstore/views/library.py +++ b/cms/djangoapps/contentstore/views/library.py @@ -186,6 +186,18 @@ def _create_library(request): library = request.json.get('number', None) if library is None: library = request.json['library'] + # Allow user to create libraries only if they belong to the organization + # This flag doesn't apply to Global Staff and Superusers + if settings.FEATURES.get('RESTRICT_COURSE_CREATION_TO_ORG_ROLES', False): + has_org_permission = has_studio_write_access(request.user, None, org) + if not has_org_permission: + log.exception( + "User does not have the permission to create library in this organization." + "User: {} Org: {} Library: {}".format(request.user.id, org, library) + ) + return JsonResponseBadRequest({ + "ErrMsg": _(u"User does not have the permission to create library in this organization") + }) store = modulestore() with store.default_store(ModuleStoreEnum.Type.split): new_lib = store.create_library( diff --git a/cms/envs/common.py b/cms/envs/common.py index 75850ebb9d6f..9c7de911d714 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -410,6 +410,21 @@ # .. toggle_warnings: Also set settings.LIBRARY_AUTHORING_MICROFRONTEND_URL and see # REDIRECT_TO_LIBRARY_AUTHORING_MICROFRONTEND for rollout. 'ENABLE_LIBRARY_AUTHORING_MICROFRONTEND': False, + + # .. toggle_name: RESTRICT_COURSE_CREATION_TO_ORG_ROLES + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Restricts users from creating courses/libraries in organisations + # which they don't belong to. This flag doesn't apply to Global Staff and Superusers. + # To enable, set to True. + # To disable, set to False. + # .. toggle_category: n/a + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2021-06-23 + # .. toggle_expiration_date: None + # .. toggle_status: supported + 'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': False + } ENABLE_JASMINE = False diff --git a/common/djangoapps/student/auth.py b/common/djangoapps/student/auth.py index 21c476098c16..0d29b4d867a6 100644 --- a/common/djangoapps/student/auth.py +++ b/common/djangoapps/student/auth.py @@ -100,7 +100,7 @@ def get_user_permissions(user, course_key, org=None): return STUDIO_NO_PERMISSIONS -def has_studio_write_access(user, course_key): +def has_studio_write_access(user, course_key, org=None): """ Return True if user has studio write access to the given course. Note that the CMS permissions model is with respect to courses. @@ -112,8 +112,9 @@ def has_studio_write_access(user, course_key): :param user: :param course_key: a CourseKey + :param org: name of organisation """ - return bool(STUDIO_EDIT_CONTENT & get_user_permissions(user, course_key)) + return bool(STUDIO_EDIT_CONTENT & get_user_permissions(user, course_key, org)) def has_course_author_access(user, course_key):