""" Tests for EmbargoMiddleware """ import mock import pygeoip import unittest from django.core.urlresolvers import reverse from django.conf import settings from django.db import connection, transaction from django.test.utils import override_settings import ddt from student.models import CourseEnrollment from student.tests.factories import UserFactory from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ( ModuleStoreTestCase, mixed_store_config ) # Explicitly import the cache from ConfigurationModel so we can reset it after each test from config_models.models import cache from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter @ddt.ddt @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') class EmbargoMiddlewareTests(ModuleStoreTestCase): """ Tests of EmbargoMiddleware """ def setUp(self): super(EmbargoMiddlewareTests, self).setUp() self.user = UserFactory(username='fred', password='secret') self.client.login(username='fred', password='secret') self.embargo_course = CourseFactory.create() self.embargo_course.save() self.regular_course = CourseFactory.create(org="Regular") self.regular_course.save() self.embargoed_page = '/courses/' + self.embargo_course.id.to_deprecated_string() + '/info' self.regular_page = '/courses/' + self.regular_course.id.to_deprecated_string() + '/info' EmbargoedCourse(course_id=self.embargo_course.id, embargoed=True).save() EmbargoedState( embargoed_countries="cu, ir, Sy, SD", changed_by=self.user, enabled=True ).save() CourseEnrollment.enroll(self.user, self.regular_course.id) CourseEnrollment.enroll(self.user, self.embargo_course.id) # Text from lms/templates/static_templates/embargo.html self.embargo_text = "Unfortunately, at this time edX must comply with export controls, and we cannot allow you to access this course." self.patcher = mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr', self.mock_country_code_by_addr) self.patcher.start() def tearDown(self): # Explicitly clear ConfigurationModel's cache so tests have a clear cache # and don't interfere with each other cache.clear() self.patcher.stop() def mock_country_code_by_addr(self, ip_addr): """ Gives us a fake set of IPs """ ip_dict = { '1.0.0.0': 'CU', '2.0.0.0': 'IR', '3.0.0.0': 'SY', '4.0.0.0': 'SD', '5.0.0.0': 'AQ', # Antartica '2001:250::': 'CN', '2001:1340::': 'CU', } return ip_dict.get(ip_addr, 'US') def test_countries(self): # Accessing an embargoed page from a blocked IP should cause a redirect response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 302) # Following the redirect should give us the embargo page response = self.client.get( self.embargoed_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0', follow=True ) self.assertIn(self.embargo_text, response.content) # Accessing a regular page from a blocked IP should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 200) # Accessing an embargoed page from a non-embargoed IP should succeed response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0') self.assertEqual(response.status_code, 200) # Accessing a regular page from a non-embargoed IP should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0') self.assertEqual(response.status_code, 200) def test_countries_ipv6(self): # Accessing an embargoed page from a blocked IP should cause a redirect response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='2001:1340::', REMOTE_ADDR='2001:1340::') self.assertEqual(response.status_code, 302) # Following the redirect should give us the embargo page response = self.client.get( self.embargoed_page, HTTP_X_FORWARDED_FOR='2001:1340::', REMOTE_ADDR='2001:1340::', follow=True ) self.assertIn(self.embargo_text, response.content) # Accessing a regular page from a blocked IP should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='2001:1340::', REMOTE_ADDR='2001:1340::') self.assertEqual(response.status_code, 200) # Accessing an embargoed page from a non-embargoed IP should succeed response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='2001:250::', REMOTE_ADDR='2001:250::') self.assertEqual(response.status_code, 200) # Accessing a regular page from a non-embargoed IP should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='2001:250::', REMOTE_ADDR='2001:250::') self.assertEqual(response.status_code, 200) def test_ip_exceptions(self): # Explicitly whitelist/blacklist some IPs IPFilter( whitelist='1.0.0.0', blacklist='5.0.0.0', changed_by=self.user, enabled=True ).save() # Accessing an embargoed page from a blocked IP that's been whitelisted # should succeed response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 200) # Accessing a regular course from a blocked IP that's been whitelisted should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 200) # Accessing an embargoed course from non-embargoed IP that's been blacklisted # should cause a redirect response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0') self.assertEqual(response.status_code, 302) # Following the redirect should give us the embargo page response = self.client.get( self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='1.0.0.0', follow=True ) self.assertIn(self.embargo_text, response.content) # Accessing a regular course from a non-embargoed IP that's been blacklisted should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0') self.assertEqual(response.status_code, 200) def test_ip_network_exceptions(self): # Explicitly whitelist/blacklist some IP networks IPFilter( whitelist='1.0.0.1/24', blacklist='5.0.0.0/16,1.1.0.0/24', changed_by=self.user, enabled=True ).save() # Accessing an embargoed page from a blocked IP that's been whitelisted with a network # should succeed response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 200) # Accessing a regular course from a blocked IP that's been whitelisted with a network # should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 200) # Accessing an embargoed course from non-embargoed IP that's been blacklisted with a network # should cause a redirect response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.100', REMOTE_ADDR='5.0.0.100') self.assertEqual(response.status_code, 302) # Following the redirect should give us the embargo page response = self.client.get( self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.100', REMOTE_ADDR='5.0.0.100', follow=True ) self.assertIn(self.embargo_text, response.content) # Accessing an embargoed course from non-embargoed IP that's been blaclisted with a network # should cause a redirect response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.1.0.1', REMOTE_ADDR='1.1.0.1') self.assertEqual(response.status_code, 302) # Following the redirect should give us the embargo page response = self.client.get( self.embargoed_page, HTTP_X_FORWARDED_FOR='1.1.0.0', REMOTE_ADDR='1.1.0.0', follow=True ) self.assertIn(self.embargo_text, response.content) # Accessing an embargoed from a blocked IP that's not blacklisted by the network rule. # should succeed response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.1.1.0', REMOTE_ADDR='1.1.1.0') self.assertEqual(response.status_code, 200) # Accessing a regular course from a non-embargoed IP that's been blacklisted # should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0') self.assertEqual(response.status_code, 200) @ddt.data( (None, False), ("", False), ("us", False), ("CU", True), ("Ir", True), ("sy", True), ("sd", True) ) @ddt.unpack def test_embargo_profile_country(self, profile_country, is_embargoed): # Set the country in the user's profile profile = self.user.profile profile.country = profile_country profile.save() # Attempt to access an embargoed course response = self.client.get(self.embargoed_page) # If the user is from an embargoed country, verify that # they are redirected to the embargo page. if is_embargoed: embargo_url = reverse('embargo') self.assertRedirects(response, embargo_url) # Otherwise, verify that the student can access the page else: self.assertEqual(response.status_code, 200) # For non-embargoed courses, the student should be able to access # the page, even if he/she is from an embargoed country. response = self.client.get(self.regular_page) self.assertEqual(response.status_code, 200) def test_embargo_profile_country_cache(self): # Set the country in the user's profile profile = self.user.profile profile.country = "us" profile.save() # Warm the cache with self.assertNumQueries(16): self.client.get(self.embargoed_page) # Access the page multiple times, but expect that we hit # the database to check the user's profile only once with self.assertNumQueries(10): self.client.get(self.embargoed_page) def test_embargo_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)]) transaction.commit_unless_managed() # Attempt to access an embargoed course # Verify that the student can access the page without an error response = self.client.get(self.embargoed_page) self.assertEqual(response.status_code, 200) @mock.patch.dict(settings.FEATURES, {'EMBARGO': False}) def test_countries_embargo_off(self): # When the middleware is turned off, all requests should go through # Accessing an embargoed page from a blocked IP OK response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 200) # Accessing a regular page from a blocked IP should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 200) # Explicitly whitelist/blacklist some IPs IPFilter( whitelist='1.0.0.0', blacklist='5.0.0.0', changed_by=self.user, enabled=True ).save() # Accessing an embargoed course from non-embargoed IP that's been blacklisted # should be OK response = self.client.get(self.embargoed_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0') self.assertEqual(response.status_code, 200) # Accessing a regular course from a non-embargoed IP that's been blacklisted should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0') self.assertEqual(response.status_code, 200) @mock.patch.dict(settings.FEATURES, {'EMBARGO': False, 'SITE_EMBARGOED': True}) def test_embargo_off_embargo_site_on(self): # When the middleware is turned on with SITE, main site access should be restricted # Accessing a regular page from a blocked IP is denied. response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 403) # Accessing a regular page from a non blocked IP should succeed response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='5.0.0.0', REMOTE_ADDR='5.0.0.0') self.assertEqual(response.status_code, 200) @mock.patch.dict(settings.FEATURES, {'EMBARGO': False, 'SITE_EMBARGOED': True}) @override_settings(EMBARGO_SITE_REDIRECT_URL='https://www.edx.org/') def test_embargo_off_embargo_site_on_with_redirect_url(self): # When the middleware is turned on with SITE_EMBARGOED, main site access # should be restricted. Accessing a regular page from a blocked IP is # denied, and redirected to EMBARGO_SITE_REDIRECT_URL rather than returning a 403. response = self.client.get(self.regular_page, HTTP_X_FORWARDED_FOR='1.0.0.0', REMOTE_ADDR='1.0.0.0') self.assertEqual(response.status_code, 302)