Commit fe85a1ee by Sarina Canelake

Django-admin for Embargo feature

Allows specification of countries to embargo, what course(s) should
apply embargo restrictions, and whitelist/blacklist capability for
specific individual IP addresses.
parent a7ae152d
"""
Django admin page for embargo models
"""
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from embargo.models import EmbargoedCourse, EmbargoedState, IPException
from embargo.forms import EmbargoedCourseForm, EmbargoedStateForm, IPExceptionForm
class EmbargoedCourseAdmin(admin.ModelAdmin):
"""Admin for embargoed course ids"""
form = EmbargoedCourseForm
fieldsets = (
(None, {
'fields': ('course_id', 'embargoed'),
'description': '''
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.
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".
'''
}),
)
class EmbargoedStateAdmin(ConfigurationModelAdmin):
"""Admin for embargoed countries"""
form = EmbargoedStateForm
fieldsets = (
(None, {
'fields': ('embargoed_countries',),
'description': '''
Enter the two-letter ISO-3166-1 Alpha-2 code of the country or countries to embargo
in the following box. For help, see
<a href="http://en.wikipedia.org/wiki/ISO_3166-1#Officially_assigned_code_elements">
this list of ISO-3166-1 country codes</a>.
Enter the embargoed country codes separated by a comma. Do not surround with quotes.
'''
}),
)
class IPExceptionAdmin(ConfigurationModelAdmin):
"""Admin for blacklisting/whitelisting specific IP addresses"""
form = IPExceptionForm
fieldsets = (
(None, {
'fields': ('whitelist', 'blacklist'),
'description': '''
Enter specific IP addresses to explicitly whitelist (not block) or blacklist (block) in
the appropriate box below. Separate IP addresses with a comma. Do not surround with quotes.
'''
}),
)
admin.site.register(EmbargoedCourse, EmbargoedCourseAdmin)
admin.site.register(EmbargoedState, EmbargoedStateAdmin)
admin.site.register(IPException, IPExceptionAdmin)
"""
List of valid ISO 3166-1 Alpha-2 country codes, used for
validating entries on entered country codes on django-admin page.
"""
COUNTRY_CODES = set([
"AC", "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AN", "AO", "AQ", "AR", "AS", "AT",
"AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BM",
"BN", "BO", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG",
"CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CX", "CY", "CZ", "DE",
"DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ER", "ES", "ET", "FI", "FJ", "FK",
"FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN",
"GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU",
"ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP",
"KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC",
"LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MG", "MH",
"MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX",
"MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ",
"OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PT", "PW", "PY",
"QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI",
"SJ", "SK", "SL", "SM", "SN", "SO", "SR", "ST", "SV", "SY", "SZ", "TA", "TC", "TD",
"TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ",
"UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF",
"WS", "YE", "YT", "ZA", "ZM", "ZW"
])
"""
Defines forms for providing validation of embargo admin details.
"""
from django import forms
from embargo.models import EmbargoedCourse, EmbargoedState, IPException
from embargo.fixtures.country_codes import COUNTRY_CODES
import socket
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
class EmbargoedCourseForm(forms.ModelForm): # pylint: disable=incomplete-protocol
"""Form providing validation of entered Course IDs."""
class Meta: # pylint: disable=missing-docstring
model = EmbargoedCourse
def clean_course_id(self):
"""Validate the course id"""
course_id = self.cleaned_data["course_id"]
try:
# Try to get the course descriptor, if we can do that,
# it's a real course.
course_loc = CourseDescriptor.id_to_location(course_id)
modulestore().get_instance(course_id, course_loc, depth=1)
except (KeyError, ItemNotFoundError):
msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(course_id)
msg += 'Please recheck that you have supplied a valid course id.'
raise forms.ValidationError(msg)
except (ValueError, InvalidLocationError):
msg = 'INVALID LOCATION'
msg += u' --- Entered course id was: "{0}". '.format(course_id)
msg += 'Please recheck that you have supplied a valid course id.'
raise forms.ValidationError(msg)
return course_id
class EmbargoedStateForm(forms.ModelForm): # pylint: disable=incomplete-protocol
"""Form validating entry of states to embargo"""
class Meta: # pylint: disable=missing-docstring
model = EmbargoedState
def _is_valid_code(self, code):
"""Whether or not code is a valid country code"""
if len(code) == 2 and code in COUNTRY_CODES:
return True
return False
def clean_embargoed_countries(self):
"""Validate the country list"""
embargoed_countries = self.cleaned_data["embargoed_countries"]
error_countries = []
for country in embargoed_countries.split(','):
country = country.strip().upper()
if not self._is_valid_code(country):
error_countries.append(country)
if error_countries:
msg = 'COULD NOT PARSE COUNTRY CODE(S) FOR: {0}'.format(error_countries)
msg += ' Please check the list of country codes and verify your entries.'
raise forms.ValidationError(msg)
return embargoed_countries
class IPExceptionForm(forms.ModelForm): # pylint: disable=incomplete-protocol
"""Form validating entry of IP addresses"""
class Meta: # pylint: disable=missing-docstring
model = IPException
def _is_valid_ipv4(self, address):
"""Whether or not address is a valid ipv4 address"""
try:
# Is this an ipv4 address?
socket.inet_pton(socket.AF_INET, address)
except socket.error:
return False
return True
def _is_valid_ipv6(self, address):
"""Whether or not address is a valid ipv6 address"""
try:
# Is this an ipv6 address?
socket.inet_pton(socket.AF_INET6, address)
except socket.error:
return False
return True
def _valid_ip_addresses(self, addresses):
"""
Checks if a csv string of IP addresses contains valid values.
If not, raises a ValidationError.
"""
if addresses == '':
return ''
error_addresses = []
for addr in addresses.split(','):
address = addr.strip()
if not (self._is_valid_ipv4(address) or self._is_valid_ipv6(address)):
error_addresses.append(address)
if error_addresses:
msg = 'Invalid IP Address(es): {0}'.format(error_addresses)
msg += ' Please fix the error(s) and try again.'
raise forms.ValidationError(msg)
return addresses
def clean_whitelist(self):
"""Validates the whitelist"""
whitelist = self.cleaned_data["whitelist"]
return self._valid_ip_addresses(whitelist)
def clean_blacklist(self):
"""Validates the blacklist"""
blacklist = self.cleaned_data["blacklist"]
return self._valid_ip_addresses(blacklist)
......@@ -5,7 +5,7 @@ Middleware for embargoing courses.
from django.shortcuts import redirect
from util.request import course_id_from_url
from embargo.models import EmbargoConfig
from embargo.models import EmbargoedCourse, EmbargoedState, IPException
from ipware.ip import get_ip
import pygeoip
from django.conf import settings
......@@ -27,11 +27,16 @@ class EmbargoMiddleware(object):
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 EmbargoedCourse.is_embargoed(course_id):
# 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:
ip_addr = get_ip(request)
# if blacklisted, immediately fail
if ip_addr in IPException.current().blacklist_ips:
return redirect('embargo')
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
# 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:
return redirect('embargo')
......@@ -8,21 +8,45 @@ from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'EmbargoConfig'
db.create_table('embargo_embargoconfig', (
# Adding model 'EmbargoedCourse'
db.create_table('embargo_embargoedcourse', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255, db_index=True)),
('embargoed', self.gf('django.db.models.fields.BooleanField')(default=False)),
))
db.send_create_signal('embargo', ['EmbargoedCourse'])
# Adding model 'EmbargoedState'
db.create_table('embargo_embargoedstate', (
('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'])
db.send_create_signal('embargo', ['EmbargoedState'])
# Adding model 'IPException'
db.create_table('embargo_ipexception', (
('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)),
('whitelist', 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'])
def backwards(self, orm):
# Deleting model 'EmbargoConfig'
db.delete_table('embargo_embargoconfig')
# Deleting model 'EmbargoedCourse'
db.delete_table('embargo_embargoedcourse')
# Deleting model 'EmbargoedState'
db.delete_table('embargo_embargoedstate')
# Deleting model 'IPException'
db.delete_table('embargo_ipexception')
models = {
......@@ -62,14 +86,28 @@ class Migration(SchemaMigration):
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'embargo.embargoconfig': {
'Meta': {'object_name': 'EmbargoConfig'},
'embargo.embargoedcourse': {
'Meta': {'object_name': 'EmbargoedCourse'},
'course_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
'embargoed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.embargoedstate': {
'Meta': {'object_name': 'EmbargoedState'},
'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'})
},
'embargo.ipexception': {
'Meta': {'object_name': 'IPException'},
'blacklist': ('django.db.models.fields.TextField', [], {'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'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'whitelist': ('django.db.models.fields.TextField', [], {'blank': 'True'})
}
}
......
"""
Models for embargoing countries
Models for embargoing visits to certain courses by IP address.
WE'RE USING MIGRATIONS!
If you make changes to this model, be sure to create an appropriate migration
file and check it in at the same time as your model changes. To do that,
1. Go to the edx-platform dir
2. ./manage.py lms schemamigration embargo --auto description_of_your_change
3. Add the migration file created in edx-platform/common/djangoapps/embargo/migrations/
"""
from django.db import models
from config_models.models import ConfigurationModel
class EmbargoConfig(ConfigurationModel):
class EmbargoedCourse(models.Model):
"""
Enable course embargo on a course-by-course basis.
"""
# The course to embargo
course_id = models.CharField(max_length=255, db_index=True, unique=True)
# Whether or not to embargo
embargoed = models.BooleanField(default=False)
@classmethod
def is_embargoed(cls, course_id):
"""
Returns whether or not the given course id is embargoed.
If course has not been explicitly embargoed, returns False.
"""
try:
record = cls.objects.get(course_id=course_id)
return record.embargoed
except cls.DoesNotExist:
return False
def __unicode__(self):
not_em = "Not "
if self.embargoed:
not_em = ""
return u"Course '{}' is {}Embargoed".format(self.course_id, not_em)
class EmbargoedState(ConfigurationModel):
"""
Configuration for the embargo feature
Register countries to be embargoed.
"""
# The countries to embargo
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(
@property
def embargoed_countries_list(self):
"""
Return a list of upper case country codes
"""
return [country.strip().upper() for country in self.embargoed_countries.split(',')] # pylint: disable=no-member
class IPException(ConfigurationModel):
"""
Register specific IP addresses to explicitly block or unblock.
"""
whitelist = models.TextField(
blank=True,
help_text="A comma-separated list of IP addresses that should not fall under embargo restrictions."
)
blacklist = models.TextField(
blank=True,
help_text="A comma-separated list of course IDs that we are enforcing the embargo for"
help_text="A comma-separated list of IP addresses that should fall under embargo restrictions."
)
@property
def embargoed_countries_list(self):
def whitelist_ips(self):
"""
Returns list of embargoed countries
Return a list of valid IP addresses to whitelist
"""
if not self.embargoed_countries.strip():
return []
return [country.strip() for country in self.embargoed_countries.split(',')]
return [addr.strip() for addr in self.whitelist.split(',')] # pylint: disable=no-member
@property
def embargoed_courses_list(self):
def blacklist_ips(self):
"""
Returns list of embargoed courses
Return a list of valid IP addresses to blacklist
"""
if not self.embargoed_courses.strip():
return []
return [course.strip() for course in self.embargoed_courses.split(',')]
return [addr.strip() for addr in self.blacklist.split(',')] # pylint: disable=no-member
......@@ -6,7 +6,7 @@ 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 embargo.models import EmbargoedCourse, EmbargoedState
from django.test import Client
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
......@@ -29,18 +29,18 @@ class EmbargoMiddlewareTests(TestCase):
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,
EmbargoedCourse(course_id=self.embargo_course.id, embargoed=True).save()
EmbargoedState(
embargoed_countries="cu, ir, Sy, SD",
changed_by=self.user,
enabled=True
).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.embargo_course.id)
def test_countries(self):
def mock_country_code_by_addr(ip):
def mock_country_code_by_addr(ip_addr):
"""
Gives us a fake set of IPs
"""
......@@ -50,7 +50,7 @@ class EmbargoMiddlewareTests(TestCase):
'3.0.0.0': 'SY',
'4.0.0.0': 'SD',
}
return ip_dict.get(ip, 'US')
return ip_dict.get(ip_addr, 'US')
with mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr') as mocked_method:
mocked_method.side_effect = mock_country_code_by_addr
......
......@@ -140,7 +140,7 @@ def _get_date_for_press(publish_date):
return date
def embargo(request):
def embargo(_request):
"""
Render the embargo page.
......
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