def _assign_course_staff_role(course_key, enrollments, staff_assignments): """ Grant or remove the course staff role for a set of enrollments on a course. For enrollment without a linked user, a CourseAccessRoleAssignment will be created (or removed) for that enrollment. Arguments: enrollments (list): ProgramCourseEnrollments to update staff_assignments (dict): Maps an enrollment's external key to a course staff value """ enrollment_role_assignments_to_delete = [] for enrollment in enrollments: if enrollment.course_key != course_key: continue external_key = enrollment.program_enrollment.external_user_key user = enrollment.program_enrollment.user course_staff = staff_assignments.get(external_key) if user: if course_staff is True: CourseStaffRole(course_key).add_users(user) elif course_staff is False: CourseStaffRole(course_key).remove_users(user) else: if course_staff is True: CourseAccessRoleAssignment.objects.update_or_create( enrollment=enrollment, role=ProgramCourseEnrollmentRoles.COURSE_STAFF) elif course_staff is False: enrollment_role_assignments_to_delete.append(enrollment) if enrollment_role_assignments_to_delete: CourseAccessRoleAssignment.objects.filter( enrollment__in=enrollment_role_assignments_to_delete).delete()
def test_no_libraries(self): """ Verify that only Course IDs are returned, not anything else like libraries. """ # Make this user a course staff user for a course, AND a library. course_staff_user = self.create_user(username='******', is_staff=False) add_users(self.global_admin, CourseStaffRole(self.course.id), course_staff_user) add_users( self.global_admin, CourseStaffRole( LibraryLocator.from_string( 'library-v1:library_org+library_name')), course_staff_user, ) # Requesting the courses should return *only* courses and not libraries. self.setup_user(self.staff_user) filtered_response = self.verify_response( params={ 'username': course_staff_user.username, 'role': 'staff' }) self.assertEqual(len(filtered_response.data['results']), 1) self.assertTrue(filtered_response.data['results'][0].startswith( self.course.org))
def test_get_user_for_role(self): """ test users_for_role """ role = CourseStaffRole(self.course_key) role.add_users(self.student) assert len(role.users_with_role()) > 0
def test_filter_by_roles_course_staff(self): """ Verify that course_ids are filtered by the provided roles. """ # Make this user a course staff user for the course. course_staff_user = self.create_user(username='******', is_staff=False) add_users(self.global_admin, CourseStaffRole(self.course.id), course_staff_user) # Create a second course, along with an instructor user for it. alternate_course1 = self.create_course(org='test1') course_instructor_user = self.create_user(username='******', is_staff=False) add_users(self.global_admin, CourseInstructorRole(alternate_course1.id), course_instructor_user) # Create a third course, along with an user that has both staff and instructor for it. alternate_course2 = self.create_course(org='test2') course_instructor_staff_user = self.create_user(username='******', is_staff=False) add_users(self.global_admin, CourseInstructorRole(alternate_course2.id), course_instructor_staff_user) add_users(self.global_admin, CourseStaffRole(alternate_course2.id), course_instructor_staff_user) # Requesting the courses for which the course staff user is staff should return *only* the single course. self.setup_user(self.staff_user) filtered_response = self.verify_response(params={ 'username': course_staff_user.username, 'role': 'staff' }) assert len(filtered_response.data['results']) == 1 assert filtered_response.data['results'][0].startswith(self.course.org) # The course staff user does *not* have the course instructor role on any courses. filtered_response = self.verify_response(params={ 'username': course_staff_user.username, 'role': 'instructor' }) assert len(filtered_response.data['results']) == 0 # The course instructor user only has the course instructor role on one course. filtered_response = self.verify_response(params={ 'username': course_instructor_user.username, 'role': 'instructor' }) assert len(filtered_response.data['results']) == 1 assert filtered_response.data['results'][0].startswith(alternate_course1.org) # The course instructor user has the inferred course staff role on one course. self.setup_user(course_instructor_user) filtered_response = self.verify_response(params={ 'username': course_instructor_user.username, 'role': 'staff' }) assert len(filtered_response.data['results']) == 1 assert filtered_response.data['results'][0].startswith(alternate_course1.org) # The user with both instructor AND staff on a course has the inferred course staff role on that one course. self.setup_user(course_instructor_staff_user) filtered_response = self.verify_response(params={ 'username': course_instructor_staff_user.username, 'role': 'staff' }) assert len(filtered_response.data['results']) == 1 assert filtered_response.data['results'][0].startswith(alternate_course2.org)
def remove_all_instructors(course_key): """ Removes all instructor and staff users from the given course. """ staff_role = CourseStaffRole(course_key) staff_role.remove_users(*staff_role.users_with_role()) instructor_role = CourseInstructorRole(course_key) instructor_role.remove_users(*instructor_role.users_with_role())
def test_add_user_to_course_group_permission_denied(self): """ Verifies PermissionDenied if caller of add_user_to_course_group is not instructor role. """ add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator) add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator) with self.assertRaises(PermissionDenied): add_users(self.staff, CourseStaffRole(self.course_key), self.staff)
def test_remove_user_from_course_group_permission_denied(self): """ Verifies PermissionDenied if caller of remove_user_from_course_group is not instructor role. """ add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator) another_staff = User.objects.create_user('another', '*****@*****.**', 'foo') add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator, self.staff, another_staff) with self.assertRaises(PermissionDenied): remove_users(self.staff, CourseStaffRole(self.course_key), another_staff)
def make_staff(self): """ create staff user. """ staff = UserFactory.create(password="******") role = CourseStaffRole(self.course.id) role.add_users(staff) return staff
def test_update_and_assign_or_revoke_staff(self): """ Successfully updates existing enrollments to assign or revoke the CourseStaff role. """ course_staff_role = CourseStaffRole(self.course_id) course_staff_role.add_users(self.student_2) self.create_program_and_course_enrollments('learner-1', user=self.student_1) self.create_program_and_course_enrollments('learner-2', user=self.student_2) self.create_program_and_course_enrollments('learner-3', user=None) learner_4_enrollment = self.create_program_and_course_enrollments( 'learner-4', user=None) learner_5_enrollment = self.create_program_and_course_enrollments( 'learner-5', user=None) CourseAccessRoleAssignment.objects.create( enrollment=learner_4_enrollment, role=ProgramCourseEnrollmentRoles.COURSE_STAFF, ) CourseAccessRoleAssignment.objects.create( enrollment=learner_5_enrollment, role=ProgramCourseEnrollmentRoles.COURSE_STAFF, ) course_enrollment_requests = [ self.course_enrollment_request('learner-1', CourseStatuses.ACTIVE, True), self.course_enrollment_request('learner-2', CourseStatuses.ACTIVE, False), self.course_enrollment_request('learner-3', CourseStatuses.ACTIVE, True), self.course_enrollment_request('learner-4', CourseStatuses.ACTIVE, False), self.course_enrollment_request('learner-5', CourseStatuses.ACTIVE, True), ] write_program_course_enrollments( self.program_uuid, self.course_id, course_enrollment_requests, True, True, ) # Role is revoked for user's with a linked enrollment self.assertListEqual([self.student_1], list(course_staff_role.users_with_role())) # CourseAccessRoleAssignment objects are created/revoked for enrollments with no linked user pending_role_assingments = CourseAccessRoleAssignment.objects.all() assert pending_role_assingments.count() == 2 pending_role_assingments.get( enrollment__program_enrollment__external_user_key='learner-3', enrollment__course_key=self.course_id) pending_role_assingments.get( enrollment__program_enrollment__external_user_key='learner-5', enrollment__course_key=self.course_id)
def test_detail_delete_staff(self): auth.add_users(self.user, CourseStaffRole(self.course.id), self.ext_user) resp = self.client.delete( self.detail_url, HTTP_ACCEPT="application/json", ) self.assertEqual(resp.status_code, 204) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) self.assertFalse(auth.user_has_role(ext_user, CourseStaffRole(self.course.id)))
def test_add_users_doesnt_add_duplicate_entry(self): """ Tests that calling add_users multiple times before a single call to remove_users does not result in the user remaining in the group. """ role = CourseStaffRole(self.course_key) role.add_users(self.student) assert role.has_user(self.student) # Call add_users a second time, then remove just once. role.add_users(self.student) role.remove_users(self.student) assert not role.has_user(self.student)
def test_staff_cannot_delete_other(self): auth.add_users(self.user, CourseStaffRole(self.course.id), self.user, self.ext_user) self.user.is_staff = False self.user.save() resp = self.client.delete(self.detail_url) self.assertEqual(resp.status_code, 403) result = json.loads(resp.content.decode('utf-8')) self.assertIn("error", result) # reload user from DB ext_user = User.objects.get(email=self.ext_user.email) self.assertTrue(auth.user_has_role(ext_user, CourseStaffRole(self.course.id)))
def test_staff_can_delete_self(self): auth.add_users(self.user, CourseStaffRole(self.course.id), self.user) self.user.is_staff = False self.user.save() self_url = self.course_team_url(email=self.user.email) resp = self.client.delete(self_url) self.assertEqual(resp.status_code, 204) # reload user from DB user = User.objects.get(email=self.user.email) self.assertFalse(auth.user_has_role(user, CourseStaffRole(self.course.id)))
def setUp(self): super().setUp() self.course_staff = UserFactory.create() CourseStaffRole(self.verified_course.id).add_users(self.course_staff) CourseStaffRole(self.masters_course.id).add_users(self.course_staff) # Enroll the user in the two courses CourseEnrollmentFactory.create(user=self.course_staff, course_id=self.verified_course.id) CourseEnrollmentFactory.create(user=self.course_staff, course_id=self.masters_course.id) # Log the staff user in self.client.login(username=self.course_staff.username, password=TEST_PASSWORD)
def test_add_user_to_course_group(self): """ Tests adding user to course group (happy path). """ # Create groups for a new course (and assign instructor role to the creator). self.assertFalse(user_has_role(self.creator, CourseInstructorRole(self.course_key))) add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator) add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator) self.assertTrue(user_has_role(self.creator, CourseInstructorRole(self.course_key))) # Add another user to the staff role. self.assertFalse(user_has_role(self.staff, CourseStaffRole(self.course_key))) add_users(self.creator, CourseStaffRole(self.course_key), self.staff) self.assertTrue(user_has_role(self.staff, CourseStaffRole(self.course_key)))
def test_course_role(self): """ Test that giving a user a course role enables access appropriately """ assert not CourseStaffRole(self.course_key).has_user(self.student), \ f'Student has premature access to {self.course_key}' CourseStaffRole(self.course_key).add_users(self.student) assert CourseStaffRole(self.course_key).has_user(self.student), \ f"Student doesn't have access to {six.text_type(self.course_key)}" # remove access and confirm CourseStaffRole(self.course_key).remove_users(self.student) assert not CourseStaffRole(self.course_key).has_user(self.student), \ f'Student still has access to {self.course_key}'
def test_remove_user_from_course_group(self): """ Tests removing user from course group (happy path). """ add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator) add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator) add_users(self.creator, CourseStaffRole(self.course_key), self.staff) self.assertTrue(user_has_role(self.staff, CourseStaffRole(self.course_key))) remove_users(self.creator, CourseStaffRole(self.course_key), self.staff) self.assertFalse(user_has_role(self.staff, CourseStaffRole(self.course_key))) remove_users(self.creator, CourseInstructorRole(self.course_key), self.creator) self.assertFalse(user_has_role(self.creator, CourseInstructorRole(self.course_key)))
def setUp(self): super(MasqueradeTestBase, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments self.course_staff = UserFactory.create() CourseStaffRole(self.verified_course.id).add_users(self.course_staff) CourseStaffRole(self.masters_course.id).add_users(self.course_staff) # Enroll the user in the two courses CourseEnrollmentFactory.create(user=self.course_staff, course_id=self.verified_course.id) CourseEnrollmentFactory.create(user=self.course_staff, course_id=self.masters_course.id) # Log the staff user in self.client.login(username=self.course_staff.username, password=TEST_PASSWORD)
def test_create_enrollments_and_assign_staff(self, request_user_key_prefix): """ Successfully creates both waiting and linked program course enrollments with the course staff role. """ course_staff_role = CourseStaffRole(self.course_id) course_staff_role.add_users(self.student_1) self.create_program_enrollment('learner-1', user=None) self.create_program_enrollment('learner-2', user=self.student_1) self.create_program_enrollment('learner-3', user=self.student_2) course_enrollment_requests = [ self.course_enrollment_request( '{}-1'.format(request_user_key_prefix), CourseStatuses.ACTIVE, True), self.course_enrollment_request( '{}-2'.format(request_user_key_prefix), CourseStatuses.ACTIVE, True), self.course_enrollment_request( '{}-3'.format(request_user_key_prefix), CourseStatuses.ACTIVE, True), ] write_program_course_enrollments( self.program_uuid, self.course_id, course_enrollment_requests, True, True, ) self.assert_program_course_enrollment('learner-1', CourseStatuses.ACTIVE, False) self.assert_program_course_enrollment('learner-2', CourseStatuses.ACTIVE, True) self.assert_program_course_enrollment('learner-3', CourseStatuses.ACTIVE, True) # Users linked to either enrollment are given the course staff role self.assertListEqual([self.student_1, self.student_2], list(course_staff_role.users_with_role())) # CourseAccessRoleAssignment objects are created for enrollments with no linked user pending_role_assingments = CourseAccessRoleAssignment.objects.all() assert pending_role_assingments.count() == 1 pending_role_assingments.get( enrollment__program_enrollment__external_user_key='learner-1', enrollment__course_key=self.course_id)
def test_remove_user_from_course_group_permission_denied(self): """ Verifies PermissionDenied if caller of remove_user_from_course_group is not instructor role. """ add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator) another_staff = UserFactory.create( username='******', email='*****@*****.**', password='******', ) add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator, self.staff, another_staff) with pytest.raises(PermissionDenied): remove_users(self.staff, CourseStaffRole(self.course_key), another_staff)
def setUp(self): """ Add courses with the end date set to various values """ super().setUp() # Base course has no end date (so is active) self.course.end = None self.course.display_name = 'Active Course 1' self.ORG = self.course.location.org self.save_course() # Active course has end date set to tomorrow self.active_course = CourseFactory.create( display_name='Active Course 2', org=self.ORG, end=self.TOMORROW, ) # Archived course has end date set to yesterday self.archived_course = CourseFactory.create( display_name='Archived Course', org=self.ORG, end=self.YESTERDAY, ) # Base user has global staff access self.assertTrue(GlobalStaff().has_user(self.user)) # Staff user just has course staff access self.staff, self.staff_password = self.create_non_staff_user() for course in (self.course, self.active_course, self.archived_course): CourseStaffRole(course.id).add_users(self.staff)
def get_user_permissions(user, course_key, org=None): """ Get the bitmask of permissions that this user has in the given course context. Can also set course_key=None and pass in an org to get the user's permissions for that organization as a whole. """ if org is None: org = course_key.org course_key = course_key.for_branch(None) else: assert course_key is None # No one has studio permissions for CCX courses if is_ccx_course(course_key): return STUDIO_NO_PERMISSIONS all_perms = STUDIO_EDIT_ROLES | STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT # global staff, org instructors, and course instructors have all permissions: if GlobalStaff().has_user(user) or OrgInstructorRole(org=org).has_user(user): return all_perms if course_key and user_has_role(user, CourseInstructorRole(course_key)): return all_perms # Staff have all permissions except EDIT_ROLES: if OrgStaffRole(org=org).has_user(user) or (course_key and user_has_role(user, CourseStaffRole(course_key))): return STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT # Otherwise, for libraries, users can view only: if course_key and isinstance(course_key, LibraryLocator): if OrgLibraryUserRole(org=org).has_user(user) or user_has_role(user, LibraryUserRole(course_key)): return STUDIO_VIEW_USERS | STUDIO_VIEW_CONTENT return STUDIO_NO_PERMISSIONS
def test_no_duplicate_emails_staff_instructor(self): """ Test that no duplicate emails are sent to a course instructor that is also course staff """ CourseStaffRole(self.course.id).add_users(self.instructor) self.test_send_to_all()
def test_add_master_course_staff_to_ccx(self): """ Test add staff of master course to ccx course """ # adding staff to master course. staff = self.make_staff() assert CourseStaffRole(self.course.id).has_user(staff) # adding instructor to master course. instructor = self.make_instructor() assert CourseInstructorRole(self.course.id).has_user(instructor) add_master_course_staff_to_ccx(self.course, self.ccx_locator, self.ccx.display_name) # assert that staff and instructors of master course has staff and instructor roles on ccx list_staff_master_course = list_with_level(self.course.id, 'staff') list_instructor_master_course = list_with_level( self.course.id, 'instructor') with ccx_course(self.ccx_locator) as course_ccx: list_staff_ccx_course = list_with_level(course_ccx.id, 'staff') assert len(list_staff_master_course) == len(list_staff_ccx_course) assert list_staff_master_course[0].email == list_staff_ccx_course[ 0].email list_instructor_ccx_course = list_with_level( course_ccx.id, 'instructor') assert len(list_instructor_ccx_course) == len( list_instructor_master_course) assert list_instructor_ccx_course[ 0].email == list_instructor_master_course[0].email
def test_remove_master_course_staff_from_ccx_display_name(self): """ Test remove role of staff of master course on ccx course. Specific test to check that a passed display name is in the subject of the email sent to the unenrolled users. """ staff = self.make_staff() assert CourseStaffRole(self.course.id).has_user(staff) # adding instructor to master course. instructor = self.make_instructor() assert CourseInstructorRole(self.course.id).has_user(instructor) outbox = self.get_outbox() add_master_course_staff_to_ccx(self.course, self.ccx_locator, self.ccx.display_name, send_email=False) # create a unique display name display_name = f'custom_display_{uuid.uuid4()}' list_staff_master_course = list_with_level(self.course.id, 'staff') list_instructor_master_course = list_with_level( self.course.id, 'instructor') assert len(outbox) == 0 # give access to the course staff/instructor remove_master_course_staff_from_ccx(self.course, self.ccx_locator, display_name) assert len(outbox) == (len(list_staff_master_course) + len(list_instructor_master_course)) for email in outbox: assert display_name in email.subject
def test_add_master_course_staff_to_ccx_with_exception(self): """ When exception raise from ``enroll_email`` assert that enrollment skipped for that staff or instructor. """ staff = self.make_staff() assert CourseStaffRole(self.course.id).has_user(staff) # adding instructor to master course. instructor = self.make_instructor() assert CourseInstructorRole(self.course.id).has_user(instructor) with mock.patch.object(CourseEnrollment, 'enroll_by_email', side_effect=CourseEnrollmentException()): add_master_course_staff_to_ccx(self.course, self.ccx_locator, self.ccx.display_name) assert not CourseEnrollment.objects.filter( course_id=self.ccx_locator, user=staff).exists() assert not CourseEnrollment.objects.filter( course_id=self.ccx_locator, user=instructor).exists() with mock.patch.object(CourseEnrollment, 'enroll_by_email', side_effect=SMTPException()): add_master_course_staff_to_ccx(self.course, self.ccx_locator, self.ccx.display_name) assert not CourseEnrollment.objects.filter( course_id=self.ccx_locator, user=staff).exists() assert not CourseEnrollment.objects.filter( course_id=self.ccx_locator, user=instructor).exists()
class RoleCacheTestCase(TestCase): # lint-amnesty, pylint: disable=missing-class-docstring IN_KEY = CourseKey.from_string('edX/toy/2012_Fall') NOT_IN_KEY = CourseKey.from_string('edX/toy/2013_Fall') ROLES = ( (CourseStaffRole(IN_KEY), ('staff', IN_KEY, 'edX')), (CourseInstructorRole(IN_KEY), ('instructor', IN_KEY, 'edX')), (OrgStaffRole(IN_KEY.org), ('staff', None, 'edX')), (OrgInstructorRole(IN_KEY.org), ('instructor', None, 'edX')), (CourseBetaTesterRole(IN_KEY), ('beta_testers', IN_KEY, 'edX')), ) def setUp(self): super(RoleCacheTestCase, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments self.user = UserFactory() @ddt.data(*ROLES) @ddt.unpack def test_only_in_role(self, role, target): role.add_users(self.user) cache = RoleCache(self.user) assert cache.has_role(*target) for other_role, other_target in self.ROLES: if other_role == role: continue assert not cache.has_role(*other_target) @ddt.data(*ROLES) @ddt.unpack def test_empty_cache(self, role, target): # lint-amnesty, pylint: disable=unused-argument cache = RoleCache(self.user) assert not cache.has_role(*target)
def get_users(self, course_id, user_id=None): """ Gets the users for a given target. Result is returned in the form of a queryset, and may contain duplicates. """ staff_qset = CourseStaffRole(course_id).users_with_role() instructor_qset = CourseInstructorRole(course_id).users_with_role() staff_instructor_qset = (staff_qset | instructor_qset) enrollment_query = models.Q(is_active=True, courseenrollment__course_id=course_id, courseenrollment__is_active=True) enrollment_qset = User.objects.filter(enrollment_query) if self.target_type == SEND_TO_MYSELF: if user_id is None: raise ValueError( "Must define self user to send email to self.") user = User.objects.filter(id=user_id) return use_read_replica_if_available(user) elif self.target_type == SEND_TO_STAFF: return use_read_replica_if_available(staff_instructor_qset) elif self.target_type == SEND_TO_LEARNERS: return use_read_replica_if_available( enrollment_qset.exclude(id__in=staff_instructor_qset)) elif self.target_type == SEND_TO_COHORT: return self.cohorttarget.cohort.users.filter( id__in=enrollment_qset) # lint-amnesty, pylint: disable=no-member elif self.target_type == SEND_TO_TRACK: return use_read_replica_if_available( User.objects.filter( models.Q(courseenrollment__mode=self.coursemodetarget. track.mode_slug) & enrollment_query)) else: raise ValueError(f"Unrecognized target type {self.target_type}")
def delete(self, request, course_key_string): course_key = CourseKey.from_string(course_key_string) email = request.data.get("email", None) validate_param_exist(email, "email") try: user = User.objects.get(email=email) except Exception: # pylint: disable=broad-except msg = { "error": "Could not find user by email address '{email}'".format( email=email) } return Response(msg, 404) auth.get_user_permissions(request.user, course_key) auth.remove_users(request.user, CourseStaffRole(course_key), user) auth.remove_users(request.user, CourseInstructorRole(course_key), user) CourseEnrollment.unenroll(user, course_key) msg = "'{email}''s permissions are revoked from '{course_key}'".format( email=email, course_key=course_key) log.info(msg) return Response( {'message': "User is removed from {}.".format(course_key)})
def test_add_master_course_staff_to_ccx_display_name(self): """ Test add staff of master course to ccx course. Specific test to check that a passed display name is in the subject of the email sent to the enrolled users. """ staff = self.make_staff() self.assertTrue(CourseStaffRole(self.course.id).has_user(staff)) # adding instructor to master course. instructor = self.make_instructor() self.assertTrue( CourseInstructorRole(self.course.id).has_user(instructor)) outbox = self.get_outbox() # create a unique display name display_name = 'custom_display_{}'.format(uuid.uuid4()) list_staff_master_course = list_with_level(self.course, 'staff') list_instructor_master_course = list_with_level( self.course, 'instructor') self.assertEqual(len(outbox), 0) # give access to the course staff/instructor add_master_course_staff_to_ccx(self.course, self.ccx_locator, display_name) self.assertEqual( len(outbox), len(list_staff_master_course) + len(list_instructor_master_course)) for email in outbox: self.assertIn(display_name, email.subject)