Merge pull request #7583 from edx/hotfix/2015-04-03
Hotfix: Add proxy to allow IE9 to make xdomain requests
This commit is contained in:
7
common/djangoapps/cors_csrf/admin.py
Normal file
7
common/djangoapps/cors_csrf/admin.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Manage cross-domain configuration. """
|
||||
from django.contrib import admin
|
||||
from config_models.admin import ConfigurationModelAdmin
|
||||
from cors_csrf.models import XDomainProxyConfiguration
|
||||
|
||||
|
||||
admin.site.register(XDomainProxyConfiguration, ConfigurationModelAdmin)
|
||||
74
common/djangoapps/cors_csrf/migrations/0001_initial.py
Normal file
74
common/djangoapps/cors_csrf/migrations/0001_initial.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as 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 'XDomainProxyConfiguration'
|
||||
db.create_table('cors_csrf_xdomainproxyconfiguration', (
|
||||
('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')()),
|
||||
))
|
||||
db.send_create_signal('cors_csrf', ['XDomainProxyConfiguration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'XDomainProxyConfiguration'
|
||||
db.delete_table('cors_csrf_xdomainproxyconfiguration')
|
||||
|
||||
|
||||
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'})
|
||||
},
|
||||
'cors_csrf.xdomainproxyconfiguration': {
|
||||
'Meta': {'object_name': 'XDomainProxyConfiguration'},
|
||||
'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', [], {})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['cors_csrf']
|
||||
0
common/djangoapps/cors_csrf/migrations/__init__.py
Normal file
0
common/djangoapps/cors_csrf/migrations/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Models for cross-domain configuration. """
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from config_models.models import ConfigurationModel
|
||||
|
||||
|
||||
class XDomainProxyConfiguration(ConfigurationModel):
|
||||
"""Cross-domain proxy configuration.
|
||||
|
||||
See `cors_csrf.views.xdomain_proxy` for an explanation of how this works.
|
||||
|
||||
"""
|
||||
|
||||
whitelist = models.fields.TextField(
|
||||
help_text=_(
|
||||
u"List of domains that are allowed to make cross-domain "
|
||||
u"requests to this site. Please list each domain on its own line."
|
||||
)
|
||||
)
|
||||
|
||||
72
common/djangoapps/cors_csrf/tests/test_views.py
Normal file
72
common/djangoapps/cors_csrf/tests/test_views.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Tests for cross-domain request views. """
|
||||
import json
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
|
||||
import ddt
|
||||
|
||||
from config_models.models import cache
|
||||
from cors_csrf.models import XDomainProxyConfiguration
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class XDomainProxyTest(TestCase):
|
||||
"""Tests for the xdomain proxy end-point. """
|
||||
|
||||
def setUp(self):
|
||||
"""Clear model-based config cache. """
|
||||
super(XDomainProxyTest, self).setUp()
|
||||
try:
|
||||
self.url = reverse('xdomain_proxy')
|
||||
except NoReverseMatch:
|
||||
self.skipTest('xdomain_proxy URL is not configured')
|
||||
|
||||
cache.clear()
|
||||
|
||||
def test_xdomain_proxy_disabled(self):
|
||||
self._configure(False)
|
||||
response = self._load_page()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@ddt.data(None, [' '], [' ', ' '])
|
||||
def test_xdomain_proxy_enabled_no_whitelist(self, whitelist):
|
||||
self._configure(True, whitelist=whitelist)
|
||||
response = self._load_page()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@ddt.data(
|
||||
(['example.com'], ['example.com']),
|
||||
(['example.com', 'sub.example.com'], ['example.com', 'sub.example.com']),
|
||||
([' example.com '], ['example.com']),
|
||||
([' ', 'example.com'], ['example.com']),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_xdomain_proxy_enabled_with_whitelist(self, whitelist, expected_whitelist):
|
||||
self._configure(True, whitelist=whitelist)
|
||||
response = self._load_page()
|
||||
self._check_whitelist(response, expected_whitelist)
|
||||
|
||||
def _configure(self, is_enabled, whitelist=None):
|
||||
"""Enable or disable the end-point and configure the whitelist. """
|
||||
config = XDomainProxyConfiguration.current()
|
||||
config.enabled = is_enabled
|
||||
|
||||
if whitelist:
|
||||
config.whitelist = "\n".join(whitelist)
|
||||
|
||||
config.save()
|
||||
cache.clear()
|
||||
|
||||
def _load_page(self):
|
||||
"""Load the end-point. """
|
||||
return self.client.get(reverse('xdomain_proxy'))
|
||||
|
||||
def _check_whitelist(self, response, expected_whitelist):
|
||||
"""Verify that the domain whitelist is rendered on the page. """
|
||||
rendered_whitelist = json.dumps({
|
||||
domain: '*'
|
||||
for domain in expected_whitelist
|
||||
})
|
||||
self.assertContains(response, 'xdomain.min.js')
|
||||
self.assertContains(response, rendered_whitelist)
|
||||
72
common/djangoapps/cors_csrf/views.py
Normal file
72
common/djangoapps/cors_csrf/views.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Views for enabling cross-domain requests. """
|
||||
import logging
|
||||
import json
|
||||
from django.conf import settings
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.http import HttpResponseNotFound
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from cors_csrf.models import XDomainProxyConfiguration
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
XDOMAIN_PROXY_CACHE_TIMEOUT = getattr(settings, 'XDOMAIN_PROXY_CACHE_TIMEOUT', 60 * 15)
|
||||
|
||||
|
||||
@cache_page(XDOMAIN_PROXY_CACHE_TIMEOUT)
|
||||
def xdomain_proxy(request): # pylint: disable=unused-argument
|
||||
"""Serve the xdomain proxy page.
|
||||
|
||||
Internet Explorer 9 does not send cookie information with CORS,
|
||||
which means we can't make cross-domain POST requests that
|
||||
require authentication (for example, from the course details
|
||||
page on the marketing site to the enrollment API
|
||||
to auto-enroll a user in an "honor" track).
|
||||
|
||||
The XDomain library [https://github.com/jpillora/xdomain]
|
||||
provides an alternative to using CORS.
|
||||
|
||||
The library works as follows:
|
||||
|
||||
1) A static HTML file ("xdomain_proxy.html") is served from courses.edx.org.
|
||||
The file includes JavaScript and a domain whitelist.
|
||||
|
||||
2) The course details page (on edx.org) creates an invisible iframe
|
||||
that loads the proxy HTML file.
|
||||
|
||||
3) A JS shim library on the course details page intercepts
|
||||
AJAX requests and communicates with JavaScript on the iframed page.
|
||||
The iframed page then proxies the request to the LMS.
|
||||
Since the iframed page is served from courses.edx.org,
|
||||
this is a same-domain request, so all cookies for the domain
|
||||
are sent along with the request.
|
||||
|
||||
You can enable this feature and configure the domain whitelist
|
||||
using Django admin.
|
||||
|
||||
"""
|
||||
config = XDomainProxyConfiguration.current()
|
||||
if not config.enabled:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
allowed_domains = []
|
||||
for domain in config.whitelist.split("\n"): # pylint: disable=no-member
|
||||
if domain.strip():
|
||||
allowed_domains.append(domain.strip())
|
||||
|
||||
if not allowed_domains:
|
||||
log.warning(
|
||||
u"No whitelist configured for cross-domain proxy. "
|
||||
u"You can configure the whitelist in Django Admin "
|
||||
u"using the XDomainProxyConfiguration model."
|
||||
)
|
||||
return HttpResponseNotFound()
|
||||
|
||||
context = {
|
||||
'xdomain_masters': json.dumps({
|
||||
domain: '*'
|
||||
for domain in allowed_domains
|
||||
})
|
||||
}
|
||||
return render_to_response('cors_csrf/xdomain_proxy.html', context)
|
||||
5
common/templates/cors_csrf/xdomain_proxy.html
Normal file
5
common/templates/cors_csrf/xdomain_proxy.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<!DOCTYPE HTML>
|
||||
<script src="${static.url('js/vendor/xdomain.min.js')}"></script>
|
||||
<script>xdomain.masters(${xdomain_masters});</script>
|
||||
@@ -1797,13 +1797,18 @@ if FEATURES.get('AUTH_USE_CAS'):
|
||||
INSTALLED_APPS += ('django_cas',)
|
||||
MIDDLEWARE_CLASSES += ('django_cas.middleware.CASMiddleware',)
|
||||
|
||||
############# CORS headers for cross-domain requests #################
|
||||
############# Cross-domain requests #################
|
||||
|
||||
if FEATURES.get('ENABLE_CORS_HEADERS'):
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ORIGIN_WHITELIST = ()
|
||||
CORS_ORIGIN_ALLOW_ALL = False
|
||||
|
||||
# Default cache expiration for the cross-domain proxy HTML page.
|
||||
# This is a static page that can be iframed into an external page
|
||||
# to simulate cross-domain requests.
|
||||
XDOMAIN_PROXY_CACHE_TIMEOUT = 60 * 15
|
||||
|
||||
###################### Registration ##################################
|
||||
|
||||
# For each of the fields, give one of the following values:
|
||||
|
||||
3
lms/static/js/vendor/xdomain.min.js
vendored
Normal file
3
lms/static/js/vendor/xdomain.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -613,6 +613,11 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
|
||||
url(r'^certificates/html', 'certificates.views.render_html_view', name='cert_html_view'),
|
||||
)
|
||||
|
||||
# XDomain proxy
|
||||
urlpatterns += (
|
||||
url(r'^xdomain_proxy.html$', 'cors_csrf.views.xdomain_proxy', name='xdomain_proxy'),
|
||||
)
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
Reference in New Issue
Block a user