Example #1
0
class RoleCacheTestCase(TestCase):

    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()
        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)
        self.assertTrue(cache.has_role(*target))

        for other_role, other_target in self.ROLES:
            if other_role == role:
                continue

            self.assertFalse(cache.has_role(*other_target))

    @ddt.data(*ROLES)
    @ddt.unpack
    def test_empty_cache(self, role, target):
        cache = RoleCache(self.user)
        self.assertFalse(cache.has_role(*target))
Example #2
0
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
Example #3
0
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)
Example #4
0
    def test_org_and_course_roles(self):
        """
        Test that Org and course roles don't interfere with course roles or vice versa
        """
        OrgInstructorRole(self.course_key.org).add_users(self.student)
        CourseInstructorRole(self.course_key).add_users(self.student)
        assert OrgInstructorRole(self.course_key.org).has_user(self.student), \
            f"Student doesn't have access to {six.text_type(self.course_key.org)}"
        assert CourseInstructorRole(self.course_key).has_user(self.student), \
            f"Student doesn't have access to {six.text_type(self.course_key)}"

        # remove access and confirm
        OrgInstructorRole(self.course_key.org).remove_users(self.student)
        assert not OrgInstructorRole(self.course_key.org).has_user(self.student), \
            f'Student still has access to {self.course_key.org}'
        assert CourseInstructorRole(self.course_key).has_user(self.student), \
            f"Student doesn't have access to {six.text_type(self.course_key)}"

        # ok now keep org role and get rid of course one
        OrgInstructorRole(self.course_key.org).add_users(self.student)
        CourseInstructorRole(self.course_key).remove_users(self.student)
        assert OrgInstructorRole(self.course_key.org).has_user(self.student), \
            f'Student lost has access to {self.course_key.org}'
        assert not CourseInstructorRole(self.course_key).has_user(self.student), \
            f"Student doesn't have access to {six.text_type(self.course_key)}"
Example #5
0
    def test_org_and_course_roles(self):
        """
        Test that Org and course roles don't interfere with course roles or vice versa
        """
        OrgInstructorRole(self.course_key.org).add_users(self.student)
        CourseInstructorRole(self.course_key).add_users(self.student)
        self.assertTrue(
            OrgInstructorRole(self.course_key.org).has_user(self.student),
            "Student doesn't have access to {}".format(
                six.text_type(self.course_key.org)))
        self.assertTrue(
            CourseInstructorRole(self.course_key).has_user(self.student),
            "Student doesn't have access to {}".format(
                six.text_type(self.course_key)))

        # remove access and confirm
        OrgInstructorRole(self.course_key.org).remove_users(self.student)
        self.assertFalse(
            OrgInstructorRole(self.course_key.org).has_user(self.student),
            "Student still has access to {}".format(self.course_key.org))
        self.assertTrue(
            CourseInstructorRole(self.course_key).has_user(self.student),
            "Student doesn't have access to {}".format(
                six.text_type(self.course_key)))

        # ok now keep org role and get rid of course one
        OrgInstructorRole(self.course_key.org).add_users(self.student)
        CourseInstructorRole(self.course_key).remove_users(self.student)
        self.assertTrue(
            OrgInstructorRole(self.course_key.org).has_user(self.student),
            "Student lost has access to {}".format(self.course_key.org))
        self.assertFalse(
            CourseInstructorRole(self.course_key).has_user(self.student),
            "Student doesn't have access to {}".format(
                six.text_type(self.course_key)))
Example #6
0
def administrative_accesses_to_course_for_user(user, course_key):
    """
    Returns types of access a user have for given course.
    """
    global_staff = GlobalStaff().has_user(user)

    staff_access = (CourseStaffRole(course_key).has_user(user)
                    or OrgStaffRole(course_key.org).has_user(user))

    instructor_access = (CourseInstructorRole(course_key).has_user(user)
                         or OrgInstructorRole(course_key.org).has_user(user))

    return global_staff, staff_access, instructor_access
Example #7
0
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

    # mcdaniel dec-2020: all approved course creators receive all_perms to the course templates.
    if str(course_key)[-8:] == 'Template':
        log.info(
            'common.djangoapps.student.auth.get_user_permissions() - template found. granting all_perms on: {course_key}'
            .format(course_key=course_key))
        return all_perms

    # 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 has_staff_roles(user, course_key):
    """
    Return true if a user has any of the following roles
    Staff, Instructor, Beta Tester, Forum Community TA, Forum Group Moderator, Forum Moderator, Forum Administrator
    """
    forum_roles = [
        FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_GROUP_MODERATOR,
        FORUM_ROLE_MODERATOR, FORUM_ROLE_ADMINISTRATOR
    ]
    is_staff = CourseStaffRole(course_key).has_user(user)
    is_instructor = CourseInstructorRole(course_key).has_user(user)
    is_beta_tester = CourseBetaTesterRole(course_key).has_user(user)
    is_org_staff = OrgStaffRole(course_key.org).has_user(user)
    is_org_instructor = OrgInstructorRole(course_key.org).has_user(user)
    is_global_staff = GlobalStaff().has_user(user)
    has_forum_role = Role.user_has_role_for_course(user, course_key,
                                                   forum_roles)
    if any([
            is_staff, is_instructor, is_beta_tester, is_org_staff,
            is_org_instructor, is_global_staff, has_forum_role
    ]):
        return True
    return False
class TestCourseListing(ModuleStoreTestCase):
    """
    Unit tests for getting the list of courses for a logged in user
    """
    def setUp(self):
        """
        Add a user and a course
        """
        super(TestCourseListing, self).setUp()  # lint-amnesty, pylint: disable=super-with-arguments
        # create and log in a staff user.
        # create and log in a non-staff user
        self.user = UserFactory()
        self.factory = RequestFactory()
        self.request = self.factory.get('/course')
        self.request.user = self.user
        self.client = AjaxEnabledTestClient()
        self.client.login(username=self.user.username, password='******')

    def _create_course_with_access_groups(self, course_location, user=None, store=ModuleStoreEnum.Type.split):
        """
        Create dummy course with 'CourseFactory' and role (instructor/staff) groups
        """
        course = CourseFactory.create(
            org=course_location.org,
            number=course_location.course,
            run=course_location.run,
            default_store=store
        )
        self._add_role_access_to_user(user, course.id)
        return course

    def _add_role_access_to_user(self, user, course_id):
        """ Assign access roles to user in the course. """
        if user is not None:
            for role in [CourseInstructorRole, CourseStaffRole]:
                role(course_id).add_users(user)

    def tearDown(self):
        """
        Reverse the setup
        """
        self.client.logout()
        ModuleStoreTestCase.tearDown(self)

    def test_empty_course_listing(self):
        """
        Test on empty course listing, studio name is properly displayed
        """
        message = u"Are you staff on an existing {studio_name} course?".format(studio_name=settings.STUDIO_SHORT_NAME)
        response = self.client.get('/home')
        self.assertContains(response, message)

    def test_get_course_list(self):
        """
        Test getting courses with new access group format e.g. 'instructor_edx.course.run'
        """
        course_location = self.store.make_course_key('Org1', 'Course1', 'Run1')
        self._create_course_with_access_groups(course_location, self.user)

        # get courses through iterating all courses
        courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
        courses_list = list(courses_iter)
        self.assertEqual(len(courses_list), 1)

        courses_summary_list, __ = _accessible_courses_summary_iter(self.request)
        self.assertEqual(len(list(courses_summary_list)), 1)

        # get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list_by_groups), 1)

        # check both course lists have same courses
        course_keys_in_course_list = [course.id for course in courses_list]
        course_keys_in_courses_list_by_groups = [course.id for course in courses_list_by_groups]

        self.assertEqual(course_keys_in_course_list, course_keys_in_courses_list_by_groups)

    def test_courses_list_with_ccx_courses(self):
        """
        Tests that CCX courses are filtered in course listing.
        """
        # Create a course and assign access roles to user.
        course_location = self.store.make_course_key('Org1', 'Course1', 'Run1')
        course = self._create_course_with_access_groups(course_location, self.user)

        # Create a ccx course key and add assign access roles to user.
        ccx_course_key = CCXLocator.from_course_locator(course.id, '1')
        self._add_role_access_to_user(self.user, ccx_course_key)

        # Test that CCX courses are filtered out.
        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), 1)
        self.assertNotIn(
            ccx_course_key,
            [course.id for course in courses_list]
        )

        # Get all courses which user has access.
        instructor_courses = UserBasedRole(self.user, CourseInstructorRole.ROLE).courses_with_role()
        staff_courses = UserBasedRole(self.user, CourseStaffRole.ROLE).courses_with_role()
        all_courses = (instructor_courses | staff_courses)

        # Verify that CCX course exists in access but filtered by `_accessible_courses_list_from_groups`.
        self.assertIn(
            ccx_course_key,
            [access.course_id for access in all_courses]
        )

        # Verify that CCX courses are filtered out while iterating over all courses
        mocked_ccx_course = Mock(id=ccx_course_key)
        with patch('xmodule.modulestore.mixed.MixedModuleStore.get_course_summaries', return_value=[mocked_ccx_course]):
            courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
            self.assertEqual(len(list(courses_iter)), 0)

    @ddt.data(
        (ModuleStoreEnum.Type.split, 3),
        (ModuleStoreEnum.Type.mongo, 2)
    )
    @ddt.unpack
    def test_staff_course_listing(self, default_store, mongo_calls):
        """
        Create courses and verify they take certain amount of mongo calls to call get_courses_accessible_to_user.
        Also verify that fetch accessible courses list for staff user returns CourseSummary instances.
        """

        # Assign & verify staff role to the user
        GlobalStaff().add_users(self.user)
        self.assertTrue(GlobalStaff().has_user(self.user))

        with self.store.default_store(default_store):
            # Create few courses
            for num in range(TOTAL_COURSES_COUNT):
                course_location = self.store.make_course_key('Org', 'CreatedCourse' + str(num), 'Run')
                self._create_course_with_access_groups(course_location, self.user, default_store)

        # Fetch accessible courses list & verify their count
        courses_list_by_staff, __ = get_courses_accessible_to_user(self.request)
        self.assertEqual(len(list(courses_list_by_staff)), TOTAL_COURSES_COUNT)

        # Verify fetched accessible courses list is a list of CourseSummery instances
        self.assertTrue(all(isinstance(course, CourseSummary) for course in courses_list_by_staff))

        # Now count the db queries for staff
        with check_mongo_calls(mongo_calls):
            list(_accessible_courses_summary_iter(self.request))

    @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
    def test_get_course_list_with_invalid_course_location(self, store):
        """
        Test getting courses with invalid course location (course deleted from modulestore).
        """
        with self.store.default_store(store):
            course_key = self.store.make_course_key('Org', 'Course', 'Run')
            self._create_course_with_access_groups(course_key, self.user, store)

        # get courses through iterating all courses
        courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
        courses_list = list(courses_iter)
        self.assertEqual(len(courses_list), 1)

        courses_summary_iter, __ = _accessible_courses_summary_iter(self.request)
        courses_summary_list = list(courses_summary_iter)

        # Verify fetched accessible courses list is a list of CourseSummery instances and only one course
        # is returned
        self.assertTrue(all(isinstance(course, CourseSummary) for course in courses_summary_list))
        self.assertEqual(len(courses_summary_list), 1)

        # get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list_by_groups), 1)

        course_keys_in_course_list = [course.id for course in courses_list]
        course_keys_in_courses_list_by_groups = [course.id for course in courses_list_by_groups]
        # check course lists have same courses
        self.assertEqual(course_keys_in_course_list, course_keys_in_courses_list_by_groups)
        # now delete this course and re-add user to instructor group of this course
        delete_course(course_key, self.user.id)

        CourseInstructorRole(course_key).add_users(self.user)

        # Get courses through iterating all courses
        courses_iter, __ = _accessible_courses_iter_for_tests(self.request)

        # Get course summaries by iterating all courses
        courses_summary_iter, __ = _accessible_courses_summary_iter(self.request)

        # Get courses by reversing group name formats
        courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)

        # Test that course list returns no course
        self.assertEqual(
            [len(list(courses_iter)), len(courses_list_by_groups), len(list(courses_summary_iter))],
            [0, 0, 0]
        )

    @ddt.data(
        (ModuleStoreEnum.Type.split, 3, 3),
        (ModuleStoreEnum.Type.mongo, 2, 2)
    )
    @ddt.unpack
    def test_course_listing_performance(self, store, courses_list_from_group_calls, courses_list_calls):
        """
        Create large number of courses and give access of some of these courses to the user and
        compare the time to fetch accessible courses for the user through traversing all courses and
        reversing django groups
        """
        # create list of random course numbers which will be accessible to the user
        user_course_ids = random.sample(list(range(TOTAL_COURSES_COUNT)), USER_COURSES_COUNT)

        # create courses and assign those to the user which have their number in user_course_ids
        with self.store.default_store(store):
            for number in range(TOTAL_COURSES_COUNT):
                org = 'Org{0}'.format(number)
                course = 'Course{0}'.format(number)
                run = 'Run{0}'.format(number)
                course_location = self.store.make_course_key(org, course, run)
                if number in user_course_ids:
                    self._create_course_with_access_groups(course_location, self.user, store=store)
                else:
                    self._create_course_with_access_groups(course_location, store=store)

        # get courses by iterating through all courses
        courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
        self.assertEqual(len(list(courses_iter)), USER_COURSES_COUNT)

        # again get courses by iterating through all courses
        courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
        self.assertEqual(len(list(courses_iter)), USER_COURSES_COUNT)

        # get courses by reversing django groups
        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), USER_COURSES_COUNT)

        # again get courses by reversing django groups
        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), USER_COURSES_COUNT)

        # Now count the db queries
        with check_mongo_calls(courses_list_from_group_calls):
            _accessible_courses_list_from_groups(self.request)

        with check_mongo_calls(courses_list_calls):
            list(_accessible_courses_iter_for_tests(self.request))
        # Calls:
        #    1) query old mongo
        #    2) get_more on old mongo
        #    3) query split (but no courses so no fetching of data)

    @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
    def test_course_listing_errored_deleted_courses(self, store):
        """
        Create good courses, courses that won't load, and deleted courses which still have
        roles. Test course listing.
        """
        with self.store.default_store(store):
            course_location = self.store.make_course_key('testOrg', 'testCourse', 'RunBabyRun')
            self._create_course_with_access_groups(course_location, self.user, store)

            course_location = self.store.make_course_key('testOrg', 'doomedCourse', 'RunBabyRun')
            self._create_course_with_access_groups(course_location, self.user, store)
            self.store.delete_course(course_location, self.user.id)

        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), 1, courses_list)

    @ddt.data(OrgStaffRole('AwesomeOrg'), OrgInstructorRole('AwesomeOrg'))
    def test_course_listing_org_permissions(self, role):
        """
        Create multiple courses within the same org.  Verify that someone with org-wide permissions can access
        all of them.
        """
        org_course_one = self.store.make_course_key('AwesomeOrg', 'Course1', 'RunBabyRun')
        CourseFactory.create(
            org=org_course_one.org,
            number=org_course_one.course,
            run=org_course_one.run
        )

        org_course_two = self.store.make_course_key('AwesomeOrg', 'Course2', 'RunBabyRun')
        CourseFactory.create(
            org=org_course_two.org,
            number=org_course_two.course,
            run=org_course_two.run
        )

        # Two types of org-wide roles have edit permissions: staff and instructor.  We test both
        role.add_users(self.user)

        with self.assertRaises(AccessListFallback):
            _accessible_courses_list_from_groups(self.request)
        courses_list, __ = get_courses_accessible_to_user(self.request)

        # Verify fetched accessible courses list is a list of CourseSummery instances and test expacted
        # course count is returned
        self.assertEqual(len(list(courses_list)), 2)
        self.assertTrue(all(isinstance(course, CourseSummary) for course in courses_list))

    def test_course_listing_with_actions_in_progress(self):
        sourse_course_key = CourseLocator('source-Org', 'source-Course', 'source-Run')

        num_courses_to_create = 3
        courses = [
            self._create_course_with_access_groups(
                CourseLocator('Org', 'CreatedCourse' + str(num), 'Run'),
                self.user,
            )
            for num in range(num_courses_to_create)
        ]
        courses_in_progress = [
            self._create_course_with_access_groups(
                CourseLocator('Org', 'InProgressCourse' + str(num), 'Run'),
                self.user,
            )
            for num in range(num_courses_to_create)
        ]

        # simulate initiation of course actions
        for course in courses_in_progress:
            CourseRerunState.objects.initiated(
                sourse_course_key, destination_course_key=course.id, user=self.user, display_name="test course"
            )

        # verify return values
        def _set_of_course_keys(course_list, key_attribute_name='id'):
            """Returns a python set of course keys by accessing the key with the given attribute name."""
            return set(getattr(c, key_attribute_name) for c in course_list)

        found_courses, unsucceeded_course_actions = _accessible_courses_iter_for_tests(self.request)
        self.assertSetEqual(_set_of_course_keys(courses + courses_in_progress), _set_of_course_keys(found_courses))
        self.assertSetEqual(
            _set_of_course_keys(courses_in_progress), _set_of_course_keys(unsucceeded_course_actions, 'course_key')
        )
Example #10
0
 def course_key(self, create, extracted, **kwargs):
     if extracted is None:
         raise ValueError(
             "Must specify a CourseKey for an org-instructor user")
     OrgInstructorRole(extracted.org).add_users(self)