Skip to content

Commit

Permalink
feat: grant course/library creation rights by organization (openedx#2…
Browse files Browse the repository at this point in the history
…6616)

feat: grant course/library creation rights by the organization (openedx#26616)

This reverts commit 5ca2ce2.

Current State (before this commit):

  Studio, as of today doesn't have a way to restrict a user to
  create a course in a particular organization. What Studio
  provides right now is a CourseCreator permission which gives
  an Admin the power to grant a user the permission to create
  a course.

  For example: If the Admin has given a user Spiderman the
  permission to create courses, Spiderman can now create courses
  in any organization i.e Marvel as well as DC.
  There is no way to restrict Spiderman from creating courses
  under DC.

Purpose of this commit:

  The changes done here gives Admin the ability to restrict a
  user on an Organization level from creating courses via the
  Course Creators section of the Studio Django administration
  panel.

  For example: Now, the Admin can give the user Spiderman the
  privilege of creating courses only under Marvel organization.
  The moment Spiderman tries to create a course under some
  other organization(i.e DC), Studio will show an error message.

  This change is available to all Studio instances that
  enable the FEATURES['ENABLE_CREATOR_GROUP'] flag.
  Regardless of the flag, it will not affect any instances that choose
  not to use it.

BB-3622


Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
  • Loading branch information
farhaanbukhsh committed Oct 4, 2021
1 parent b98bcd0 commit a85f2d0
Show file tree
Hide file tree
Showing 16 changed files with 447 additions and 225 deletions.
163 changes: 114 additions & 49 deletions cms/djangoapps/contentstore/tests/test_course_create_rerun.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,37 @@


import datetime
from unittest import mock

import ddt
import six
from django.contrib.admin.sites import AdminSite
from django.http import HttpRequest
from django.test import override_settings
from django.test.client import RequestFactory
from django.urls import reverse
from mock import patch
from opaque_keys.edx.keys import CourseKey

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 organizations.api import add_organization, get_course_organizations, get_organization_by_short_name
from organizations.models import Organization
from xmodule.course_module import CourseFields
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory

from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, parse_json
from cms.djangoapps.course_creators.admin import CourseCreatorAdmin
from cms.djangoapps.course_creators.models import CourseCreator
from common.djangoapps.student.auth import update_org_role
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, OrgContentCreatorRole
from common.djangoapps.student.tests.factories import AdminFactory, UserFactory


def mock_render_to_string(template_name, context):
"""Return a string that encodes template_name and context"""
return str((template_name, context))


@ddt.ddt
class TestCourseListing(ModuleStoreTestCase):
Expand All @@ -35,12 +47,10 @@ 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()
self.global_admin = AdminFactory()
self.client = AjaxEnabledTestClient()
self.client.login(username=self.user.username, password='test')
self.course_create_rerun_url = reverse('course_handler')
Expand All @@ -60,6 +70,12 @@ def setUp(self):
)
self.source_course_key = source_course.id

self.course_creator_entry = CourseCreator(user=self.user)
self.course_creator_entry.save()
self.request = HttpRequest()
self.request.user = self.global_admin
self.creator_admin = CourseCreatorAdmin(self.course_creator_entry, AdminSite())

for role in [CourseInstructorRole, CourseStaffRole]:
role(self.source_course_key).add_users(self.user)

Expand All @@ -68,7 +84,6 @@ 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})
Expand Down Expand Up @@ -182,11 +197,11 @@ def test_course_creation_with_org_in_system(self, store):
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})
@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': 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.
Tests course creation when user doesn't have the required role.
"""
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
Expand All @@ -195,67 +210,117 @@ def test_course_creation_when_user_not_in_org(self, store):
'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'
)
self.assertEqual(response.status_code, 403)

@patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True})
@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_creation_when_user_in_org(self, store):
def test_course_creation_when_user_in_org_with_creator_role(self, store):
"""
Tests course creation with restriction and user registered as staff.
Tests course creation with user having the organization content creation role.
"""
staff_role = 'staff'
CourseAccessRole.objects.create(
org='TestorgX', role=staff_role, user=self.user
)
add_organization({
'name': 'Test Organization',
'short_name': self.source_course_key.org,
'description': 'Testing Organization Description',
})
update_org_role(self.global_admin, OrgContentCreatorRole, self.user, [self.source_course_key.org])
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': 'TestorgX',
'org': self.source_course_key.org,
'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})
@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_creation_when_user_in_org_with_non_access_role(self, store):
@mock.patch(
'cms.djangoapps.course_creators.admin.render_to_string',
mock.Mock(side_effect=mock_render_to_string, autospec=True)
)
def test_course_creation_with_all_org_checked(self, store):
"""
Tests course creation with restriction and user registered as role who doesn't have the access.
Tests course creation with user having permission to create course for all organization.
"""
staff_role = 'finance_admin'
CourseAccessRole.objects.create(
org='Stark', role=staff_role, user=self.user
)
add_organization({
'name': 'Test Organization',
'short_name': self.source_course_key.org,
'description': 'Testing Organization Description',
})
self.course_creator_entry.all_organizations = True
self.course_creator_entry.state = CourseCreator.GRANTED
self.creator_admin.save_model(self.request, self.course_creator_entry, None, True)
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',
'org': self.source_course_key.org,
'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'
)
self.assertEqual(response.status_code, 200)

@patch.dict('django.conf.settings.FEATURES', {'RESTRICT_COURSE_CREATION_TO_ORG_ROLES': True})
@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_creation_when_user_is_global_staff(self, store):
@mock.patch(
'cms.djangoapps.course_creators.admin.render_to_string',
mock.Mock(side_effect=mock_render_to_string, autospec=True)
)
def test_course_creation_with_permission_for_specific_organization(self, store):
"""
Tests course creation with restriction and user is global staff.
Tests course creation with user having permission to create course for specific organization.
"""
add_organization({
'name': 'Test Organization',
'short_name': self.source_course_key.org,
'description': 'Testing Organization Description',
})
self.course_creator_entry.all_organizations = False
self.course_creator_entry.state = CourseCreator.GRANTED
self.creator_admin.save_model(self.request, self.course_creator_entry, None, True)
dc_org_object = Organization.objects.get(name='Test Organization')
self.course_creator_entry.organizations.add(dc_org_object)
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',
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': self.source_course_key.org,
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2021_T1'
})
self.assertEqual(response.status_code, 200)

@override_settings(FEATURES={'ENABLE_CREATOR_GROUP': True})
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@mock.patch(
'cms.djangoapps.course_creators.admin.render_to_string',
mock.Mock(side_effect=mock_render_to_string, autospec=True)
)
def test_course_creation_without_permission_for_specific_organization(self, store):
"""
Tests course creation with user not having permission to create course for specific organization.
"""
add_organization({
'name': 'Test Organization',
'short_name': self.source_course_key.org,
'description': 'Testing Organization Description',
})
add_organization({
'name': 'DC',
'short_name': 'DC',
'description': 'DC Comics',
})
self.course_creator_entry.all_organizations = False
self.course_creator_entry.state = CourseCreator.GRANTED
self.creator_admin.save_model(self.request, self.course_creator_entry, None, True)
# User has been given the permission to create course under `DC` organization.
# When the user tries to create course under `Test Organization` it throws a 403.
dc_org_object = Organization.objects.get(name='DC')
self.course_creator_entry.organizations.add(dc_org_object)
with modulestore().default_store(store):
response = self.client.ajax_post(self.course_create_rerun_url, {
'org': self.source_course_key.org,
'number': 'CS101',
'display_name': 'Course with web certs enabled',
'run': '2021_T1'
})
self.assertEqual(response.status_code, 403)
72 changes: 0 additions & 72 deletions cms/djangoapps/contentstore/tests/test_libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
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
Expand Down Expand Up @@ -830,77 +829,6 @@ 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):
Expand Down

0 comments on commit a85f2d0

Please sign in to comment.