Move cors_crsf to openedx/core

This commit is contained in:
Andy Armstrong
2016-10-07 08:52:22 -04:00
parent 8c22d5229b
commit 03c3ee53e5
20 changed files with 35 additions and 19 deletions

View File

@@ -1,7 +0,0 @@
"""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)

View File

@@ -1,29 +0,0 @@
"""Django Rest Framework Authentication classes for cross-domain end-points."""
from rest_framework import authentication
from cors_csrf.helpers import is_cross_domain_request_allowed, skip_cross_domain_referer_check
class SessionAuthenticationCrossDomainCsrf(authentication.SessionAuthentication):
"""Session authentication that skips the referer check over secure connections.
Django Rest Framework's `SessionAuthentication` class calls Django's
CSRF middleware implementation directly, which bypasses the middleware
stack.
This version of `SessionAuthentication` performs the same workaround
as `CorsCSRFMiddleware` to skip the referer check for whitelisted
domains over a secure connection. See `cors_csrf.middleware` for
more information.
Since this subclass overrides only the `enforce_csrf()` method,
it can be mixed in with other `SessionAuthentication` subclasses.
"""
def enforce_csrf(self, request):
"""Skip the referer check if the cross-domain request is allowed. """
if is_cross_domain_request_allowed(request):
with skip_cross_domain_referer_check(request):
return super(SessionAuthenticationCrossDomainCsrf, self).enforce_csrf(request)
else:
return super(SessionAuthenticationCrossDomainCsrf, self).enforce_csrf(request)

View File

@@ -1,28 +0,0 @@
"""Decorators for cross-domain CSRF. """
from django.views.decorators.csrf import ensure_csrf_cookie
def ensure_csrf_cookie_cross_domain(func):
"""View decorator for sending a cross-domain CSRF cookie.
This works like Django's `@ensure_csrf_cookie`, but
will also set an additional CSRF cookie for use
cross-domain.
Arguments:
func (function): The view function to decorate.
"""
def _inner(*args, **kwargs): # pylint: disable=missing-docstring
if args:
# Set the META `CROSS_DOMAIN_CSRF_COOKIE_USED` flag so
# that `CsrfCrossDomainCookieMiddleware` knows to set
# the cross-domain version of the CSRF cookie.
request = args[0]
request.META['CROSS_DOMAIN_CSRF_COOKIE_USED'] = True
# Decorate the request with Django's
# `ensure_csrf_cookie` to ensure that the usual
# CSRF cookie gets set.
return ensure_csrf_cookie(func)(*args, **kwargs)
return _inner

View File

@@ -1,92 +0,0 @@
"""Helper methods for CORS and CSRF checks. """
import logging
import urlparse
import contextlib
from django.conf import settings
log = logging.getLogger(__name__)
def is_cross_domain_request_allowed(request):
"""Check whether we should allow the cross-domain request.
We allow a cross-domain request only if:
1) The request is made securely and the referer has "https://" as the protocol.
2) The referer domain has been whitelisted.
Arguments:
request (HttpRequest)
Returns:
bool
"""
referer = request.META.get('HTTP_REFERER')
referer_parts = urlparse.urlparse(referer) if referer else None
referer_hostname = referer_parts.hostname if referer_parts is not None else None
# Use CORS_ALLOW_INSECURE *only* for development and testing environments;
# it should never be enabled in production.
if not getattr(settings, 'CORS_ALLOW_INSECURE', False):
if not request.is_secure():
log.debug(
u"Request is not secure, so we cannot send the CSRF token. "
u"For testing purposes, you can disable this check by setting "
u"`CORS_ALLOW_INSECURE` to True in the settings"
)
return False
if not referer:
log.debug(u"No referer provided over a secure connection, so we cannot check the protocol.")
return False
if not referer_parts.scheme == 'https':
log.debug(u"Referer '%s' must have the scheme 'https'")
return False
domain_is_whitelisted = (
getattr(settings, 'CORS_ORIGIN_ALLOW_ALL', False) or
referer_hostname in getattr(settings, 'CORS_ORIGIN_WHITELIST', [])
)
if not domain_is_whitelisted:
if referer_hostname is None:
# If no referer is specified, we can't check if it's a cross-domain
# request or not.
log.debug(u"Referrer hostname is `None`, so it is not on the whitelist.")
elif referer_hostname != request.get_host():
log.info(
(
u"Domain '%s' is not on the cross domain whitelist. "
u"Add the domain to `CORS_ORIGIN_WHITELIST` or set "
u"`CORS_ORIGIN_ALLOW_ALL` to True in the settings."
), referer_hostname
)
else:
log.debug(
(
u"Domain '%s' is the same as the hostname in the request, "
u"so we are not going to treat it as a cross-domain request."
), referer_hostname
)
return False
return True
@contextlib.contextmanager
def skip_cross_domain_referer_check(request):
"""Skip the cross-domain CSRF referer check.
Django's CSRF middleware performs the referer check
only when the request is made over a secure connection.
To skip the check, we patch `request.is_secure()` to
False.
"""
is_secure_default = request.is_secure
request.is_secure = lambda: False
try:
yield
finally:
request.is_secure = is_secure_default

View File

@@ -1,146 +0,0 @@
"""
Middleware for handling CSRF checks with CORS requests
CSRF and referrer domain checks
-------------------------------
When processing HTTPS requests, the default CSRF middleware checks that the referer
domain and protocol is the same as the request's domain and protocol. This is meant
to avoid a type of attack for sites which serve their content with both HTTP and HTTPS,
with a man in the middle on the HTTP requests.
https://github.com/django/django/blob/b91c385e324f1cb94d20e2ad146372c259d51d3b/django/middleware/csrf.py#L117
This doesn't work well with CORS requests, which aren't vulnerable to this attack when
the server from which the request is coming uses HTTPS too, as it prevents the man in the
middle attack vector.
We thus do the CSRF check of requests coming from an authorized CORS host separately
in this middleware, applying the same protections as the default CSRF middleware, but
without the referrer check, when both the request and the referer use HTTPS.
CSRF cookie domains
-------------------
In addition, in order to make cross-domain AJAX calls to CSRF-protected end-points,
we need to send the CSRF token in the HTTP header of the request.
The simple way to do this would be to set the CSRF_COOKIE_DOMAIN to ".edx.org",
but unfortunately this can cause problems. For example, suppose that
"first.edx.org" sets the cookie with domain ".edx.org", but "second.edx.org"
sets a cookie with domain "second.edx.org". In this case, the browser
would have two different CSRF tokens set (one for each cookie domain),
which can cause non-deterministic failures depending on which cookie
is sent first.
For this reason, we add a second cookie that (a) has the domain set to ".edx.org",
but (b) does NOT have the same name as the CSRF_COOKIE_NAME. Clients making
cross-domain requests can use this cookie instead of the subdomain-specific
CSRF cookie.
"""
import logging
from django.conf import settings
from django.middleware.csrf import CsrfViewMiddleware
from django.core.exceptions import MiddlewareNotUsed, ImproperlyConfigured
from cors_csrf.helpers import is_cross_domain_request_allowed, skip_cross_domain_referer_check
log = logging.getLogger(__name__)
class CorsCSRFMiddleware(CsrfViewMiddleware):
"""
Middleware for handling CSRF checks with CORS requests
"""
def __init__(self):
"""Disable the middleware if the feature flag is disabled. """
if not settings.FEATURES.get('ENABLE_CORS_HEADERS'):
raise MiddlewareNotUsed()
def process_view(self, request, callback, callback_args, callback_kwargs):
"""Skip the usual CSRF referer check if this is an allowed cross-domain request. """
if not is_cross_domain_request_allowed(request):
log.debug("Could not disable CSRF middleware referer check for cross-domain request.")
return
with skip_cross_domain_referer_check(request):
return super(CorsCSRFMiddleware, self).process_view(request, callback, callback_args, callback_kwargs)
class CsrfCrossDomainCookieMiddleware(object):
"""Set an additional "cross-domain" CSRF cookie.
Usage:
1) Decorate a view with `@ensure_csrf_cookie_cross_domain`.
2) Set `CROSS_DOMAIN_CSRF_COOKIE_NAME` and `CROSS_DOMAIN_CSRF_COOKIE_DOMAIN`
in settings.
3) Add the domain to `CORS_ORIGIN_WHITELIST`
4) Enable `FEATURES['ENABLE_CROSS_DOMAIN_CSRF_COOKIE']`
For testing, it is often easier to relax the security checks by setting:
* `CORS_ALLOW_INSECURE = True`
* `CORS_ORIGIN_ALLOW_ALL = True`
"""
def __init__(self):
"""Disable the middleware if the feature is not enabled. """
if not settings.FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF_COOKIE'):
raise MiddlewareNotUsed()
if not getattr(settings, 'CROSS_DOMAIN_CSRF_COOKIE_NAME', ''):
raise ImproperlyConfigured(
"You must set `CROSS_DOMAIN_CSRF_COOKIE_NAME` when "
"`FEATURES['ENABLE_CROSS_DOMAIN_CSRF_COOKIE']` is True."
)
if not getattr(settings, 'CROSS_DOMAIN_CSRF_COOKIE_DOMAIN', ''):
raise ImproperlyConfigured(
"You must set `CROSS_DOMAIN_CSRF_COOKIE_DOMAIN` when "
"`FEATURES['ENABLE_CROSS_DOMAIN_CSRF_COOKIE']` is True."
)
def process_response(self, request, response):
"""Set the cross-domain CSRF cookie. """
# Check whether this is a secure request from a domain on our whitelist.
if not is_cross_domain_request_allowed(request):
log.debug("Could not set cross-domain CSRF cookie.")
return response
# Check whether (a) the CSRF middleware has already set a cookie, and
# (b) this is a view decorated with `@ensure_cross_domain_csrf_cookie`
# If so, we can send the cross-domain CSRF cookie.
should_set_cookie = (
request.META.get('CROSS_DOMAIN_CSRF_COOKIE_USED', False) and
request.META.get('CSRF_COOKIE_USED', False) and
request.META.get('CSRF_COOKIE') is not None
)
if should_set_cookie:
# This is very similar to the code in Django's CSRF middleware
# implementation, with two exceptions:
# 1) We change the cookie name and domain so it can be used cross-domain.
# 2) We always set "secure" to True, so that the CSRF token must be
# sent over a secure connection.
response.set_cookie(
settings.CROSS_DOMAIN_CSRF_COOKIE_NAME,
request.META['CSRF_COOKIE'],
max_age=settings.CSRF_COOKIE_AGE,
domain=settings.CROSS_DOMAIN_CSRF_COOKIE_DOMAIN,
path=settings.CSRF_COOKIE_PATH,
secure=True
)
log.debug(
"Set cross-domain CSRF cookie '%s' for domain '%s'",
settings.CROSS_DOMAIN_CSRF_COOKIE_NAME,
settings.CROSS_DOMAIN_CSRF_COOKIE_DOMAIN
)
return response

View File

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='XDomainProxyConfiguration',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('whitelist', models.TextField(help_text='List of domains that are allowed to make cross-domain requests to this site. Please list each domain on its own line.')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
options={
'ordering': ('-change_date',),
'abstract': False,
},
),
]

View File

@@ -1,19 +0,0 @@
"""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."
)
)

View File

@@ -1,56 +0,0 @@
"""Tests for the CORS CSRF version of Django Rest Framework's SessionAuthentication."""
from mock import patch
from django.test import TestCase
from django.test.utils import override_settings
from django.test.client import RequestFactory
from django.conf import settings
from rest_framework.exceptions import PermissionDenied
from cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
class CrossDomainAuthTest(TestCase):
"""Tests for the CORS CSRF version of Django Rest Framework's SessionAuthentication. """
URL = "/dummy_url"
REFERER = "https://www.edx.org"
CSRF_TOKEN = 'abcd1234'
def setUp(self):
super(CrossDomainAuthTest, self).setUp()
self.auth = SessionAuthenticationCrossDomainCsrf()
def test_perform_csrf_referer_check(self):
request = self._fake_request()
with self.assertRaisesRegexp(PermissionDenied, 'CSRF'):
self.auth.enforce_csrf(request)
@patch.dict(settings.FEATURES, {
'ENABLE_CORS_HEADERS': True,
'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True
})
@override_settings(
CORS_ORIGIN_WHITELIST=["www.edx.org"],
CROSS_DOMAIN_CSRF_COOKIE_NAME="prod-edx-csrftoken",
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=".edx.org"
)
def test_skip_csrf_referer_check(self):
request = self._fake_request()
result = self.auth.enforce_csrf(request)
self.assertIs(result, None)
self.assertTrue(request.is_secure())
def _fake_request(self):
"""Construct a fake request with a referer and CSRF token over a secure connection. """
factory = RequestFactory()
factory.cookies[settings.CSRF_COOKIE_NAME] = self.CSRF_TOKEN
request = factory.post(
self.URL,
HTTP_REFERER=self.REFERER,
HTTP_X_CSRFTOKEN=self.CSRF_TOKEN
)
request.is_secure = lambda: True
return request

View File

@@ -1,24 +0,0 @@
"""Tests for cross-domain CSRF decorators. """
import json
import mock
from django.http import HttpResponse
from django.test import TestCase
from cors_csrf.decorators import ensure_csrf_cookie_cross_domain
def fake_view(request):
"""Fake view that returns the request META as a JSON-encoded string. """
return HttpResponse(json.dumps(request.META))
class TestEnsureCsrfCookieCrossDomain(TestCase):
"""Test the `ensucre_csrf_cookie_cross_domain` decorator. """
def test_ensure_csrf_cookie_cross_domain(self):
request = mock.Mock()
request.META = {}
wrapped_view = ensure_csrf_cookie_cross_domain(fake_view)
response = wrapped_view(request)
response_meta = json.loads(response.content)
self.assertEqual(response_meta['CROSS_DOMAIN_CSRF_COOKIE_USED'], True)
self.assertEqual(response_meta['CSRF_COOKIE_USED'], True)

View File

@@ -1,275 +0,0 @@
"""
Tests for the CORS CSRF middleware
"""
from mock import patch, Mock
import ddt
from django.test import TestCase
from django.test.utils import override_settings
from django.core.exceptions import MiddlewareNotUsed, ImproperlyConfigured
from django.http import HttpResponse
from django.middleware.csrf import CsrfViewMiddleware
from cors_csrf.middleware import CorsCSRFMiddleware, CsrfCrossDomainCookieMiddleware
SENTINEL = object()
class TestCorsMiddlewareProcessRequest(TestCase):
"""
Test processing a request through the middleware
"""
def get_request(self, is_secure, http_referer):
"""
Build a test request
"""
request = Mock()
request.META = {'HTTP_REFERER': http_referer}
request.is_secure = lambda: is_secure
return request
@override_settings(FEATURES={'ENABLE_CORS_HEADERS': True})
def setUp(self):
super(TestCorsMiddlewareProcessRequest, self).setUp()
self.middleware = CorsCSRFMiddleware()
def check_not_enabled(self, request):
"""
Check that the middleware does NOT process the provided request
"""
with patch.object(CsrfViewMiddleware, 'process_view') as mock_method:
res = self.middleware.process_view(request, None, None, None)
self.assertIsNone(res)
self.assertFalse(mock_method.called)
def check_enabled(self, request):
"""
Check that the middleware does process the provided request
"""
def cb_check_req_is_secure_false(request, callback, args, kwargs):
"""
Check that the request doesn't pass (yet) the `is_secure()` test
"""
self.assertFalse(request.is_secure())
return SENTINEL
with patch.object(CsrfViewMiddleware, 'process_view') as mock_method:
mock_method.side_effect = cb_check_req_is_secure_false
res = self.middleware.process_view(request, None, None, None)
self.assertIs(res, SENTINEL)
self.assertTrue(request.is_secure())
@override_settings(CORS_ORIGIN_WHITELIST=['foo.com'])
def test_enabled(self):
request = self.get_request(is_secure=True, http_referer='https://foo.com/bar')
self.check_enabled(request)
@override_settings(
FEATURES={'ENABLE_CORS_HEADERS': False},
CORS_ORIGIN_WHITELIST=['foo.com']
)
def test_disabled_no_cors_headers(self):
with self.assertRaises(MiddlewareNotUsed):
CorsCSRFMiddleware()
@override_settings(CORS_ORIGIN_WHITELIST=['bar.com'])
def test_disabled_wrong_cors_domain(self):
request = self.get_request(is_secure=True, http_referer='https://foo.com/bar')
self.check_not_enabled(request)
@override_settings(CORS_ORIGIN_WHITELIST=['foo.com'])
def test_disabled_wrong_cors_domain_reversed(self):
request = self.get_request(is_secure=True, http_referer='https://bar.com/bar')
self.check_not_enabled(request)
@override_settings(CORS_ORIGIN_WHITELIST=['foo.com'])
def test_disabled_http_request(self):
request = self.get_request(is_secure=False, http_referer='https://foo.com/bar')
self.check_not_enabled(request)
@override_settings(CORS_ORIGIN_WHITELIST=['foo.com'])
def test_disabled_http_referer(self):
request = self.get_request(is_secure=True, http_referer='http://foo.com/bar')
self.check_not_enabled(request)
@ddt.ddt
class TestCsrfCrossDomainCookieMiddleware(TestCase):
"""Tests for `CsrfCrossDomainCookieMiddleware`. """
REFERER = 'https://www.example.com'
COOKIE_NAME = 'shared-csrftoken'
COOKIE_VALUE = 'abcd123'
COOKIE_DOMAIN = '.edx.org'
@override_settings(
FEATURES={'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True},
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN
)
def setUp(self):
super(TestCsrfCrossDomainCookieMiddleware, self).setUp()
self.middleware = CsrfCrossDomainCookieMiddleware()
@override_settings(FEATURES={'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': False})
def test_disabled_by_feature_flag(self):
with self.assertRaises(MiddlewareNotUsed):
CsrfCrossDomainCookieMiddleware()
@ddt.data('CROSS_DOMAIN_CSRF_COOKIE_NAME', 'CROSS_DOMAIN_CSRF_COOKIE_DOMAIN')
def test_improperly_configured(self, missing_setting):
settings = {
'FEATURES': {'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True},
'CROSS_DOMAIN_CSRF_COOKIE_NAME': self.COOKIE_NAME,
'CROSS_DOMAIN_CSRF_COOKIE_DOMAIN': self.COOKIE_DOMAIN
}
del settings[missing_setting]
with override_settings(**settings):
with self.assertRaises(ImproperlyConfigured):
CsrfCrossDomainCookieMiddleware()
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
CORS_ORIGIN_ALLOW_ALL=True
)
def test_skip_if_not_secure(self):
response = self._get_response(is_secure=False)
self._assert_cookie_sent(response, False)
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
CORS_ORIGIN_ALLOW_ALL=True
)
def test_skip_if_not_sending_csrf_token(self):
response = self._get_response(csrf_cookie_used=False)
self._assert_cookie_sent(response, False)
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
CORS_ORIGIN_ALLOW_ALL=True
)
def test_skip_if_not_cross_domain_decorator(self):
response = self._get_response(cross_domain_decorator=False)
self._assert_cookie_sent(response, False)
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
CORS_ORIGIN_WHITELIST=['other.example.com']
)
def test_skip_if_referer_not_whitelisted(self):
response = self._get_response()
self._assert_cookie_sent(response, False)
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN
)
def test_skip_if_not_cross_domain(self):
response = self._get_response(
referer="https://courses.edx.org/foo",
host="courses.edx.org"
)
self._assert_cookie_sent(response, False)
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
CORS_ORIGIN_ALLOW_ALL=True
)
def test_skip_if_no_referer(self):
response = self._get_response(delete_referer=True)
self._assert_cookie_sent(response, False)
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
CORS_ORIGIN_ALLOW_ALL=True
)
def test_skip_if_referer_not_https(self):
response = self._get_response(referer="http://www.example.com")
self._assert_cookie_sent(response, False)
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
CORS_ORIGIN_ALLOW_ALL=True
)
def test_skip_if_referer_no_protocol(self):
response = self._get_response(referer="example.com")
self._assert_cookie_sent(response, False)
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
CORS_ALLOW_INSECURE=True
)
def test_skip_if_no_referer_insecure(self):
response = self._get_response(delete_referer=True)
self._assert_cookie_sent(response, False)
@override_settings(
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
CORS_ORIGIN_WHITELIST=['www.example.com']
)
def test_set_cross_domain_cookie(self):
response = self._get_response()
self._assert_cookie_sent(response, True)
def _get_response(self,
is_secure=True,
csrf_cookie_used=True,
cross_domain_decorator=True,
referer=None,
host=None,
delete_referer=False):
"""Process a request using the middleware. """
request = Mock()
request.META = {
'HTTP_REFERER': (
referer if referer is not None
else self.REFERER
)
}
request.is_secure = lambda: is_secure
if host is not None:
request.get_host = lambda: host
if delete_referer:
del request.META['HTTP_REFERER']
if csrf_cookie_used:
request.META['CSRF_COOKIE_USED'] = True
request.META['CSRF_COOKIE'] = self.COOKIE_VALUE
if cross_domain_decorator:
request.META['CROSS_DOMAIN_CSRF_COOKIE_USED'] = True
return self.middleware.process_response(request, HttpResponse())
def _assert_cookie_sent(self, response, is_set):
"""Check that the cross-domain CSRF cookie was sent. """
if is_set:
self.assertIn(self.COOKIE_NAME, response.cookies)
cookie_header = str(response.cookies[self.COOKIE_NAME])
expected = 'Set-Cookie: {name}={value}; Domain={domain};'.format(
name=self.COOKIE_NAME,
value=self.COOKIE_VALUE,
domain=self.COOKIE_DOMAIN
)
self.assertIn(expected, cookie_header)
self.assertIn('Max-Age=31449600; Path=/; secure', cookie_header)
else:
self.assertNotIn(self.COOKIE_NAME, response.cookies)

View File

@@ -1,72 +0,0 @@
"""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)

View File

@@ -1,72 +0,0 @@
"""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"):
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)

View File

@@ -18,8 +18,8 @@ from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView
from opaque_keys.edx.keys import CourseKey
from embargo import api as embargo_api
from cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
from cors_csrf.decorators import ensure_csrf_cookie_cross_domain
from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain
from openedx.core.lib.api.authentication import (
SessionAuthenticationAllowInactiveUser,
OAuth2AuthenticationAllowInactiveUser,

View File

@@ -1,5 +0,0 @@
<%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>