""" This module defines tests for courseware.access that are specific to group access control rules. """ import ddt from nose.plugins.attrib import attr from stevedore.extension import Extension, ExtensionManager import courseware.access as access from courseware.tests.factories import StaffFactory, UserFactory from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.partitions.partitions import USER_PARTITION_SCHEME_NAMESPACE, Group, UserPartition class MemoryUserPartitionScheme(object): """ In-memory partition scheme for testing. """ name = "memory" def __init__(self): self.current_group = {} def set_group_for_user(self, user, user_partition, group): """ Link this user to this group in this partition, in memory. """ self.current_group.setdefault(user.id, {})[user_partition.id] = group def get_group_for_user(self, course_id, user, user_partition): # pylint: disable=unused-argument """ Fetch the group to which this user is linked in this partition, or None. """ return self.current_group.get(user.id, {}).get(user_partition.id) def resolve_attrs(test_method): """ Helper function used with ddt. It allows passing strings to test methods via @ddt.data, which are the names of instance attributes on `self`, and replaces them with the resolved values of those attributes in the method call. """ def _wrapper(self, *args): # pylint: disable=missing-docstring new_args = [getattr(self, arg) for arg in args] return test_method(self, *new_args) return _wrapper @attr(shard=2) @ddt.ddt class GroupAccessTestCase(ModuleStoreTestCase): """ Tests to ensure that has_access() correctly enforces the visibility restrictions specified in the `group_access` field of XBlocks. """ def set_user_group(self, user, partition, group): """ Internal DRY / shorthand. """ partition.scheme.set_group_for_user(user, partition, group) def set_group_access(self, block_location, access_dict): """ Set group_access on block specified by location. """ block = modulestore().get_item(block_location) block.group_access = access_dict modulestore().update_item(block, 1) def set_user_partitions(self, block_location, partitions): """ Sets the user_partitions on block specified by location. """ block = modulestore().get_item(block_location) block.user_partitions = partitions modulestore().update_item(block, 1) def setUp(self): super(GroupAccessTestCase, self).setUp() UserPartition.scheme_extensions = ExtensionManager.make_test_instance( [ Extension( "memory", USER_PARTITION_SCHEME_NAMESPACE, MemoryUserPartitionScheme(), None ), Extension( "random", USER_PARTITION_SCHEME_NAMESPACE, MemoryUserPartitionScheme(), None ) ], namespace=USER_PARTITION_SCHEME_NAMESPACE ) self.cat_group = Group(10, 'cats') self.dog_group = Group(20, 'dogs') self.worm_group = Group(30, 'worms') self.animal_partition = UserPartition( 0, 'Pet Partition', 'which animal are you?', [self.cat_group, self.dog_group, self.worm_group], scheme=UserPartition.get_scheme("memory"), ) self.red_group = Group(1000, 'red') self.blue_group = Group(2000, 'blue') self.gray_group = Group(3000, 'gray') self.color_partition = UserPartition( 100, 'Color Partition', 'what color are you?', [self.red_group, self.blue_group, self.gray_group], scheme=UserPartition.get_scheme("memory"), ) self.course = CourseFactory.create( user_partitions=[self.animal_partition, self.color_partition], ) with self.store.bulk_operations(self.course.id, emit_signals=False): chapter = ItemFactory.create(category='chapter', parent=self.course) section = ItemFactory.create(category='sequential', parent=chapter) vertical = ItemFactory.create(category='vertical', parent=section) component = ItemFactory.create(category='problem', parent=vertical) self.chapter_location = chapter.location self.section_location = section.location self.vertical_location = vertical.location self.component_location = component.location self.red_cat = UserFactory() # student in red and cat groups self.set_user_group(self.red_cat, self.animal_partition, self.cat_group) self.set_user_group(self.red_cat, self.color_partition, self.red_group) self.blue_dog = UserFactory() # student in blue and dog groups self.set_user_group(self.blue_dog, self.animal_partition, self.dog_group) self.set_user_group(self.blue_dog, self.color_partition, self.blue_group) self.white_mouse = UserFactory() # student in no group self.gray_worm = UserFactory() # student in deleted group self.set_user_group(self.gray_worm, self.animal_partition, self.worm_group) self.set_user_group(self.gray_worm, self.color_partition, self.gray_group) # delete the gray/worm groups from the partitions now so we can test scenarios # for user whose group is missing. self.animal_partition.groups.pop() self.color_partition.groups.pop() # add a staff user, whose access will be unconditional in spite of group access. self.staff = StaffFactory.create(course_key=self.course.id) # avoid repeatedly declaring the same sequence for ddt in all the test cases. PARENT_CHILD_PAIRS = ( ('chapter_location', 'chapter_location'), ('chapter_location', 'section_location'), ('chapter_location', 'vertical_location'), ('chapter_location', 'component_location'), ('section_location', 'section_location'), ('section_location', 'vertical_location'), ('section_location', 'component_location'), ('vertical_location', 'vertical_location'), ('vertical_location', 'component_location'), ) def tearDown(self): """ Clear out the stevedore extension points on UserPartition to avoid side-effects in other tests. """ UserPartition.scheme_extensions = None super(GroupAccessTestCase, self).tearDown() def check_access(self, user, block_location, is_accessible): """ DRY helper. """ self.assertIs( bool(access.has_access(user, 'load', modulestore().get_item(block_location), self.course.id)), is_accessible ) def ensure_staff_access(self, block_location): """ Another DRY helper. """ block = modulestore().get_item(block_location) self.assertTrue(access.has_access(self.staff, 'load', block, self.course.id)) # NOTE: in all the tests that follow, `block_specified` and # `block_accessed` designate the place where group_access rules are # specified, and where access is being checked in the test, respectively. @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_single_partition_single_group(self, block_specified, block_accessed): """ Access checks are correctly enforced on the block when a single group is specified for a single partition. """ self.set_group_access( block_specified, {self.animal_partition.id: [self.cat_group.id]}, ) self.check_access(self.red_cat, block_accessed, True) self.check_access(self.blue_dog, block_accessed, False) self.check_access(self.white_mouse, block_accessed, False) self.check_access(self.gray_worm, block_accessed, False) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_single_partition_two_groups(self, block_specified, block_accessed): """ Access checks are correctly enforced on the block when multiple groups are specified for a single partition. """ self.set_group_access( block_specified, {self.animal_partition.id: [self.cat_group.id, self.dog_group.id]}, ) self.check_access(self.red_cat, block_accessed, True) self.check_access(self.blue_dog, block_accessed, True) self.check_access(self.white_mouse, block_accessed, False) self.check_access(self.gray_worm, block_accessed, False) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_single_partition_disjoint_groups(self, block_specified, block_accessed): """ When the parent's and child's group specifications do not intersect, access is denied to the child regardless of the user's groups. """ if block_specified == block_accessed: # this test isn't valid unless block_accessed is a descendant of # block_specified. return self.set_group_access( block_specified, {self.animal_partition.id: [self.dog_group.id]}, ) self.set_group_access( block_accessed, {self.animal_partition.id: [self.cat_group.id]}, ) self.check_access(self.red_cat, block_accessed, False) self.check_access(self.blue_dog, block_accessed, False) self.check_access(self.white_mouse, block_accessed, False) self.check_access(self.gray_worm, block_accessed, False) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_single_empty_partition(self, block_specified, block_accessed): """ No group access checks are enforced on the block when group_access declares a partition but does not specify any groups. """ self.set_group_access(block_specified, {self.animal_partition.id: []}) self.check_access(self.red_cat, block_accessed, True) self.check_access(self.blue_dog, block_accessed, True) self.check_access(self.white_mouse, block_accessed, True) self.check_access(self.gray_worm, block_accessed, True) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_empty_dict(self, block_specified, block_accessed): """ No group access checks are enforced on the block when group_access is an empty dictionary. """ self.set_group_access(block_specified, {}) self.check_access(self.red_cat, block_accessed, True) self.check_access(self.blue_dog, block_accessed, True) self.check_access(self.white_mouse, block_accessed, True) self.check_access(self.gray_worm, block_accessed, True) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_none(self, block_specified, block_accessed): """ No group access checks are enforced on the block when group_access is None. """ self.set_group_access(block_specified, None) self.check_access(self.red_cat, block_accessed, True) self.check_access(self.blue_dog, block_accessed, True) self.check_access(self.white_mouse, block_accessed, True) self.check_access(self.gray_worm, block_accessed, True) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_single_partition_group_none(self, block_specified, block_accessed): """ No group access checks are enforced on the block when group_access specifies a partition but its value is None. """ self.set_group_access(block_specified, {self.animal_partition.id: None}) self.check_access(self.red_cat, block_accessed, True) self.check_access(self.blue_dog, block_accessed, True) self.check_access(self.white_mouse, block_accessed, True) self.check_access(self.gray_worm, block_accessed, True) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_single_partition_group_empty_list(self, block_specified, block_accessed): """ No group access checks are enforced on the block when group_access specifies a partition but its value is an empty list. """ self.set_group_access(block_specified, {self.animal_partition.id: []}) self.check_access(self.red_cat, block_accessed, True) self.check_access(self.blue_dog, block_accessed, True) self.check_access(self.white_mouse, block_accessed, True) self.check_access(self.gray_worm, block_accessed, True) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_nonexistent_nonempty_partition(self, block_specified, block_accessed): """ Access will be denied to the block when group_access specifies a nonempty partition that does not exist in course.user_partitions. """ self.set_group_access(block_specified, {9: [99]}) self.check_access(self.red_cat, block_accessed, False) self.check_access(self.blue_dog, block_accessed, False) self.check_access(self.white_mouse, block_accessed, False) self.check_access(self.gray_worm, block_accessed, False) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_has_access_nonexistent_group(self, block_specified, block_accessed): """ Access will be denied to the block when group_access contains a group id that does not exist in its referenced partition. """ self.set_group_access(block_specified, {self.animal_partition.id: [99]}) self.check_access(self.red_cat, block_accessed, False) self.check_access(self.blue_dog, block_accessed, False) self.check_access(self.white_mouse, block_accessed, False) self.check_access(self.gray_worm, block_accessed, False) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_multiple_partitions(self, block_specified, block_accessed): """ Group access restrictions are correctly enforced when multiple partition / group rules are defined. """ self.set_group_access( block_specified, { self.animal_partition.id: [self.cat_group.id], self.color_partition.id: [self.red_group.id], }, ) self.check_access(self.red_cat, block_accessed, True) self.check_access(self.blue_dog, block_accessed, False) self.check_access(self.white_mouse, block_accessed, False) self.check_access(self.gray_worm, block_accessed, False) self.ensure_staff_access(block_accessed) @ddt.data(*PARENT_CHILD_PAIRS) @ddt.unpack @resolve_attrs def test_multiple_partitions_deny_access(self, block_specified, block_accessed): """ Group access restrictions correctly deny access even when some (but not all) group_access rules are satisfied. """ self.set_group_access( block_specified, { self.animal_partition.id: [self.cat_group.id], self.color_partition.id: [self.blue_group.id], }, ) self.check_access(self.red_cat, block_accessed, False) self.check_access(self.blue_dog, block_accessed, False) self.check_access(self.gray_worm, block_accessed, False) self.ensure_staff_access(block_accessed)