From 8ae40dc77bc99110e7dfca8125396becd8937282 Mon Sep 17 00:00:00 2001 From: Maxim Beder Date: Fri, 5 Nov 2021 02:35:04 +0100 Subject: [PATCH] feat: implement a login redirect middleware Implemented a new middleware, that redirects all unathorised user, that are trying to vising any page but login and register, to login page. The middleware is enabled via a ENABLE_REDIRECT_UNAUTHORISED_USERS_TO_LOGIN setting. Added the middleware and the setting to the LMS. --- lms/envs/common.py | 14 ++ .../core/djangoapps/user_authn/middleware.py | 60 +++++++++ .../user_authn/tests/test_middlewares.py | 122 ++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 openedx/core/djangoapps/user_authn/middleware.py create mode 100644 openedx/core/djangoapps/user_authn/tests/test_middlewares.py diff --git a/lms/envs/common.py b/lms/envs/common.py index 5014f095ff7f..18b76ed77984 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -948,6 +948,18 @@ # .. toggle_target_removal_date: 2021-10-01 # .. toggle_tickets: 'https://openedx.atlassian.net/browse/MICROBA-1405' 'ENABLE_V2_CERT_DISPLAY_SETTINGS': False, + + # .. toggle_name: FEATURES['ENABLE_REDIRECT_UNAUTHENTICATED_USERS_TO_LOGIN'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Enable this feature to redirect all unauthenticated users to login page. Used to make all + # content private, where when it's disabled, there are some pages which can be viewed by unauthenticated users. + # .. toggle_use_cases: temporary + # .. toggle_creation_date: 2021-11-12 + # .. toggle_target_removal_date: None + # .. toggle_warnings: None + # .. toggle_tickets: 'https://github.com/open-craft/edx-platform/pull/439' + 'ENABLE_REDIRECT_UNAUTHENTICATED_USERS_TO_LOGIN': False, } # Specifies extra XBlock fields that should available when requested via the Course Blocks API @@ -2082,6 +2094,8 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring #'django.contrib.auth.middleware.AuthenticationMiddleware', 'openedx.core.djangoapps.cache_toolbox.middleware.CacheBackedAuthenticationMiddleware', + 'openedx.core.djangoapps.user_authn.middleware.RedirectUnauthenticatedToLoginMiddleware', + 'common.djangoapps.student.middleware.UserStandingMiddleware', 'openedx.core.djangoapps.contentserver.middleware.StaticContentServer', diff --git a/openedx/core/djangoapps/user_authn/middleware.py b/openedx/core/djangoapps/user_authn/middleware.py new file mode 100644 index 000000000000..8b58026ce8e2 --- /dev/null +++ b/openedx/core/djangoapps/user_authn/middleware.py @@ -0,0 +1,60 @@ +""" +Middleware for User Authentication +""" + + +from django.conf import settings +from django.http import QueryDict +from django.shortcuts import redirect + + +class RedirectUnauthenticatedToLoginMiddleware: + """ + Middleware that redirects unauthenticated users to login page. + + Any GET request comming from an unauthenticated user will be responded with + a redirect to the login url. The middleware ignores requests to login and + register pages. + + If redirects, passes the requested url to login as 'next' query string + parameter. + + To enable the middleware, ENABLE_REDIRECT_UNAUTHENTICATED_USERS_TO_LOGIN + setting has to be set to True. + + Assumed that the requests passed to the middleware have user attribute set. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if self._should_redirect(request): + return redirect(settings.LOGIN_URL + '?' + self._get_redirect_query_string(request)) + + return self.get_response(request) + + def _should_redirect(self, request): + """ + Determines if a request should be redirected to login. + """ + if not settings.FEATURES.get('ENABLE_REDIRECT_UNAUTHENTICATED_USERS_TO_LOGIN', False): + return False + + is_get_request = request.method == 'GET' + is_login_or_register_url = request.path in (settings.LOGIN_URL, '/register') + + return ( + is_get_request and + (not is_login_or_register_url) and + (not request.user.is_authenticated) + ) + + def _get_redirect_query_string(self, request): + """ + Generates query string for redirect. + """ + # calling copy to get mutable QueryDict + query = QueryDict().copy() + query['next'] = request.get_full_path() + return query.urlencode() diff --git a/openedx/core/djangoapps/user_authn/tests/test_middlewares.py b/openedx/core/djangoapps/user_authn/tests/test_middlewares.py new file mode 100644 index 000000000000..61e8fcdb89e3 --- /dev/null +++ b/openedx/core/djangoapps/user_authn/tests/test_middlewares.py @@ -0,0 +1,122 @@ +# pylint: disable=missing-docstring + + +import ddt +from django.conf import settings +from django.http import QueryDict +from django.test import RequestFactory, TestCase +from unittest.mock import Mock, patch +from urllib.parse import urlparse + +from common.djangoapps.student.tests.factories import AnonymousUserFactory, UserFactory +from openedx.core.djangoapps.user_authn.middleware import RedirectUnauthenticatedToLoginMiddleware + + +@ddt.ddt +@patch.dict("django.conf.settings.FEATURES", {"ENABLE_REDIRECT_UNAUTHENTICATED_USERS_TO_LOGIN": True}) +class RedirectUnauthenticatedToLoginMiddlewareTests(TestCase): + """ + Tests for RedirectUnauthenticatedToLoginMiddleware. + """ + + def setUp(self): + super().setUp() + self.mock_response = Mock() + self.middleware = RedirectUnauthenticatedToLoginMiddleware( + lambda request: self.mock_response + ) + + @ddt.data( + RequestFactory().head('/'), + RequestFactory().post('/'), + RequestFactory().put('/'), + RequestFactory().options('/'), + RequestFactory().delete('/'), + ) + def test_does_not_redirect_non_GET_requests(self, request): + """ + Middleware doesn't redirect non GET requests. + """ + request.user = AnonymousUserFactory.create() + + response = self.middleware(request) + + self.assertEqual(response, self.mock_response) + + def test_redirects_unauthenticated_user_to_login(self): + """ + Middleware redirects unauthenticated user to login page. + """ + request = RequestFactory().get('/') + request.user = AnonymousUserFactory.create() + + response = self.middleware(request) + + self.assertNotEqual(response, self.mock_response) + path = urlparse(response.url).path + self.assertEqual(path, settings.LOGIN_URL) + + def test_passes_url_in_next_query_string(self): + """ + Middleware passes url in 'next' query string parameter. + + When redirecting, the middleware should add 'next' query string + parameter, that contains the url that was originally requested. + """ + request = RequestFactory().get('/dashboard?test=123') + request.user = AnonymousUserFactory.create() + + response = self.middleware(request) + + self.assertNotEqual(response, self.mock_response) + query = QueryDict(urlparse(response.url).query) + self.assertIn('next', query) + self.assertEqual(query['next'], '/dashboard?test=123') + + def test_does_not_redirect_if_user_is_authenticated(self): + """ + Middleware doesn't redirect authenticated users. + """ + request = RequestFactory().get('/') + request.user = UserFactory.create() + + response = self.middleware(request) + + self.assertEqual(response, self.mock_response) + + def test_get_login_does_not_redirect_unauthenticated_user(self): + """ + Middleware doesn't redirect unauthenticated user visiting login page. + """ + request = RequestFactory().get(settings.LOGIN_URL) + request.user = AnonymousUserFactory.create() + + response = self.middleware(request) + + self.assertEqual(response, self.mock_response) + + def test_get_register_does_not_redirect_unauthenticated_user(self): + """ + Middleware doesn't redirect unauthenticated user visiting register page. + """ + request = RequestFactory().get('/register') + request.user = AnonymousUserFactory.create() + + response = self.middleware(request) + + self.assertEqual(response, self.mock_response) + + @patch.dict("django.conf.settings.FEATURES", {"ENABLE_REDIRECT_UNAUTHENTICATED_USERS_TO_LOGIN": False}) + def test_does_not_redirect_unauthenticated_user_if_setting_disabled(self): + """ + Middleware doesn't redirect if settings is set to False. + + If ENABLE_REDIRECT_UNAUTHENTICATED_USERS_TO_LOGIN setting is set to + False, the middleware should not redirect unauthenticated users. + """ + request = RequestFactory().get('/') + request.user = AnonymousUserFactory.create() + + response = self.middleware(request) + + self.assertEqual(response, self.mock_response)