Move cors_crsf to openedx/core
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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."
|
||||
)
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user