From 676456a98c73000d859d6aec2a38379eee82a0e1 Mon Sep 17 00:00:00 2001 From: Brandon Rodriguez <brodriguez8774@gmail.com> Date: Tue, 28 Jan 2025 23:05:50 -0500 Subject: [PATCH] Add additional whitelist logic, to hopefully account for things like DjangoDebugBar --- adminlte2_pdq/constants/__init__.py | 2 + .../constants/route_and_policy_constants.py | 19 + adminlte2_pdq/middleware.py | 14 +- .../testing/test_urls_2.py | 21 + tests/django_adminlte2_pdq/testing/urls.py | 1 + .../tests/test_app_wide_whitelists.py | 437 ++++++++++++++++++ 6 files changed, 492 insertions(+), 2 deletions(-) create mode 100644 tests/django_adminlte2_pdq/testing/test_urls_2.py create mode 100644 tests/django_adminlte2_pdq/tests/test_app_wide_whitelists.py diff --git a/adminlte2_pdq/constants/__init__.py b/adminlte2_pdq/constants/__init__.py index c1213b9..ccc62a3 100644 --- a/adminlte2_pdq/constants/__init__.py +++ b/adminlte2_pdq/constants/__init__.py @@ -46,6 +46,8 @@ from .route_and_policy_constants import ( from .route_and_policy_constants import ( LOGIN_EXEMPT_WHITELIST, STRICT_POLICY_WHITELIST, + APP_WIDE_LOGIN_EXEMPT_WHITELIST, + APP_WIDE_STRICT_POLICY_WHITELIST, LOGIN_REQUIRED, STRICT_POLICY, ) diff --git a/adminlte2_pdq/constants/route_and_policy_constants.py b/adminlte2_pdq/constants/route_and_policy_constants.py index e12789e..c04d86c 100644 --- a/adminlte2_pdq/constants/route_and_policy_constants.py +++ b/adminlte2_pdq/constants/route_and_policy_constants.py @@ -48,6 +48,25 @@ LOGIN_EXEMPT_WHITELIST += getattr(settings, "ADMINLTE2_LOGIN_EXEMPT_WHITELIST", STRICT_POLICY_WHITELIST += getattr(settings, "ADMINLTE2_STRICT_POLICY_WHITELIST", []) +# App-wide whitelists. +# These take a url base, and whitelist any urls that stem from said base. +# For example, this is required to make the Django Debug Toolbar function. +APP_WIDE_LOGIN_EXEMPT_WHITELIST = tuple( + getattr( + settings, + "ADMINLTE2_APP_WIDE_LOGIN_EXEMPT_WHITELIST", + [], + ) +) +APP_WIDE_STRICT_POLICY_WHITELIST = tuple( + getattr( + settings, + "ADMINLTE2_APP_WIDE_STRICT_POLICY_WHITELIST", + [], + ) +) + + # Get whether or not we are using LoginRequired and PermissionRequired. # NOTE: By nature of what STRICT_POLICY is, it implicitly means login is required. STRICT_POLICY = getattr(settings, "ADMINLTE2_USE_STRICT_POLICY", False) diff --git a/adminlte2_pdq/middleware.py b/adminlte2_pdq/middleware.py index fbea0a5..8485858 100644 --- a/adminlte2_pdq/middleware.py +++ b/adminlte2_pdq/middleware.py @@ -17,12 +17,14 @@ from django.views.generic.base import RedirectView from .constants import ( LOGIN_REQUIRED, LOGIN_EXEMPT_WHITELIST, + APP_WIDE_LOGIN_EXEMPT_WHITELIST, RESPONSE_403_DEBUG_MESSAGE, RESPONSE_403_PRODUCTION_MESSAGE, RESPONSE_404_DEBUG_MESSAGE, RESPONSE_404_PRODUCTION_MESSAGE, STRICT_POLICY, STRICT_POLICY_WHITELIST, + APP_WIDE_STRICT_POLICY_WHITELIST, LOGIN_URL, HOME_ROUTE, MEDIA_ROUTE, @@ -681,9 +683,13 @@ class AuthMiddleware: def is_login_whitelisted(self, view_data): """Determines if view is login-whitelisted. Used for login_required mode or strict mode.""" + try: return bool( - view_data["current_url_name"] in LOGIN_EXEMPT_WHITELIST + # In "app-wide" exemption list. + view_data["path"].startswith(APP_WIDE_LOGIN_EXEMPT_WHITELIST) + # In "standard" exemption list. + or view_data["current_url_name"] in LOGIN_EXEMPT_WHITELIST or view_data["fully_qualified_url_name"] in LOGIN_EXEMPT_WHITELIST ) except KeyError: @@ -691,9 +697,13 @@ class AuthMiddleware: def is_permission_whitelisted(self, view_data): """Determines if view is permission-whitelisted. Used for strict mode.""" + try: return bool( - view_data["current_url_name"] in STRICT_POLICY_WHITELIST + # In "app-wide" exemption list. + view_data["path"].startswith(APP_WIDE_STRICT_POLICY_WHITELIST) + # In "standard" exemption list. + or view_data["current_url_name"] in STRICT_POLICY_WHITELIST or view_data["fully_qualified_url_name"] in STRICT_POLICY_WHITELIST ) except KeyError: diff --git a/tests/django_adminlte2_pdq/testing/test_urls_2.py b/tests/django_adminlte2_pdq/testing/test_urls_2.py new file mode 100644 index 0000000..023eed7 --- /dev/null +++ b/tests/django_adminlte2_pdq/testing/test_urls_2.py @@ -0,0 +1,21 @@ +""" +Project testing views. +These urls exist to reliably test the two settings: + * ADMINLTE2_APP_WIDE_LOGIN_EXEMPT_WHITELIST + * ADMINLTE2_APP_WIDE_STRICT_POLICY_WHITELIST +without having potential side-effects on other existing tests. +""" + +# Third-Party Imports. +from django.urls import include, path + +# Internal Imports. +from . import views + + +app_name = "adminlte2_pdq_tests_2" +urlpatterns = [ + path("standard-1/", views.standard_view, name="standard-1"), + path("standard-2/", views.standard_view, name="standard-2"), + path("standard-3/", views.standard_view, name="standard-3"), +] diff --git a/tests/django_adminlte2_pdq/testing/urls.py b/tests/django_adminlte2_pdq/testing/urls.py index a74a2b3..f3e3893 100644 --- a/tests/django_adminlte2_pdq/testing/urls.py +++ b/tests/django_adminlte2_pdq/testing/urls.py @@ -27,6 +27,7 @@ urlpatterns = [ path("admin/", admin.site.urls), # Testing views. path("tests/", include("tests.django_adminlte2_pdq.testing.test_urls")), + path("tests-2/", include("tests.django_adminlte2_pdq.testing.test_urls_2")), # Adminlte2 views. path("accounts/", include("django.contrib.auth.urls")), path("", include("adminlte2_pdq.urls")), diff --git a/tests/django_adminlte2_pdq/tests/test_app_wide_whitelists.py b/tests/django_adminlte2_pdq/tests/test_app_wide_whitelists.py new file mode 100644 index 0000000..b10e5ed --- /dev/null +++ b/tests/django_adminlte2_pdq/tests/test_app_wide_whitelists.py @@ -0,0 +1,437 @@ +""" +Tests for the "app-wide" whitelist settings. Aka: + * ADMINLTE2_APP_WIDE_LOGIN_EXEMPT_WHITELIST + * ADMINLTE2_APP_WIDE_STRICT_POLICY_WHITELIST +""" + +# System Imports. +from unittest.mock import patch +from pytest import warns + +# Third-Party Imports. +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.test import override_settings +from django_expanded_test_cases import IntegrationTestCase + + +# Module Variables. +UserModel = get_user_model() + + +@override_settings(ADMINLTE2_USE_LOGIN_REQUIRED=True) +@override_settings(LOGIN_REQUIRED=True) +@patch("adminlte2_pdq.constants.LOGIN_REQUIRED", True) +@patch("adminlte2_pdq.middleware.LOGIN_REQUIRED", True) +class Test_LoginRequiredMode_AppWideWhitelistSettings(IntegrationTestCase): + """Test for project "app-wide" whitelist settings. + + For these tests, we only use the Anonymous user and the default user, without any modifications. + All the work should be done by the whitelists. + """ + + APP_WIDE_LOGIN_WHITELIST_VIEWS = ("/tests-2/",) + APP_WIDE_PERM_WHITELIST_VIEWS = ("/tests-2/",) + + def test__ensure_views_cannot_be_accessed_without_settings(self): + """Sanity check tests, to ensure these specific views cannot be accessed by default.""" + + # Should fail and redirect to login for anyone unauthenticated. + with self.subTest("As Anonymous User"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=AnonymousUser(), + # Expected view return data. + expected_status=200, + view_should_redirect=True, + # Expected content on page. + expected_title="Login |", + expected_content=[ + "Sign in to start your session", + "Remember Me", + "I forgot my password", + ], + ) + + # Should succeed and load as expected. + with self.subTest("As default standard user"): + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=self.test_user, + # Expected view return data. + expected_status=200, + view_should_redirect=False, + # Expected content on page. + expected_title="Standard View | Django AdminLtePdq Testing", + expected_header="Django AdminLtePdq | Standard View Header", + expected_content=[ + "Django AdminLtePdq | Standard View Subheader", + "Django AdminLtePdq | Standard View Content", + ], + ) + + @override_settings(ADMINLTE2_APP_WIDE_LOGIN_EXEMPT_WHITELIST=APP_WIDE_LOGIN_WHITELIST_VIEWS) + @override_settings(APP_WIDE_LOGIN_EXEMPT_WHITELIST=APP_WIDE_LOGIN_WHITELIST_VIEWS) + @patch("adminlte2_pdq.constants.APP_WIDE_LOGIN_EXEMPT_WHITELIST", APP_WIDE_LOGIN_WHITELIST_VIEWS) + @patch("adminlte2_pdq.middleware.APP_WIDE_LOGIN_EXEMPT_WHITELIST", APP_WIDE_LOGIN_WHITELIST_VIEWS) + def test__verify_login_whitelist(self): + """Tests to verify handling of "app-wide" login whitelist.""" + + # Should succeed and load as expected. + with self.subTest("As Anonymous User"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=AnonymousUser(), + # Expected view return data. + expected_status=200, + view_should_redirect=False, + # Expected content on page. + expected_title="Standard View | Django AdminLtePdq Testing", + expected_header="Django AdminLtePdq | Standard View Header", + expected_content=[ + "Django AdminLtePdq | Standard View Subheader", + "Django AdminLtePdq | Standard View Content", + ], + ) + + # Should succeed and load as expected. + with self.subTest("As default standard user"): + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=self.test_user, + # Expected view return data. + expected_status=200, + view_should_redirect=False, + # Expected content on page. + expected_title="Standard View | Django AdminLtePdq Testing", + expected_header="Django AdminLtePdq | Standard View Header", + expected_content=[ + "Django AdminLtePdq | Standard View Subheader", + "Django AdminLtePdq | Standard View Content", + ], + ) + + @override_settings(ADMINLTE2_APP_WIDE_STRICT_POLICY_WHITELIST=APP_WIDE_PERM_WHITELIST_VIEWS) + @override_settings(APP_WIDE_STRICT_POLICY_WHITELIST=APP_WIDE_PERM_WHITELIST_VIEWS) + @patch("adminlte2_pdq.constants.APP_WIDE_STRICT_POLICY_WHITELIST", APP_WIDE_PERM_WHITELIST_VIEWS) + @patch("adminlte2_pdq.middleware.APP_WIDE_STRICT_POLICY_WHITELIST", APP_WIDE_PERM_WHITELIST_VIEWS) + def test__verify_perm_whitelist(self): + """Tests to verify handling of "app-wide" perm whitelist. + In this case, should handle identical to no settings provided. + """ + + # Should fail and redirect to login for anyone unauthenticated. + with self.subTest("As Anonymous User"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=AnonymousUser(), + # Expected view return data. + expected_status=200, + view_should_redirect=True, + # Expected content on page. + expected_title="Login |", + expected_content=[ + "Sign in to start your session", + "Remember Me", + "I forgot my password", + ], + ) + + # Should succeed and load as expected. + with self.subTest("As default standard user"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=self.test_user, + # Expected view return data. + expected_status=200, + view_should_redirect=False, + # Expected content on page. + expected_title="Standard View | Django AdminLtePdq Testing", + expected_header="Django AdminLtePdq | Standard View Header", + expected_content=[ + "Django AdminLtePdq | Standard View Subheader", + "Django AdminLtePdq | Standard View Content", + ], + ) + + @override_settings(ADMINLTE2_APP_WIDE_LOGIN_EXEMPT_WHITELIST=APP_WIDE_LOGIN_WHITELIST_VIEWS) + @override_settings(LOGIN_APP_WIDE_EXEMPT_WHITELIST=APP_WIDE_LOGIN_WHITELIST_VIEWS) + @override_settings(ADMINLTE2_APP_WIDE_STRICT_POLICY_WHITELIST=APP_WIDE_PERM_WHITELIST_VIEWS) + @override_settings(APP_WIDE_STRICT_POLICY_WHITELIST=APP_WIDE_PERM_WHITELIST_VIEWS) + @patch("adminlte2_pdq.constants.APP_WIDE_LOGIN_EXEMPT_WHITELIST", APP_WIDE_LOGIN_WHITELIST_VIEWS) + @patch("adminlte2_pdq.middleware.APP_WIDE_LOGIN_EXEMPT_WHITELIST", APP_WIDE_LOGIN_WHITELIST_VIEWS) + @patch("adminlte2_pdq.constants.APP_WIDE_STRICT_POLICY_WHITELIST", APP_WIDE_PERM_WHITELIST_VIEWS) + @patch("adminlte2_pdq.middleware.APP_WIDE_STRICT_POLICY_WHITELIST", APP_WIDE_PERM_WHITELIST_VIEWS) + def test__verify_both_whitelists(self): + """Tests to verify handling of combined "app-wide" login and permission whitelists.""" + + # Should succeed and load as expected. + with self.subTest("As Anonymous User"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=AnonymousUser(), + # Expected view return data. + expected_status=200, + view_should_redirect=False, + # Expected content on page. + expected_title="Standard View | Django AdminLtePdq Testing", + expected_header="Django AdminLtePdq | Standard View Header", + expected_content=[ + "Django AdminLtePdq | Standard View Subheader", + "Django AdminLtePdq | Standard View Content", + ], + ) + + # Should succeed and load as expected. + with self.subTest("As default standard user"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=self.test_user, + # Expected view return data. + expected_status=200, + view_should_redirect=False, + # Expected content on page. + expected_title="Standard View | Django AdminLtePdq Testing", + expected_header="Django AdminLtePdq | Standard View Header", + expected_content=[ + "Django AdminLtePdq | Standard View Subheader", + "Django AdminLtePdq | Standard View Content", + ], + ) + + +@override_settings(ADMINLTE2_USE_LOGIN_REQUIRED=True) +@override_settings(LOGIN_REQUIRED=True) +@override_settings(ADMINLTE2_USE_STRICT_POLICY=True) +@override_settings(STRICT_POLICY=True) +@patch("adminlte2_pdq.constants.LOGIN_REQUIRED", True) +@patch("adminlte2_pdq.middleware.LOGIN_REQUIRED", True) +@patch("adminlte2_pdq.constants.STRICT_POLICY", True) +@patch("adminlte2_pdq.middleware.STRICT_POLICY", True) +class Test_StrictMode_AppWideWhitelistSettings(IntegrationTestCase): + """Test for project "app-wide" whitelist settings. + + For these tests, we only use the Anonymous user and the default user, without any modifications. + All the work should be done by the whitelists. + """ + + APP_WIDE_LOGIN_WHITELIST_VIEWS = ("/tests-2/",) + APP_WIDE_PERM_WHITELIST_VIEWS = ("/tests-2/",) + + def test__ensure_views_cannot_be_accessed_without_settings(self): + """Sanity check tests, to ensure these specific views cannot be accessed by default.""" + + # Should fail and redirect to login for anyone unauthenticated. + with self.subTest("As Anonymous User"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=AnonymousUser(), + # Expected view return data. + expected_status=200, + view_should_redirect=True, + # Expected content on page. + expected_title="Login |", + expected_content=[ + "Sign in to start your session", + "Remember Me", + "I forgot my password", + ], + ) + + # Should fail and redirect to home. + with self.subTest("As default standard user"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=self.test_user, + # Expected view return data. + expected_status=200, + view_should_redirect=True, + # Expected content on page. + expected_title="Dashboard", + expected_header="Dashboard <small>Version 2.0</small>", + expected_content=[ + "Monthly Recap Report", + "Visitors Report", + "Inventory", + "Downloads", + ], + ) + + @override_settings(ADMINLTE2_APP_WIDE_LOGIN_EXEMPT_WHITELIST=APP_WIDE_LOGIN_WHITELIST_VIEWS) + @override_settings(APP_WIDE_LOGIN_EXEMPT_WHITELIST=APP_WIDE_LOGIN_WHITELIST_VIEWS) + @patch("adminlte2_pdq.constants.APP_WIDE_LOGIN_EXEMPT_WHITELIST", APP_WIDE_LOGIN_WHITELIST_VIEWS) + @patch("adminlte2_pdq.middleware.APP_WIDE_LOGIN_EXEMPT_WHITELIST", APP_WIDE_LOGIN_WHITELIST_VIEWS) + def test__verify_login_whitelist(self): + """Tests to verify handling of "app-wide" login whitelist.""" + + # Should fail and redirect to home. + with self.subTest("As Anonymous User"): + with warns(Warning) as warning_info: + + # Should fail and redirect to home. But then home requires login so it still redirects to login. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=AnonymousUser(), + # Expected view return data. + expected_status=200, + view_should_redirect=True, + # Expected content on page. + expected_title="Login |", + expected_content=[ + "Sign in to start your session", + "Remember Me", + "I forgot my password", + ], + ) + + # Should fail and redirect to home. + with self.subTest("As default standard user"): + with warns(Warning) as warning_info: + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=self.test_user, + # Expected view return data. + expected_status=200, + view_should_redirect=True, + # Expected content on page. + expected_title="Dashboard", + expected_header="Dashboard <small>Version 2.0</small>", + expected_content=[ + "Monthly Recap Report", + "Visitors Report", + "Inventory", + "Downloads", + ], + ) + + @override_settings(ADMINLTE2_APP_WIDE_STRICT_POLICY_WHITELIST=APP_WIDE_PERM_WHITELIST_VIEWS) + @override_settings(APP_WIDE_STRICT_POLICY_WHITELIST=APP_WIDE_PERM_WHITELIST_VIEWS) + @patch("adminlte2_pdq.constants.APP_WIDE_STRICT_POLICY_WHITELIST", APP_WIDE_PERM_WHITELIST_VIEWS) + @patch("adminlte2_pdq.middleware.APP_WIDE_STRICT_POLICY_WHITELIST", APP_WIDE_PERM_WHITELIST_VIEWS) + def test__verify_perm_whitelist(self): + """Tests to verify handling of "app-wide" perm whitelist.""" + + # Should fail and redirect to login for anyone unauthenticated. + with self.subTest("As Anonymous User"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=AnonymousUser(), + # Expected view return data. + expected_status=200, + view_should_redirect=True, + # Expected content on page. + expected_title="Login |", + expected_content=[ + "Sign in to start your session", + "Remember Me", + "I forgot my password", + ], + ) + + # Should succeed and load as expected. + with self.subTest("As default standard user"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=self.test_user, + # Expected view return data. + expected_status=200, + view_should_redirect=False, + # Expected content on page. + expected_title="Standard View | Django AdminLtePdq Testing", + expected_header="Django AdminLtePdq | Standard View Header", + expected_content=[ + "Django AdminLtePdq | Standard View Subheader", + "Django AdminLtePdq | Standard View Content", + ], + ) + + @override_settings(ADMINLTE2_APP_WIDE_LOGIN_EXEMPT_WHITELIST=APP_WIDE_LOGIN_WHITELIST_VIEWS) + @override_settings(LOGIN_APP_WIDE_EXEMPT_WHITELIST=APP_WIDE_LOGIN_WHITELIST_VIEWS) + @override_settings(ADMINLTE2_APP_WIDE_STRICT_POLICY_WHITELIST=APP_WIDE_PERM_WHITELIST_VIEWS) + @override_settings(APP_WIDE_STRICT_POLICY_WHITELIST=APP_WIDE_PERM_WHITELIST_VIEWS) + @patch("adminlte2_pdq.constants.APP_WIDE_LOGIN_EXEMPT_WHITELIST", APP_WIDE_LOGIN_WHITELIST_VIEWS) + @patch("adminlte2_pdq.middleware.APP_WIDE_LOGIN_EXEMPT_WHITELIST", APP_WIDE_LOGIN_WHITELIST_VIEWS) + @patch("adminlte2_pdq.constants.APP_WIDE_STRICT_POLICY_WHITELIST", APP_WIDE_PERM_WHITELIST_VIEWS) + @patch("adminlte2_pdq.middleware.APP_WIDE_STRICT_POLICY_WHITELIST", APP_WIDE_PERM_WHITELIST_VIEWS) + def test__verify_both_whitelists(self): + """Tests to verify handling of combined "app-wide" login and permission whitelists.""" + + # Should succeed and load as expected. + with self.subTest("As Anonymous User"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=AnonymousUser(), + # Expected view return data. + expected_status=200, + view_should_redirect=False, + # Expected content on page. + expected_title="Standard View | Django AdminLtePdq Testing", + expected_header="Django AdminLtePdq | Standard View Header", + expected_content=[ + "Django AdminLtePdq | Standard View Subheader", + "Django AdminLtePdq | Standard View Content", + ], + ) + + # Should succeed and load as expected. + with self.subTest("As default standard user"): + + # Verify we get the expected page. + self.assertGetResponse( + # View setup. + "adminlte2_pdq_tests_2:standard-1", + user=self.test_user, + # Expected view return data. + expected_status=200, + view_should_redirect=False, + # Expected content on page. + expected_title="Standard View | Django AdminLtePdq Testing", + expected_header="Django AdminLtePdq | Standard View Header", + expected_content=[ + "Django AdminLtePdq | Standard View Subheader", + "Django AdminLtePdq | Standard View Content", + ], + ) -- GitLab