diff --git a/common/djangoapps/microsite_configuration/microsite.py b/common/djangoapps/microsite_configuration/microsite.py index 05409fe9e4..390aba6c5c 100644 --- a/common/djangoapps/microsite_configuration/microsite.py +++ b/common/djangoapps/microsite_configuration/microsite.py @@ -47,6 +47,15 @@ def get_value(val_name, default=None): return configuration.get(val_name, default) +def has_override_value(val_name): + """ + Returns True/False whether a Microsite has a definition for the + specified named value + """ + configuration = get_configuration() + return val_name in configuration + + def get_template_path(relative_path): """ Returns a path (string) to a Mako template, which can either be in diff --git a/common/djangoapps/microsite_configuration/middleware.py b/common/djangoapps/microsite_configuration/middleware.py index f9e1e0ba15..b6ca090c37 100644 --- a/common/djangoapps/microsite_configuration/middleware.py +++ b/common/djangoapps/microsite_configuration/middleware.py @@ -6,6 +6,8 @@ A microsite enables the following features: 2) Present a landing page with a listing of courses that are specific to the 'brand' 3) Ability to swap out some branding elements in the website """ + +from django.conf import settings from microsite_configuration import microsite @@ -34,3 +36,50 @@ class MicrositeMiddleware(object): """ microsite.clear() return response + + +class MicrositeSessionCookieDomainMiddleware(): + """ + Special case middleware which should be at the very end of the MIDDLEWARE list (so that it runs first + on the process_response chain). This middleware will define a wrapper function for the set_cookie() function + on the HttpResponse object, if the request is running in a middleware. + + This wrapped set_cookie will change the SESSION_COOKIE_DOMAIN setting so that the cookie can be bound to a + fully customized URL. + """ + + def process_response(self, request, response): + """ + Standard Middleware entry point + """ + + # See if we are running in a Microsite *AND* we have a custom SESSION_COOKIE_DOMAIN defined + # in configuration + if microsite.has_override_value('SESSION_COOKIE_DOMAIN'): + + # define wrapper function for the standard set_cookie() + def _set_cookie_wrapper(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False): + + # only override if we are setting the cookie name to be the one the Django Session Middleware uses + # as defined in settings.SESSION_COOKIE_NAME + if key == settings.SESSION_COOKIE_NAME: + domain = microsite.get_value('SESSION_COOKIE_DOMAIN', domain) + + # then call down into the normal Django set_cookie method + return response.set_cookie_wrapped_func( + key, + value, + max_age=max_age, + expires=expires, + path=path, + domain=domain, + secure=secure, + httponly=httponly + ) + + # then point the HttpResponse.set_cookie to point to the wrapper and keep + # the original around + response.set_cookie_wrapped_func = response.set_cookie + response.set_cookie = _set_cookie_wrapper + + return response diff --git a/common/djangoapps/microsite_configuration/tests/test_middleware.py b/common/djangoapps/microsite_configuration/tests/test_middleware.py new file mode 100644 index 0000000000..3cf65b1199 --- /dev/null +++ b/common/djangoapps/microsite_configuration/tests/test_middleware.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" +Test Microsite middleware. +""" +from mock import patch + +from django.test import TestCase +from django.conf import settings +from django.test.client import Client +from django.test.utils import override_settings +import unittest + + +# NOTE: We set SESSION_SAVE_EVERY_REQUEST to True in order to make sure +# Sessions are always started on every request +@override_settings(SESSION_SAVE_EVERY_REQUEST=True) +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class MicroSiteSessionCookieTests(TestCase): + """ + Tests regarding the session cookie management in the middlware for MicroSites + """ + + def setUp(self): + # create a test client + self.client = Client() + + def test_session_cookie_domain_no_microsite(self): + """ + Tests that non-microsite behaves according to default behavior + """ + + response = self.client.get('/') + self.assertNotIn('test_microsite.localhost', str(getattr(response, 'cookies')['sessionid'])) + self.assertNotIn('Domain', str(getattr(response, 'cookies')['sessionid'])) + + def test_session_cookie_domain(self): + """ + Makes sure that the cookie being set in a Microsite + is the one specially overridden in configuration, + in this case in test.py + """ + + response = self.client.get('/', HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME) + self.assertIn('test_microsite.localhost', str(getattr(response, 'cookies')['sessionid'])) + + @patch.dict("django.conf.settings.MICROSITE_CONFIGURATION", {'test_microsite': {'SESSION_COOKIE_DOMAIN': None}}) + def test_microsite_none_cookie_domain(self): + """ + Tests to make sure that a Microsite that specifies None for 'SESSION_COOKIE_DOMAIN' does not + set a domain on the session cookie + """ + + response = self.client.get('/', HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME) + self.assertNotIn('test_microsite.localhost', str(getattr(response, 'cookies')['sessionid'])) + self.assertNotIn('Domain', str(getattr(response, 'cookies')['sessionid'])) diff --git a/lms/envs/common.py b/lms/envs/common.py index 5d3f1fe9a4..342e4c2805 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -983,6 +983,9 @@ MIDDLEWARE_CLASSES = ( 'courseware.middleware.RedirectUnenrolledMiddleware', 'course_wiki.middleware.WikiAccessMiddleware', + + # This must be last + 'microsite_configuration.middleware.MicrositeSessionCookieDomainMiddleware', ) # Clickjacking protection can be enabled by setting this to 'DENY' diff --git a/lms/envs/test.py b/lms/envs/test.py index 7f1e751c4d..2f801e278b 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -367,6 +367,7 @@ MICROSITE_CONFIGURATION = { "COURSE_ABOUT_VISIBILITY_PERMISSION": "see_about_page", "ENABLE_SHOPPING_CART": True, "ENABLE_PAID_COURSE_REGISTRATION": True, + "SESSION_COOKIE_DOMAIN": "test_microsite.localhost", }, "default": { "university": "default_university",