Commit a7ae152d by Julia Hansbrough Committed by Sarina Canelake

Embargo Middleware feature

Adds configurable middleware in common/djangoapps/embargo that
allows specific courses to comply with US Export regulations by
embargoing students from specific countries, whilst simultaneously
allowing other courses to be freely open to all.
parent 331a94c1
......@@ -97,6 +97,9 @@ sys.path.append(PROJECT_ROOT / 'lib')
sys.path.append(COMMON_ROOT / 'djangoapps')
sys.path.append(COMMON_ROOT / 'lib')
# For geolocation ip database
GEOIP_PATH = REPO_ROOT / "common/static/data/geoip/GeoIP.dat"
############################# WEB CONFIGURATION #############################
# This is where we stick our compiled template files.
......@@ -195,6 +198,8 @@ MIDDLEWARE_CLASSES = (
# for expiring inactive sessions
'session_inactivity_timeout.middleware.SessionInactivityTimeout',
'embargo.middleware.EmbargoMiddleware',
)
############# XBlock Configuration ##########
......@@ -465,6 +470,8 @@ INSTALLED_APPS = (
# User preferences
'user_api',
'django_openid_auth',
'embargo',
)
......
......@@ -51,6 +51,7 @@ urlpatterns += patterns(
# ajax view that actually does the work
url(r'^login_post$', 'student.views.login_user', name='login_post'),
url(r'^logout$', 'student.views.logout_user', name='logout'),
url(r'^embargo$', 'student.views.embargo', name="embargo"),
)
# restful api
......
"""
Middleware for embargoing courses.
"""
from django.shortcuts import redirect
from util.request import course_id_from_url
from embargo.models import EmbargoConfig
from ipware.ip import get_ip
import pygeoip
from django.conf import settings
class EmbargoMiddleware(object):
"""
Middleware for embargoing courses
This is configured by creating ``DarkLangConfig`` rows in the database,
using the django admin site.
"""
def process_request(self, request):
"""
Processes embargo requests
"""
url = request.path
course_id = course_id_from_url(url)
# If they're trying to access a course that cares about embargoes
if course_id in EmbargoConfig.current().embargoed_courses_list:
# If we're having performance issues, add caching here
ip = get_ip(request)
country_code_from_ip = pygeoip.GeoIP(settings.GEOIP_PATH).country_code_by_addr(ip)
is_embargoed = (country_code_from_ip in EmbargoConfig.current().embargoed_countries_list)
if is_embargoed:
return redirect('embargo')
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'EmbargoConfig'
db.create_table('embargo_embargoconfig', (
('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)),
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
('embargoed_countries', self.gf('django.db.models.fields.TextField')(blank=True)),
('embargoed_courses', self.gf('django.db.models.fields.TextField')(blank=True)),
))
db.send_create_signal('embargo', ['EmbargoConfig'])
def backwards(self, orm):
# Deleting model 'EmbargoConfig'
db.delete_table('embargo_embargoconfig')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'embargo.embargoconfig': {
'Meta': {'object_name': 'EmbargoConfig'},
'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'}),
'embargoed_countries': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'embargoed_courses': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
}
}
complete_apps = ['embargo']
\ No newline at end of file
"""
Models for embargoing countries
"""
from django.db import models
from config_models.models import ConfigurationModel
class EmbargoConfig(ConfigurationModel):
"""
Configuration for the embargo feature
"""
embargoed_countries = models.TextField(
blank=True,
help_text="A comma-separated list of country codes that fall under U.S. embargo restrictions"
)
embargoed_courses = models.TextField(
blank=True,
help_text="A comma-separated list of course IDs that we are enforcing the embargo for"
)
@property
def embargoed_countries_list(self):
"""
Returns list of embargoed countries
"""
if not self.embargoed_countries.strip():
return []
return [country.strip() for country in self.embargoed_countries.split(',')]
@property
def embargoed_courses_list(self):
"""
Returns list of embargoed courses
"""
if not self.embargoed_courses.strip():
return []
return [course.strip() for course in self.embargoed_courses.split(',')]
"""
Tests for EmbargoMiddleware
"""
from xmodule.modulestore.tests.factories import CourseFactory
from django.test import TestCase
from django.test.utils import override_settings
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from embargo.models import EmbargoConfig
from django.test import Client
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
import mock
import pygeoip
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class EmbargoMiddlewareTests(TestCase):
"""
Tests of EmbargoMiddleware
"""
def setUp(self):
self.client = Client()
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 + '/info'
self.regular_page = '/courses/' + self.regular_course.id + '/info'
EmbargoConfig(
embargoed_countries="CU, IR, SY,SD",
embargoed_courses=self.embargo_course.id,
changed_by=self.user,
enabled=True
).save()
CourseEnrollment.enroll(self.user, self.regular_course.id)
CourseEnrollment.enroll(self.user, self.embargo_course.id)
def test_countries(self):
def mock_country_code_by_addr(ip):
"""
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',
}
return ip_dict.get(ip, 'US')
with mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr') as mocked_method:
mocked_method.side_effect = mock_country_code_by_addr
# 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)
# Accessing a regular course 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, 404)
# Accessing any course from non-embaroged IPs 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, 404)
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)
......@@ -140,6 +140,15 @@ def _get_date_for_press(publish_date):
return date
def embargo(request):
"""
Render the embargo page.
Explains to the user why they are not able to access a particular embargoed course.
"""
return render_to_response('static_templates/embargo.html')
def press(request):
json_articles = cache.get("student_press_json_articles")
if json_articles is None:
......@@ -718,7 +727,7 @@ def login_user(request, error=""):
# This is actually the common case, logging in user without external linked login
AUDIT_LOG.info("User %s w/o external auth attempting login", user)
# see if account has been locked out due to excessive login failres
# see if account has been locked out due to excessive login failures
user_found_by_email_lookup = user
if user_found_by_email_lookup and LoginFailures.is_feature_enabled():
if LoginFailures.is_user_locked_out(user_found_by_email_lookup):
......
""" Utility functions related to HTTP requests """
from django.conf import settings
from microsite_configuration.middleware import MicrositeConfiguration
from track.contexts import COURSE_REGEX
def safe_get_host(request):
......@@ -16,3 +17,17 @@ def safe_get_host(request):
return request.get_host()
else:
return MicrositeConfiguration.get_microsite_configuration_value('site_domain', settings.SITE_NAME)
def course_id_from_url(url):
"""
Extracts the course_id from the given `url`.
"""
url = url or ''
match = COURSE_REGEX.match(url)
course_id = ''
if match:
course_id = match.group('course_id') or ''
return course_id
This product includes GeoLite data created by MaxMind, available from
http://www.maxmind.com.
\ No newline at end of file
......@@ -256,6 +256,9 @@ node_paths = [
]
NODE_PATH = ':'.join(node_paths)
# For geolocation ip database
GEOIP_PATH = REPO_ROOT / "common/static/data/geoip/GeoIP.dat"
# Where to look for a status message
STATUS_MESSAGE_PATH = ENV_ROOT / "status_message.json"
......@@ -724,6 +727,8 @@ MIDDLEWARE_CLASSES = (
# for expiring inactive sessions
'session_inactivity_timeout.middleware.SessionInactivityTimeout',
'embargo.middleware.EmbargoMiddleware',
)
############################### Pipeline #######################################
......@@ -1138,6 +1143,8 @@ INSTALLED_APPS = (
# Student Identity Reverification
'reverification',
'embargo',
)
######################### MARKETING SITE ###############################
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="../main.html" />
<%block name="pagetitle">${_("This Course Unavailable In Your Country")}</%block>
<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>
</section>
......@@ -11,7 +11,6 @@ if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
urlpatterns = ('', # nopep8
# certificate view
url(r'^update_certificate$', 'certificates.views.update_certificate'),
url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
......@@ -66,6 +65,8 @@ urlpatterns = ('', # nopep8
url(r'^', include('waffle.urls')),
url(r'^i18n/', include('django.conf.urls.i18n')),
url(r'^embargo$', 'student.views.embargo', name="embargo"),
)
# if settings.FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"):
......
......@@ -13,6 +13,10 @@
-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/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
# TODO clear this library with appropriate people
-e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip
# Our libraries:
-e git+https://github.com/edx/XBlock.git@893cd83dfb24405ce81b07f49c1c2e3053cdc865#egg=XBlock
......
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