def test_staff_access_country_block(self, staff_role_cls): # Add a country to the blacklist CountryAccessRule.objects.create( rule_type=CountryAccessRule.BLACKLIST_RULE, restricted_course=self.restricted_course, country=Country.objects.get(country='US') ) # Appear to make a request from an IP in the blocked country with self._mock_geoip('US'): result = embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0') # Expect that the user is blocked, because the user isn't staff self.assertFalse(result, msg="User should not have access because the user isn't staff.") # Instantiate the role, configuring it for this course or org if issubclass(staff_role_cls, CourseRole): staff_role = staff_role_cls(self.course.id) elif issubclass(staff_role_cls, OrgRole): staff_role = staff_role_cls(self.course.id.org) else: staff_role = staff_role_cls() # Add the user to the role staff_role.add_users(self.user) # Now the user should have access with self._mock_geoip('US'): result = embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0') self.assertTrue(result, msg="User should have access because the user is staff.")
def test_caching_no_restricted_courses(self): RestrictedCourse.objects.all().delete() cache.clear() with self.assertNumQueries(1): embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0') with self.assertNumQueries(0): embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0')
def test_caching(self): # Test the scenario that will go through every check # (restricted course, but pass all the checks) # This is the worst case, so it will hit all of the # caching code. with self.assertNumQueries(3): embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0') with self.assertNumQueries(0): embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0')
def test_course_not_restricted(self): # No restricted course model for this course key, # so all access checks should be skipped. unrestricted_course = CourseFactory.create() with self.assertNumQueries(1): embargo_api.check_course_access(unrestricted_course.id, user=self.user, ip_address='0.0.0.0') # The second check should require no database queries with self.assertNumQueries(0): embargo_api.check_course_access(unrestricted_course.id, user=self.user, ip_address='0.0.0.0')
def test_country_access_rules(self, ip_country, profile_country, blacklist, whitelist, allow_access): # Configure the access rules for whitelist_country in whitelist: CountryAccessRule.objects.create( rule_type=CountryAccessRule.WHITELIST_RULE, restricted_course=self.restricted_course, country=Country.objects.get(country=whitelist_country)) for blacklist_country in blacklist: CountryAccessRule.objects.create( rule_type=CountryAccessRule.BLACKLIST_RULE, restricted_course=self.restricted_course, country=Country.objects.get(country=blacklist_country)) # Configure the user's profile country if profile_country is not None: self.user.profile.country = profile_country self.user.profile.save() # Appear to make a request from an IP in a particular country with self._mock_geoip(ip_country): # Call the API. Note that the IP address we pass in doesn't # matter, since we're injecting a mock for geo-location result = embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0') # Verify that the access rules were applied correctly self.assertEqual(result, allow_access)
def test_country_access_fallback_to_continent_code(self): # Simulate PyGeoIP falling back to a continent code # instead of a country code. In this case, we should # allow the user access. with self._mock_geoip('EU'): result = embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0') self.assertTrue(result)
def test_country_access_rules(self, ip_country, profile_country, blacklist, whitelist, allow_access): # Configure the access rules for whitelist_country in whitelist: CountryAccessRule.objects.create( rule_type=CountryAccessRule.WHITELIST_RULE, restricted_course=self.restricted_course, country=Country.objects.get(country=whitelist_country) ) for blacklist_country in blacklist: CountryAccessRule.objects.create( rule_type=CountryAccessRule.BLACKLIST_RULE, restricted_course=self.restricted_course, country=Country.objects.get(country=blacklist_country) ) # Configure the user's profile country if profile_country is not None: self.user.profile.country = profile_country self.user.profile.save() # Appear to make a request from an IP in a particular country with mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr') as mock_ip: mock_ip.return_value = ip_country # Call the API. Note that the IP address we pass in doesn't # matter, since we're injecting a mock for geo-location result = embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0') # Verify that the access rules were applied correctly self.assertEqual(result, allow_access)
def test_ip_v6(self): # Test the scenario that will go through every check # (restricted course, but pass all the checks) result = embargo_api.check_course_access( self.course.id, user=self.user, ip_address='FE80::0202:B3FF:FE1E:8329') self.assertTrue(result)
def test_no_user_has_access(self): CountryAccessRule.objects.create( rule_type=CountryAccessRule.BLACKLIST_RULE, restricted_course=self.restricted_course, country=Country.objects.get(country='US') ) # The user is set to None, because the user has not been authenticated. result = embargo_api.check_course_access(self.course.id, ip_address='0.0.0.0') self.assertTrue(result)
def test_no_user_has_access(self): CountryAccessRule.objects.create( rule_type=CountryAccessRule.BLACKLIST_RULE, restricted_course=self.restricted_course, country=Country.objects.get(country='US')) # The user is set to None, because the user has not been authenticated. result = embargo_api.check_course_access(self.course.id, ip_address='0.0.0.0') self.assertTrue(result)
def test_no_user_blocked(self): CountryAccessRule.objects.create( rule_type=CountryAccessRule.BLACKLIST_RULE, restricted_course=self.restricted_course, country=Country.objects.get(country='US') ) with mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr') as mock_ip: mock_ip.return_value = 'US' # The user is set to None, because the user has not been authenticated. result = embargo_api.check_course_access(self.course.id, ip_address='0.0.0.0') self.assertFalse(result)
def test_profile_country_db_null(self): # Django country fields treat NULL values inconsistently. # When saving a profile with country set to None, Django saves an empty string to the database. # However, when the country field loads a NULL value from the database, it sets # `country.code` to `None`. This caused a bug in which country values created by # the original South schema migration -- which defaulted to NULL -- caused a runtime # exception when the embargo middleware treated the value as a string. # In order to simulate this behavior, we can't simply set `profile.country = None`. # (because when we save it, it will set the database field to an empty string instead of NULL) query = "UPDATE auth_userprofile SET country = NULL WHERE id = %s" connection.cursor().execute(query, [str(self.user.profile.id)]) # Verify that we can check the user's access without error result = embargo_api.check_course_access(self.course.id, user=self.user, ip_address='0.0.0.0') self.assertTrue(result)
def change_enrollment(strategy, auth_entry=None, user=None, *args, **kwargs): """Enroll a user in a course. If a user entered the authentication flow when trying to enroll in a course, then attempt to enroll the user. We will try to do this if the pipeline was started with the querystring param `enroll_course_id`. In the following cases, we can't enroll the user: * The course does not have an honor mode. * The course has an honor mode with a minimum price. * The course is not yet open for enrollment. * The course does not exist. If we can't enroll the user now, then skip this step. For paid courses, users will be redirected to the payment flow upon completion of the authentication pipeline (configured using the ?next parameter to the third party auth login url). Keyword Arguments: auth_entry: The entry mode into the pipeline. user (User): The user being authenticated. """ # We skip enrollment if the user entered the flow from the "link account" # button on the account settings page. At this point, either: # # 1) The user already had a linked account when they started the enrollment flow, # in which case they would have been enrolled during the normal authentication process. # # 2) The user did NOT have a linked account, in which case they would have # needed to go through the login/register page. Since we preserve the querystring # args when sending users to this page, successfully authenticating through this page # would also enroll the student in the course. enroll_course_id = strategy.session_get('enroll_course_id') if enroll_course_id and auth_entry != AUTH_ENTRY_ACCOUNT_SETTINGS: course_id = CourseKey.from_string(enroll_course_id) modes = CourseMode.modes_for_course_dict(course_id) # If the email opt in parameter is found, set the preference. email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY) if email_opt_in: opt_in = email_opt_in.lower() == 'true' update_email_opt_in(user, course_id.org, opt_in) # Check whether we're blocked from enrolling by a # country access rule. # Note: We skip checking the user's profile setting # for country here because the "redirect URL" pointing # to the blocked message page is set when the user # *enters* the pipeline, at which point they're # not authenticated. If they end up being blocked # from the courseware, it's better to let them # enroll and then show the message when they # enter the course than to skip enrollment # altogether. is_blocked = not embargo_api.check_course_access( course_id, ip_address=get_ip(strategy.request), url=strategy.request.path ) if is_blocked: # If we're blocked, skip enrollment. # A redirect URL should have been set so the user # ends up on the embargo page when enrollment completes. pass elif CourseMode.can_auto_enroll(course_id, modes_dict=modes): try: CourseEnrollment.enroll(user, course_id, check_access=True) except CourseEnrollmentException: pass except Exception as ex: logger.exception(ex) # Handle white-label courses as a special case # If a course is white-label, we should add it to the shopping cart. elif CourseMode.is_white_label(course_id, modes_dict=modes): try: cart = Order.get_cart_for_user(user) PaidCourseRegistration.add_to_order(cart, course_id) except ( CourseDoesNotExistException, ItemAlreadyInCartException, AlreadyEnrolledInCourseException, ): pass # It's more important to complete login than to # ensure that the course was added to the shopping cart. # Log errors, but don't stop the authentication pipeline. except Exception as ex: # pylint: disable=broad-except logger.exception(ex)
def test_ip_v6(self): # Test the scenario that will go through every check # (restricted course, but pass all the checks) result = embargo_api.check_course_access(self.course.id, user=self.user, ip_address='FE80::0202:B3FF:FE1E:8329') self.assertTrue(result)
def change_enrollment(strategy, auth_entry=None, user=None, *args, **kwargs): """Enroll a user in a course. If a user entered the authentication flow when trying to enroll in a course, then attempt to enroll the user. We will try to do this if the pipeline was started with the querystring param `enroll_course_id`. In the following cases, we can't enroll the user: * The course does not have an honor mode. * The course has an honor mode with a minimum price. * The course is not yet open for enrollment. * The course does not exist. If we can't enroll the user now, then skip this step. For paid courses, users will be redirected to the payment flow upon completion of the authentication pipeline (configured using the ?next parameter to the third party auth login url). Keyword Arguments: auth_entry: The entry mode into the pipeline. user (User): The user being authenticated. """ # We skip enrollment if the user entered the flow from the "link account" # button on the student dashboard. At this point, either: # # 1) The user already had a linked account when they started the enrollment flow, # in which case they would have been enrolled during the normal authentication process. # # 2) The user did NOT have a linked account, in which case they would have # needed to go through the login/register page. Since we preserve the querystring # args when sending users to this page, successfully authenticating through this page # would also enroll the student in the course. enroll_course_id = strategy.session_get('enroll_course_id') if enroll_course_id and auth_entry != AUTH_ENTRY_DASHBOARD: course_id = CourseKey.from_string(enroll_course_id) modes = CourseMode.modes_for_course_dict(course_id) # If the email opt in parameter is found, set the preference. email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY) if email_opt_in: opt_in = email_opt_in.lower() == 'true' update_email_opt_in(user, course_id.org, opt_in) # Check whether we're blocked from enrolling by a # country access rule. # Note: We skip checking the user's profile setting # for country here because the "redirect URL" pointing # to the blocked message page is set when the user # *enters* the pipeline, at which point they're # not authenticated. If they end up being blocked # from the courseware, it's better to let them # enroll and then show the message when they # enter the course than to skip enrollment # altogether. is_blocked = not embargo_api.check_course_access( course_id, ip_address=get_ip(strategy.request), url=strategy.request.path) if is_blocked: # If we're blocked, skip enrollment. # A redirect URL should have been set so the user # ends up on the embargo page when enrollment completes. pass elif CourseMode.can_auto_enroll(course_id, modes_dict=modes): try: CourseEnrollment.enroll(user, course_id, check_access=True) except CourseEnrollmentException: pass except Exception as ex: logger.exception(ex) # Handle white-label courses as a special case # If a course is white-label, we should add it to the shopping cart. elif CourseMode.is_white_label(course_id, modes_dict=modes): try: cart = Order.get_cart_for_user(user) PaidCourseRegistration.add_to_order(cart, course_id) except ( CourseDoesNotExistException, ItemAlreadyInCartException, AlreadyEnrolledInCourseException, ): pass # It's more important to complete login than to # ensure that the course was added to the shopping cart. # Log errors, but don't stop the authentication pipeline. except Exception as ex: # pylint: disable=broad-except logger.exception(ex)