Commit e71bbeb1 by Sarina Canelake

Tests for embargo middleware feature

parent fe85a1ee
...@@ -186,6 +186,8 @@ MIDDLEWARE_CLASSES = ( ...@@ -186,6 +186,8 @@ MIDDLEWARE_CLASSES = (
# Allows us to dark-launch particular languages # Allows us to dark-launch particular languages
'dark_lang.middleware.DarkLangMiddleware', 'dark_lang.middleware.DarkLangMiddleware',
'embargo.middleware.EmbargoMiddleware',
# Detects user-requested locale from 'accept-language' header in http request # Detects user-requested locale from 'accept-language' header in http request
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
...@@ -198,8 +200,6 @@ MIDDLEWARE_CLASSES = ( ...@@ -198,8 +200,6 @@ MIDDLEWARE_CLASSES = (
# for expiring inactive sessions # for expiring inactive sessions
'session_inactivity_timeout.middleware.SessionInactivityTimeout', 'session_inactivity_timeout.middleware.SessionInactivityTimeout',
'embargo.middleware.EmbargoMiddleware',
) )
############# XBlock Configuration ########## ############# XBlock Configuration ##########
......
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
Django admin page for embargo models Django admin page for embargo models
""" """
from django.contrib import admin from django.contrib import admin
import textwrap
from config_models.admin import ConfigurationModelAdmin from config_models.admin import ConfigurationModelAdmin
from embargo.models import EmbargoedCourse, EmbargoedState, IPException from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
from embargo.forms import EmbargoedCourseForm, EmbargoedStateForm, IPExceptionForm from embargo.forms import EmbargoedCourseForm, EmbargoedStateForm, IPFilterForm
class EmbargoedCourseAdmin(admin.ModelAdmin): class EmbargoedCourseAdmin(admin.ModelAdmin):
...@@ -14,13 +15,15 @@ class EmbargoedCourseAdmin(admin.ModelAdmin): ...@@ -14,13 +15,15 @@ class EmbargoedCourseAdmin(admin.ModelAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('course_id', 'embargoed'), 'fields': ('course_id', 'embargoed'),
'description': ''' 'description': textwrap.dedent("""Enter a course id in the following box.
Enter a course id in the following box. Do not enter leading or trailing slashes. There is no need to surround the course ID with quotes. Do not enter leading or trailing slashes. There is no need to surround the
course ID with quotes.
Validation will be performed on the course name, and if it is invalid, an error message will display. Validation will be performed on the course name, and if it is invalid, an
error message will display.
To enable embargos against this course (restrict course access from embargoed states), check the "Embargoed" box, then click "Save".
''' To enable embargos against this course (restrict course access from embargoed
states), check the "Embargoed" box, then click "Save".
""")
}), }),
) )
...@@ -31,31 +34,30 @@ class EmbargoedStateAdmin(ConfigurationModelAdmin): ...@@ -31,31 +34,30 @@ class EmbargoedStateAdmin(ConfigurationModelAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('embargoed_countries',), 'fields': ('embargoed_countries',),
'description': ''' 'description': textwrap.dedent("""Enter the two-letter ISO-3166-1 Alpha-2
Enter the two-letter ISO-3166-1 Alpha-2 code of the country or countries to embargo code of the country or countries to embargo in the following box. For help,
in the following box. For help, see see <a href="http://en.wikipedia.org/wiki/ISO_3166-1#Officially_assigned_code_elements">
<a href="http://en.wikipedia.org/wiki/ISO_3166-1#Officially_assigned_code_elements"> this list of ISO-3166-1 country codes</a>.
this list of ISO-3166-1 country codes</a>.
Enter the embargoed country codes separated by a comma. Do not surround with quotes.
Enter the embargoed country codes separated by a comma. Do not surround with quotes. """)
'''
}), }),
) )
class IPExceptionAdmin(ConfigurationModelAdmin): class IPFilterAdmin(ConfigurationModelAdmin):
"""Admin for blacklisting/whitelisting specific IP addresses""" """Admin for blacklisting/whitelisting specific IP addresses"""
form = IPExceptionForm form = IPFilterForm
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('whitelist', 'blacklist'), 'fields': ('whitelist', 'blacklist'),
'description': ''' 'description': textwrap.dedent("""Enter specific IP addresses to explicitly
Enter specific IP addresses to explicitly whitelist (not block) or blacklist (block) in whitelist (not block) or blacklist (block) in the appropriate box below.
the appropriate box below. Separate IP addresses with a comma. Do not surround with quotes. Separate IP addresses with a comma. Do not surround with quotes.
''' """)
}), }),
) )
admin.site.register(EmbargoedCourse, EmbargoedCourseAdmin) admin.site.register(EmbargoedCourse, EmbargoedCourseAdmin)
admin.site.register(EmbargoedState, EmbargoedStateAdmin) admin.site.register(EmbargoedState, EmbargoedStateAdmin)
admin.site.register(IPException, IPExceptionAdmin) admin.site.register(IPFilter, IPFilterAdmin)
...@@ -4,7 +4,7 @@ Defines forms for providing validation of embargo admin details. ...@@ -4,7 +4,7 @@ Defines forms for providing validation of embargo admin details.
from django import forms from django import forms
from embargo.models import EmbargoedCourse, EmbargoedState, IPException from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
from embargo.fixtures.country_codes import COUNTRY_CODES from embargo.fixtures.country_codes import COUNTRY_CODES
import socket import socket
...@@ -50,13 +50,16 @@ class EmbargoedStateForm(forms.ModelForm): # pylint: disable=incomplete-protoco ...@@ -50,13 +50,16 @@ class EmbargoedStateForm(forms.ModelForm): # pylint: disable=incomplete-protoco
def _is_valid_code(self, code): def _is_valid_code(self, code):
"""Whether or not code is a valid country code""" """Whether or not code is a valid country code"""
if len(code) == 2 and code in COUNTRY_CODES: if code in COUNTRY_CODES:
return True return True
return False return False
def clean_embargoed_countries(self): def clean_embargoed_countries(self):
"""Validate the country list""" """Validate the country list"""
embargoed_countries = self.cleaned_data["embargoed_countries"] embargoed_countries = self.cleaned_data["embargoed_countries"]
if embargoed_countries == '':
return ''
error_countries = [] error_countries = []
for country in embargoed_countries.split(','): for country in embargoed_countries.split(','):
...@@ -72,11 +75,11 @@ class EmbargoedStateForm(forms.ModelForm): # pylint: disable=incomplete-protoco ...@@ -72,11 +75,11 @@ class EmbargoedStateForm(forms.ModelForm): # pylint: disable=incomplete-protoco
return embargoed_countries return embargoed_countries
class IPExceptionForm(forms.ModelForm): # pylint: disable=incomplete-protocol class IPFilterForm(forms.ModelForm): # pylint: disable=incomplete-protocol
"""Form validating entry of IP addresses""" """Form validating entry of IP addresses"""
class Meta: # pylint: disable=missing-docstring class Meta: # pylint: disable=missing-docstring
model = IPException model = IPFilter
def _is_valid_ipv4(self, address): def _is_valid_ipv4(self, address):
"""Whether or not address is a valid ipv4 address""" """Whether or not address is a valid ipv4 address"""
......
""" """
Middleware for embargoing courses. Middleware for embargoing courses.
IMPORTANT NOTE: This code WILL NOT WORK if you have a misconfigured proxy
server. If you are configuring embargo functionality, or if you are
experiencing mysterious problems with embargoing, please check that your
reverse proxy is setting any of the well known client IP address headers (ex.,
HTTP_X_FORWARDED_FOR).
""" """
import pygeoip
import django.core.exceptions
from django.conf import settings
from django.shortcuts import redirect from django.shortcuts import redirect
from util.request import course_id_from_url
from embargo.models import EmbargoedCourse, EmbargoedState, IPException
from ipware.ip import get_ip from ipware.ip import get_ip
import pygeoip from util.request import course_id_from_url
from django.conf import settings
from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
class EmbargoMiddleware(object): class EmbargoMiddleware(object):
""" """
Middleware for embargoing courses Middleware for embargoing courses
This is configured by creating ``DarkLangConfig`` rows in the database, This is configured by creating ``EmbargoedCourse``, ``EmbargoedState``, and
using the django admin site. optionally ``IPFilter`` rows in the database, using the django admin site.
""" """
def __init__(self):
# If embargoing is turned off, make this middleware do nothing
if not settings.FEATURES.get('EMBARGO', False):
raise django.core.exceptions.MiddlewareNotUsed()
def process_request(self, request): def process_request(self, request):
""" """
...@@ -28,15 +40,15 @@ class EmbargoMiddleware(object): ...@@ -28,15 +40,15 @@ class EmbargoMiddleware(object):
# If they're trying to access a course that cares about embargoes # If they're trying to access a course that cares about embargoes
if EmbargoedCourse.is_embargoed(course_id): if EmbargoedCourse.is_embargoed(course_id):
# If we're having performance issues, add caching here # If we're having performance issues, add caching here
ip_addr = get_ip(request) ip_addr = get_ip(request)
# if blacklisted, immediately fail # if blacklisted, immediately fail
if ip_addr in IPException.current().blacklist_ips: if ip_addr in IPFilter.current().blacklist_ips:
return redirect('embargo') return redirect('embargo')
country_code_from_ip = pygeoip.GeoIP(settings.GEOIP_PATH).country_code_by_addr(ip_addr) country_code_from_ip = pygeoip.GeoIP(settings.GEOIP_PATH).country_code_by_addr(ip_addr)
is_embargoed = country_code_from_ip in EmbargoedState.current().embargoed_countries_list is_embargoed = country_code_from_ip in EmbargoedState.current().embargoed_countries_list
# Fail if country is embargoed and the ip address isn't explicitly whitelisted # Fail if country is embargoed and the ip address isn't explicitly whitelisted
if is_embargoed and ip_addr not in IPException.current().whitelist_ips: if is_embargoed and ip_addr not in IPFilter.current().whitelist_ips:
return redirect('embargo') return redirect('embargo')
...@@ -26,8 +26,8 @@ class Migration(SchemaMigration): ...@@ -26,8 +26,8 @@ class Migration(SchemaMigration):
)) ))
db.send_create_signal('embargo', ['EmbargoedState']) db.send_create_signal('embargo', ['EmbargoedState'])
# Adding model 'IPException' # Adding model 'IPFilter'
db.create_table('embargo_ipexception', ( db.create_table('embargo_ipfilter', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
...@@ -35,7 +35,7 @@ class Migration(SchemaMigration): ...@@ -35,7 +35,7 @@ class Migration(SchemaMigration):
('whitelist', self.gf('django.db.models.fields.TextField')(blank=True)), ('whitelist', self.gf('django.db.models.fields.TextField')(blank=True)),
('blacklist', self.gf('django.db.models.fields.TextField')(blank=True)), ('blacklist', self.gf('django.db.models.fields.TextField')(blank=True)),
)) ))
db.send_create_signal('embargo', ['IPException']) db.send_create_signal('embargo', ['IPFilter'])
def backwards(self, orm): def backwards(self, orm):
...@@ -45,8 +45,8 @@ class Migration(SchemaMigration): ...@@ -45,8 +45,8 @@ class Migration(SchemaMigration):
# Deleting model 'EmbargoedState' # Deleting model 'EmbargoedState'
db.delete_table('embargo_embargoedstate') db.delete_table('embargo_embargoedstate')
# Deleting model 'IPException' # Deleting model 'IPFilter'
db.delete_table('embargo_ipexception') db.delete_table('embargo_ipfilter')
models = { models = {
...@@ -100,8 +100,8 @@ class Migration(SchemaMigration): ...@@ -100,8 +100,8 @@ class Migration(SchemaMigration):
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
}, },
'embargo.ipexception': { 'embargo.ipfilter': {
'Meta': {'object_name': 'IPException'}, 'Meta': {'object_name': 'IPFilter'},
'blacklist': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 'blacklist': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
......
...@@ -60,10 +60,12 @@ class EmbargoedState(ConfigurationModel): ...@@ -60,10 +60,12 @@ class EmbargoedState(ConfigurationModel):
""" """
Return a list of upper case country codes Return a list of upper case country codes
""" """
if self.embargoed_countries == '':
return []
return [country.strip().upper() for country in self.embargoed_countries.split(',')] # pylint: disable=no-member return [country.strip().upper() for country in self.embargoed_countries.split(',')] # pylint: disable=no-member
class IPException(ConfigurationModel): class IPFilter(ConfigurationModel):
""" """
Register specific IP addresses to explicitly block or unblock. Register specific IP addresses to explicitly block or unblock.
""" """
...@@ -82,6 +84,8 @@ class IPException(ConfigurationModel): ...@@ -82,6 +84,8 @@ class IPException(ConfigurationModel):
""" """
Return a list of valid IP addresses to whitelist Return a list of valid IP addresses to whitelist
""" """
if self.whitelist == '':
return []
return [addr.strip() for addr in self.whitelist.split(',')] # pylint: disable=no-member return [addr.strip() for addr in self.whitelist.split(',')] # pylint: disable=no-member
@property @property
...@@ -89,4 +93,6 @@ class IPException(ConfigurationModel): ...@@ -89,4 +93,6 @@ class IPException(ConfigurationModel):
""" """
Return a list of valid IP addresses to blacklist Return a list of valid IP addresses to blacklist
""" """
if self.blacklist == '':
return []
return [addr.strip() for addr in self.blacklist.split(',')] # pylint: disable=no-member return [addr.strip() for addr in self.blacklist.split(',')] # pylint: disable=no-member
# -*- coding: utf-8 -*-
"""
Unit tests for embargo app admin forms.
"""
from django.test import TestCase
from django.test.utils import override_settings
# Explicitly import the cache from ConfigurationModel so we can reset it after each test
from config_models.models import cache
from embargo.forms import EmbargoedCourseForm, EmbargoedStateForm, IPFilterForm
from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class EmbargoCourseFormTest(ModuleStoreTestCase):
"""Test the course form properly validates course IDs"""
def setUp(self):
self.course = CourseFactory.create()
self.true_form_data = {'course_id': self.course.id, 'embargoed': True}
self.false_form_data = {'course_id': self.course.id, 'embargoed': False}
def tearDown(self):
# Delete any EmbargoedCourse record we may have created
try:
record = EmbargoedCourse.objects.get(course_id=self.course.id)
record.delete()
except EmbargoedCourse.DoesNotExist:
return
def test_embargo_course(self):
self.assertFalse(EmbargoedCourse.is_embargoed(self.course.id))
# Test adding embargo to this course
form = EmbargoedCourseForm(data=self.true_form_data)
# Validation should work
self.assertTrue(form.is_valid())
form.save()
# Check that this course is embargoed
self.assertTrue(EmbargoedCourse.is_embargoed(self.course.id))
def test_repeat_course(self):
# Initially course shouldn't be authorized
self.assertFalse(EmbargoedCourse.is_embargoed(self.course.id))
# Test authorizing the course, which should totally work
form = EmbargoedCourseForm(data=self.true_form_data)
# Validation should work
self.assertTrue(form.is_valid())
form.save()
# Check that this course is authorized
self.assertTrue(EmbargoedCourse.is_embargoed(self.course.id))
# Now make a new course authorization with the same course id that tries to turn email off
form = EmbargoedCourseForm(data=self.false_form_data)
# Validation should not work because course_id field is unique
self.assertFalse(form.is_valid())
self.assertEquals(
"Embargoed course with this Course id already exists.",
form._errors['course_id'][0] # pylint: disable=protected-access
)
with self.assertRaisesRegexp(ValueError, "The EmbargoedCourse could not be created because the data didn't validate."):
form.save()
# Course should still be authorized (invalid attempt had no effect)
self.assertTrue(EmbargoedCourse.is_embargoed(self.course.id))
def test_form_typo(self):
# Munge course id
bad_id = self.course.id + '_typo'
form_data = {'course_id': bad_id, 'embargoed': True}
form = EmbargoedCourseForm(data=form_data)
# Validation shouldn't work
self.assertFalse(form.is_valid())
msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(bad_id)
msg += 'Please recheck that you have supplied a valid course id.'
self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
with self.assertRaisesRegexp(ValueError, "The EmbargoedCourse could not be created because the data didn't validate."):
form.save()
def test_invalid_location(self):
# Munge course id
bad_id = self.course.id.split('/')[-1]
form_data = {'course_id': bad_id, 'embargoed': True}
form = EmbargoedCourseForm(data=form_data)
# Validation shouldn't work
self.assertFalse(form.is_valid())
msg = 'INVALID LOCATION'
msg += u' --- Entered course id was: "{0}". '.format(bad_id)
msg += 'Please recheck that you have supplied a valid course id.'
self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
with self.assertRaisesRegexp(ValueError, "The EmbargoedCourse could not be created because the data didn't validate."):
form.save()
class EmbargoedStateFormTest(TestCase):
"""Test form for adding new states"""
def tearDown(self):
# Explicitly clear ConfigurationModel's cache so tests have a clear cache
# and don't interfere with each other
cache.clear()
def test_add_valid_states(self):
# test adding valid two letter states
# case and spacing should not matter
form_data = {'embargoed_countries': 'cu, Sy , US'}
form = EmbargoedStateForm(data=form_data)
self.assertTrue(form.is_valid())
form.save()
current_embargoes = EmbargoedState.current().embargoed_countries_list
for country in ["CU", "SY", "US"]:
self.assertIn(country, current_embargoes)
# Test clearing by adding an empty list is OK too
form_data = {'embargoed_countries': ''}
form = EmbargoedStateForm(data=form_data)
self.assertTrue(form.is_valid())
form.save()
self.assertTrue(len(EmbargoedState.current().embargoed_countries_list) == 0)
def test_add_invalid_states(self):
# test adding invalid codes
# xx is not valid
# usa is not valid
form_data = {'embargoed_countries': 'usa, xx'}
form = EmbargoedStateForm(data=form_data)
self.assertFalse(form.is_valid())
msg = 'COULD NOT PARSE COUNTRY CODE(S) FOR: {0}'.format([u'USA', u'XX'])
msg += ' Please check the list of country codes and verify your entries.'
self.assertEquals(msg, form._errors['embargoed_countries'][0]) # pylint: disable=protected-access
with self.assertRaisesRegexp(ValueError, "The EmbargoedState could not be created because the data didn't validate."):
form.save()
self.assertFalse('USA' in EmbargoedState.current().embargoed_countries_list)
self.assertFalse('XX' in EmbargoedState.current().embargoed_countries_list)
class IPFilterFormTest(TestCase):
"""Test form for adding [black|white]list IP addresses"""
def tearDown(self):
# Explicitly clear ConfigurationModel's cache so tests have a clear cache
# and don't interfere with each other
cache.clear()
def test_add_valid_ips(self):
# test adding valid ip addresses
# should be able to do both ipv4 and ipv6
# spacing should not matter
form_data = {
'whitelist': '127.0.0.1, 2003:dead:beef:4dad:23:46:bb:101',
'blacklist': ' 18.244.1.5 , 2002:c0a8:101::42, 18.36.22.1'
}
form = IPFilterForm(data=form_data)
self.assertTrue(form.is_valid())
form.save()
whitelist = IPFilter.current().whitelist_ips
blacklist = IPFilter.current().blacklist_ips
for addr in '127.0.0.1, 2003:dead:beef:4dad:23:46:bb:101'.split(','):
self.assertIn(addr.strip(), whitelist)
for addr in '18.244.1.5, 2002:c0a8:101::42, 18.36.22.1'.split(','):
self.assertIn(addr.strip(), blacklist)
# Test clearing by adding an empty list is OK too
form_data = {
'whitelist': '',
'blacklist': ''
}
form = IPFilterForm(data=form_data)
self.assertTrue(form.is_valid())
form.save()
self.assertTrue(len(IPFilter.current().whitelist) == 0)
self.assertTrue(len(IPFilter.current().blacklist) == 0)
def test_add_invalid_ips(self):
# test adding invalid ip addresses
form_data = {
'whitelist': '.0.0.1, :dead:beef:::',
'blacklist': ' 18.244.* , 999999:c0a8:101::42'
}
form = IPFilterForm(data=form_data)
self.assertFalse(form.is_valid())
wmsg = "Invalid IP Address(es): [u'.0.0.1', u':dead:beef:::'] Please fix the error(s) and try again."
self.assertEquals(wmsg, form._errors['whitelist'][0]) # pylint: disable=protected-access
bmsg = "Invalid IP Address(es): [u'18.244.*', u'999999:c0a8:101::42'] Please fix the error(s) and try again."
self.assertEquals(bmsg, form._errors['blacklist'][0]) # pylint: disable=protected-access
with self.assertRaisesRegexp(ValueError, "The IPFilter could not be created because the data didn't validate."):
form.save()
...@@ -2,16 +2,21 @@ ...@@ -2,16 +2,21 @@
Tests for EmbargoMiddleware Tests for EmbargoMiddleware
""" """
from xmodule.modulestore.tests.factories import CourseFactory import mock
from django.test import TestCase import pygeoip
import unittest
from django.conf import settings
from django.test import TestCase, Client
from django.test.utils import override_settings from django.test.utils import override_settings
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from embargo.models import EmbargoedCourse, EmbargoedState
from django.test import Client
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
import mock from xmodule.modulestore.tests.factories import CourseFactory
import pygeoip
# 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
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
...@@ -35,37 +40,93 @@ class EmbargoMiddlewareTests(TestCase): ...@@ -35,37 +40,93 @@ class EmbargoMiddlewareTests(TestCase):
changed_by=self.user, changed_by=self.user,
enabled=True enabled=True
).save() ).save()
# TODO need to set up & test whitelist/blacklist IPs (IPException model)
CourseEnrollment.enroll(self.user, self.regular_course.id) CourseEnrollment.enroll(self.user, self.regular_course.id)
CourseEnrollment.enroll(self.user, self.embargo_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 particular course."
def test_countries(self): def tearDown(self):
def mock_country_code_by_addr(ip_addr): # Explicitly clear ConfigurationModel's cache so tests have a clear cache
""" # and don't interfere with each other
Gives us a fake set of IPs cache.clear()
"""
ip_dict = { def mock_country_code_by_addr(self, ip_addr):
'1.0.0.0': 'CU', """
'2.0.0.0': 'IR', Gives us a fake set of IPs
'3.0.0.0': 'SY', """
'4.0.0.0': 'SD', ip_dict = {
} '1.0.0.0': 'CU',
return ip_dict.get(ip_addr, 'US') '2.0.0.0': 'IR',
'3.0.0.0': 'SY',
'4.0.0.0': 'SD',
'5.0.0.0': 'AQ', # Antartica
}
return ip_dict.get(ip_addr, 'US')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_countries(self):
with mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr') as mocked_method: with mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr') as mocked_method:
mocked_method.side_effect = mock_country_code_by_addr mocked_method.side_effect = self.mock_country_code_by_addr
# Accessing an embargoed page from a blocked IP should cause a redirect # 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') 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) 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 course from a blocked IP should succeed # 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') 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, 404) 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 any course from non-embaroged IPs should succeed # 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') 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, 404) self.assertEqual(response.status_code, 200)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
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()
with mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr') as mocked_method:
mocked_method.side_effect = self.mock_country_code_by_addr
# 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') 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, 404) 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)
"""Test of models for embargo middleware app"""
from django.test import TestCase
from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
class EmbargoModelsTest(TestCase):
"""Test each of the 3 models in embargo.models"""
def test_course_embargo(self):
course_id = 'abc/123/doremi'
# Test that course is not authorized by default
self.assertFalse(EmbargoedCourse.is_embargoed(course_id))
# Authorize
cauth = EmbargoedCourse(course_id=course_id, embargoed=True)
cauth.save()
# Now, course should be embargoed
self.assertTrue(EmbargoedCourse.is_embargoed(course_id))
self.assertEquals(
cauth.__unicode__(),
"Course 'abc/123/doremi' is Embargoed"
)
# Unauthorize by explicitly setting email_enabled to False
cauth.embargoed = False
cauth.save()
# Test that course is now unauthorized
self.assertFalse(EmbargoedCourse.is_embargoed(course_id))
self.assertEquals(
cauth.__unicode__(),
"Course 'abc/123/doremi' is Not Embargoed"
)
def test_state_embargo(self):
# Azerbaijan and France should not be blocked
good_states = ['AZ', 'FR']
# Gah block USA and Antartica
blocked_states = ['US', 'AQ']
currently_blocked = EmbargoedState.current().embargoed_countries_list
for state in blocked_states + good_states:
self.assertFalse(state in currently_blocked)
# Block
cauth = EmbargoedState(embargoed_countries='US, AQ')
cauth.save()
currently_blocked = EmbargoedState.current().embargoed_countries_list
for state in good_states:
self.assertFalse(state in currently_blocked)
for state in blocked_states:
self.assertTrue(state in currently_blocked)
# Change embargo - block Isle of Man too
blocked_states.append('IM')
cauth.embargoed_countries = 'US, AQ, IM'
cauth.save()
currently_blocked = EmbargoedState.current().embargoed_countries_list
for state in good_states:
self.assertFalse(state in currently_blocked)
for state in blocked_states:
self.assertTrue(state in currently_blocked)
def test_ip_blocking(self):
whitelist = '127.0.0.1'
blacklist = '18.244.51.3'
cwhitelist = IPFilter.current().whitelist_ips
self.assertFalse(whitelist in cwhitelist)
cblacklist = IPFilter.current().blacklist_ips
self.assertFalse(blacklist in cblacklist)
IPFilter(whitelist=whitelist, blacklist=blacklist).save()
cwhitelist = IPFilter.current().whitelist_ips
self.assertTrue(whitelist in cwhitelist)
cblacklist = IPFilter.current().blacklist_ips
self.assertTrue(blacklist in cblacklist)
"""Generates common contexts""" """Generates common contexts"""
import re
import logging import logging
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from util.request import COURSE_REGEX
COURSE_REGEX = re.compile(r'^.*?/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
""" Utility functions related to HTTP requests """ """ Utility functions related to HTTP requests """
import re
from django.conf import settings from django.conf import settings
from microsite_configuration.middleware import MicrositeConfiguration from microsite_configuration.middleware import MicrositeConfiguration
from track.contexts import COURSE_REGEX
COURSE_REGEX = re.compile(r'^.*?/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)')
def safe_get_host(request): def safe_get_host(request):
......
...@@ -703,6 +703,7 @@ MIDDLEWARE_CLASSES = ( ...@@ -703,6 +703,7 @@ MIDDLEWARE_CLASSES = (
# Allows us to dark-launch particular languages # Allows us to dark-launch particular languages
'dark_lang.middleware.DarkLangMiddleware', 'dark_lang.middleware.DarkLangMiddleware',
'embargo.middleware.EmbargoMiddleware',
# Allows us to set user preferences # Allows us to set user preferences
# should be after DarkLangMiddleware # should be after DarkLangMiddleware
...@@ -728,7 +729,6 @@ MIDDLEWARE_CLASSES = ( ...@@ -728,7 +729,6 @@ MIDDLEWARE_CLASSES = (
# for expiring inactive sessions # for expiring inactive sessions
'session_inactivity_timeout.middleware.SessionInactivityTimeout', 'session_inactivity_timeout.middleware.SessionInactivityTimeout',
'embargo.middleware.EmbargoMiddleware',
) )
############################### Pipeline ####################################### ############################### Pipeline #######################################
......
...@@ -4,5 +4,5 @@ ...@@ -4,5 +4,5 @@
<%block name="pagetitle">${_("This Course Unavailable In Your Country")}</%block> <%block name="pagetitle">${_("This Course Unavailable In Your Country")}</%block>
<section class="outside-app"> <section class="outside-app">
<p>${_('Our system indicates that you are trying to access an edX course from an IP address associated with a country currently subjected to U.S. economic and trade sanctions. Unfortunately, at this time edX must comply with export controls, and we cannot allow you to register for this particular course. Feel free to browse our catalogue to find other courses you may be interested in taking.')}</p> <p>${_("Our system indicates that you are trying to access an edX course from an IP address associated with a country currently subjected to U.S. economic and trade sanctions. Unfortunately, at this time edX must comply with export controls, and we cannot allow you to access this particular course. Feel free to browse our catalogue to find other courses you may be interested in taking.")}</p>
</section> </section>
...@@ -13,9 +13,7 @@ ...@@ -13,9 +13,7 @@
-e git+https://github.com/gabrielfalcao/lettuce.git@cccc3978ad2df82a78b6f9648fe2e9baddd22f88#egg=lettuce -e git+https://github.com/gabrielfalcao/lettuce.git@cccc3978ad2df82a78b6f9648fe2e9baddd22f88#egg=lettuce
-e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev -e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk -e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# TODO clear this library with appropriate people
-e git+https://github.com/un33k/django-ipware.git@42cb1bb1dc680a60c6452e8bb2b843c2a0382c90#egg=django-ipware -e git+https://github.com/un33k/django-ipware.git@42cb1bb1dc680a60c6452e8bb2b843c2a0382c90#egg=django-ipware
# TODO clear this library with appropriate people
-e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip -e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip
# Our libraries: # Our libraries:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment