From 5d6f163a0b65002b0a75dc478916e238485aa36f Mon Sep 17 00:00:00 2001 From: Manjinder Singh <49171515+jinder1s@users.noreply.github.com> Date: Wed, 25 Aug 2021 12:43:21 -0400 Subject: [PATCH] feat: Adding new CookieNameChange middleware (#28404) Description: Adds a new middleware to help with cookie name changes. It uses the idea of expand and contract, where after we've changed the name, the middleware allows up to accept either a cookie with new name (given higher priority when both are present) or cookie with old name. This is also helpful when changing domain of a cookie. impacts: developers, users(anyone that has cookies) Change depends on django setting changes. See CookieNameChange middleware for more info. --- .../djangoapps/cookie_metadata/__init__.py | 0 .../djangoapps/cookie_metadata/middleware.py | 58 ++++++++++ .../cookie_metadata/tests/__init__.py | 0 .../tests/test_cookie_name_change.py | 109 ++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 openedx/core/djangoapps/cookie_metadata/__init__.py create mode 100644 openedx/core/djangoapps/cookie_metadata/middleware.py create mode 100644 openedx/core/djangoapps/cookie_metadata/tests/__init__.py create mode 100644 openedx/core/djangoapps/cookie_metadata/tests/test_cookie_name_change.py diff --git a/openedx/core/djangoapps/cookie_metadata/__init__.py b/openedx/core/djangoapps/cookie_metadata/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/cookie_metadata/middleware.py b/openedx/core/djangoapps/cookie_metadata/middleware.py new file mode 100644 index 0000000000..ecd276d96d --- /dev/null +++ b/openedx/core/djangoapps/cookie_metadata/middleware.py @@ -0,0 +1,58 @@ +"""Middleware to change name of an incoming cookie""" +from django.conf import settings + + +class CookieNameChange: + """Changes name of an incoming cookie""" + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + """ + Changes the names of a cookie in request.COOKIES + + For this middleware to run: + - set COOKIE_NAME_CHANGE_ACTIVATE_EXPAND_PHASE = True + - COOKIE_NAME_CHANGE_EXPAND_INFO is a dict and has following info: + "old_name": Previous name of cookie + "new_name": New name of cookie + + Actions taken by middleware: + - will delete any cookie with "old_name" + - if create a new cookie with "new_name" and set its value to value of "old_name" cookie + + it will not modify cookie with "new_name" if it already exists in request.COOKIES + """ + + # .. toggle_name: COOKIE_NAME_CHANGE_ACTIVATE_EXPAND_PHASE + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Used to enable CookieNameChange middleware which changes a cookie name in request.COOKIES + # .. toggle_warnings: This should be set at the same time you set COOKIE_NAME_CHANGE_EXPAND_INFO setting + # .. toggle_use_cases: temporary + # .. toggle_creation_date: 2021-08-04 + # .. toggle_target_removal_date: 2021-10-01 + # .. toggle_tickets: https://openedx.atlassian.net/browse/ARCHBOM-1872 + if getattr(settings, "COOKIE_NAME_CHANGE_ACTIVATE_EXPAND_PHASE", False): + old_cookie_in_request = False + expand_settings = getattr(settings, "COOKIE_NAME_CHANGE_EXPAND_INFO", None) + + if ( + expand_settings is not None + and isinstance(expand_settings, dict) + and "new_name" in expand_settings + and "old_name" in expand_settings + ): + if expand_settings["old_name"] in request.COOKIES: + old_cookie_in_request = True + old_cookie_value = request.COOKIES[expand_settings["old_name"]] + del request.COOKIES[expand_settings["old_name"]] + + if ( + expand_settings["new_name"] not in request.COOKIES + and old_cookie_in_request + ): + request.COOKIES[expand_settings["new_name"]] = old_cookie_value + + response = self.get_response(request) + return response diff --git a/openedx/core/djangoapps/cookie_metadata/tests/__init__.py b/openedx/core/djangoapps/cookie_metadata/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/cookie_metadata/tests/test_cookie_name_change.py b/openedx/core/djangoapps/cookie_metadata/tests/test_cookie_name_change.py new file mode 100644 index 0000000000..3a4d1ee844 --- /dev/null +++ b/openedx/core/djangoapps/cookie_metadata/tests/test_cookie_name_change.py @@ -0,0 +1,109 @@ +""" +Test Module to test CookieNameChange class +""" +from unittest.mock import Mock + +from django.test import TestCase + + +from ..middleware import CookieNameChange + + +class TestCookieNameChange(TestCase): + """ + Test class for CookieNameChange + """ + + def setUp(self): + super().setUp() + self.mock_response = Mock() + self.cookie_name_change_middleware = CookieNameChange(self.mock_response) + self.mock_request = Mock() + + self.old_value = "." * 100 + self.old_key = 'a' + self.extra_cookies = { + "_b": "." * 13, + "_c_": "." * 13, + "a.b": "." * 10, + } + self.old_dict = { + self.old_key: self.old_value, + } + + self.expand_settings = { + "old_name": self.old_key, + "new_name": "b", + } + + def test_cookie_swap(self): + """Check to make sure self.Middleware correctly swaps keys""" + + self.old_dict.update(self.extra_cookies) + + self.mock_request.COOKIES = self.old_dict.copy() + + with self.settings( + COOKIE_NAME_CHANGE_ACTIVATE_EXPAND_PHASE=True + ), self.settings(COOKIE_NAME_CHANGE_EXPAND_INFO=self.expand_settings): + self.cookie_name_change_middleware(self.mock_request) + + assert self.expand_settings["old_name"] not in self.mock_request.COOKIES.keys() + assert self.expand_settings["new_name"] in self.mock_request.COOKIES.keys() + assert self.mock_request.COOKIES[self.expand_settings["new_name"]] == self.old_value + test_dict = self.extra_cookies.copy() + test_dict[self.expand_settings['new_name']] = self.old_value + assert self.mock_request.COOKIES == test_dict + + # make sure response function is called once + self.mock_response.assert_called_once() + + def test_cookie_no_swap(self): + """Make sure self.cookie_name_change_middleware does not change cookie if new_name cookie is already present""" + + new_value = "." * 13 + no_change_cookies = { + self.expand_settings['new_name']: new_value, + "_c_": "." * 13, + "a.b": "." * 10, + } + + self.old_dict.update(no_change_cookies) + + self.mock_request.COOKIES = self.old_dict.copy() + + with self.settings( + COOKIE_NAME_CHANGE_ACTIVATE_EXPAND_PHASE=True + ), self.settings(COOKIE_NAME_CHANGE_EXPAND_INFO=self.expand_settings): + self.cookie_name_change_middleware(self.mock_request) + + assert self.expand_settings["old_name"] not in self.mock_request.COOKIES.keys() + assert self.expand_settings["new_name"] in self.mock_request.COOKIES.keys() + assert self.mock_request.COOKIES[self.expand_settings["new_name"]] == new_value + assert self.mock_request.COOKIES == no_change_cookies + + # make sure response function is called once + self.mock_response.assert_called_once() + + def test_does_nothing(self): + """Make sure turning off toggle turns off self.cookie_name_change_middleware""" + + new_value = "." * 13 + no_change_cookies = { + self.expand_settings['new_name']: new_value, + "_c_": "." * 13, + "a.b": "." * 10, + } + self.old_dict.update(no_change_cookies) + + self.mock_request.COOKIES = self.old_dict.copy() + + with self.settings( + COOKIE_NAME_CHANGE_ACTIVATE_EXPAND_PHASE=False + ), self.settings(COOKIE_NAME_CHANGE_EXPAND_INFO=self.expand_settings): + self.cookie_name_change_middleware(self.mock_request) + + assert self.mock_request.COOKIES == self.old_dict + + # make sure response function is called once + self.mock_response.assert_called_once()