diff --git a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py index 357a4375552f..bee1070ce577 100644 --- a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py +++ b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py @@ -13,6 +13,7 @@ from opaque_keys.edx.keys import CourseKey from contentstore.tests.utils import AjaxEnabledTestClient, parse_json +from student.models import CourseAccessRole from student.roles import CourseInstructorRole, CourseStaffRole from student.tests.factories import UserFactory from util.organizations_helpers import add_organization, get_course_organizations @@ -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 ea3bf9980650..2cd903320301 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -26,6 +26,7 @@ OrgLibraryUserRole, OrgStaffRole ) +from student.models import CourseAccessRole from student.tests.factories import UserFactory from xblock_django.user_service import DjangoXBlockUserService from xmodule.modulestore import ModuleStoreEnum @@ -813,6 +814,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 dd541849d69a..cdafd3457920 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -810,6 +810,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 8fc60dc9ad36..953b162ceb66 100644 --- a/cms/djangoapps/contentstore/views/library.py +++ b/cms/djangoapps/contentstore/views/library.py @@ -152,6 +152,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 8cbc2ec631e1..5332bf4fb1fd 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -405,6 +405,21 @@ # .. toggle_tickets: https://openedx.atlassian.net/browse/DEPR-58 # .. toggle_status: supported 'DEPRECATE_OLD_COURSE_KEYS_IN_STUDIO': True, + + # .. 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 1215972b2819..5748648fe7a6 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):