From 1e2ed4075925b63fa88db0199e14a91fe9eaeed5 Mon Sep 17 00:00:00 2001 From: Farhaan Bukhsh Date: Tue, 23 Feb 2021 02:50:23 +0530 Subject: [PATCH] [BB-3622] Restrict user create course (#319) * Add course creation condition for organization The condition added to ensure that if the feature is enabled user will not be able to create the course outside of the organization in which they belong. Signed-off-by: Farhaan Bukhsh --- .../tests/test_course_create_rerun.py | 83 +++++++++++++++++++ .../contentstore/tests/test_libraries.py | 72 ++++++++++++++++ cms/djangoapps/contentstore/views/course.py | 14 ++++ cms/djangoapps/contentstore/views/library.py | 12 +++ cms/envs/common.py | 15 ++++ common/djangoapps/student/auth.py | 5 +- 6 files changed, 199 insertions(+), 2 deletions(-) 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 df1d783aa639..fa96924dc35d 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -407,6 +407,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):