From d019a98b78c69d573277f1b1ad0e29b2fda07e5d Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Sun, 5 Dec 2021 16:22:24 +0100 Subject: [PATCH] Activate secure settings by default --- src/inventory/tests/test_item_images.py | 33 +++++++- src/inventory_project/settings/base.py | 25 ++++++ src/inventory_project/settings/local.py | 10 +++ src/inventory_project/settings/tests.py | 2 +- src/inventory_project/tests/test_admin.py | 13 +-- .../tests/test_admin_item.py | 35 +++++++- .../test_admin_item_login_1.snapshot.html | 81 +++++++++++++++++++ .../tests/test_admin_memo.py | 12 ++- 8 files changed, 194 insertions(+), 17 deletions(-) create mode 100644 src/inventory_project/tests/test_admin_item_login_1.snapshot.html diff --git a/src/inventory/tests/test_item_images.py b/src/inventory/tests/test_item_images.py index 10e575c..ea004a0 100644 --- a/src/inventory/tests/test_item_images.py +++ b/src/inventory/tests/test_item_images.py @@ -10,6 +10,7 @@ from inventory.models import ItemImageModel from inventory.tests.fixtures.users import get_normal_pyinventory_user +# @override_settings(SECURE_SSL_REDIRECT=False) class ItemImagesTestCase(TestCase): def test_basics(self): with mock.patch('secrets.token_urlsafe', return_value='user1token'): @@ -40,23 +41,47 @@ class ItemImagesTestCase(TestCase): url = image_instance.image.url assert url == '/media/user1token/12345678901234567890/mock_img.jpeg' + # HTTP -> HTTPS redirect: + response = self.client.get( + '/media/user1token/12345678901234567890/mock_img.jpeg', + secure=False + ) + self.assertRedirects( + response, + status_code=301, + expected_url='https://testserver/media/user1token/12345678901234567890/mock_img.jpeg', + fetch_redirect_response=False, + ) + # Anonymous has no access: - response = self.client.get('/media/user1token/12345678901234567890/mock_img.jpeg') + response = self.client.get( + '/media/user1token/12345678901234567890/mock_img.jpeg', + secure=True, + ) assert response.status_code == 403 # Can't access with wrong user: self.client.force_login(pyinventory_user2) - response = self.client.get('/media/user1token/12345678901234567890/mock_img.jpeg') + response = self.client.get( + '/media/user1token/12345678901234567890/mock_img.jpeg', + secure=True, + ) assert response.status_code == 403 # Can access with the right user: self.client.force_login(pyinventory_user1) - response = self.client.get('/media/user1token/12345678901234567890/mock_img.jpeg') + response = self.client.get( + '/media/user1token/12345678901234567890/mock_img.jpeg', + secure=True, + ) assert response.status_code == 200 assert isinstance(response, FileResponse) assert response.getvalue() == image_instance.image.open('rb').read() # Test whats happen, if token was deleted UserMediaTokenModel.objects.all().delete() - response = self.client.get('/media/user1token/12345678901234567890/mock_img.jpeg') + response = self.client.get( + '/media/user1token/12345678901234567890/mock_img.jpeg', + secure=True, + ) assert response.status_code == 400 # SuspiciousOperation -> HttpResponseBadRequest diff --git a/src/inventory_project/settings/base.py b/src/inventory_project/settings/base.py index 8788469..500e0c1 100644 --- a/src/inventory_project/settings/base.py +++ b/src/inventory_project/settings/base.py @@ -131,6 +131,31 @@ TEMPLATES = [ }, ] +# _____________________________________________________________________________ + +# Mark CSRF cookie as "secure" -> browsers sent cookie only with an HTTPS connection: +CSRF_COOKIE_SECURE = True + +# Mark session cookie as "secure" -> browsers sent cookie only with an HTTPS connection: +SESSION_COOKIE_SECURE = True + +# HTTP header/value combination that signifies a request is secure +# Your nginx.conf must set "X-Forwarded-Protocol" proxy header! +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') + +# SecurityMiddleware should redirects all non-HTTPS requests to HTTPS: +SECURE_SSL_REDIRECT = True + +# SecurityMiddleware should preload directive to the HTTP Strict Transport Security header: +SECURE_HSTS_PRELOAD = True + +# Instruct modern browsers to refuse to connect to your domain name via an insecure connection: +SECURE_HSTS_SECONDS = 3600 + +# SecurityMiddleware should add the "includeSubDomains" directive to the Strict-Transport-Security +# header: All subdomains of your domain should be served exclusively via SSL! +SECURE_HSTS_INCLUDE_SUBDOMAINS = True + # _____________________________________________________________________________ # Internationalization diff --git a/src/inventory_project/settings/local.py b/src/inventory_project/settings/local.py index fca9601..13eeff6 100644 --- a/src/inventory_project/settings/local.py +++ b/src/inventory_project/settings/local.py @@ -34,6 +34,16 @@ DATABASES = { } } print(f'Use Database: {DATABASES["default"]["NAME"]!r}', file=__sys.stderr) +# _____________________________________________________________________________ + +# Disable security features, because development server doesn't support HTTPS +CSRF_COOKIE_SECURE = False +SESSION_COOKIE_SECURE = False +SECURE_PROXY_SSL_HEADER = None +SECURE_SSL_REDIRECT = False +SECURE_HSTS_PRELOAD = False +SECURE_HSTS_SECONDS = 0 +SECURE_HSTS_INCLUDE_SUBDOMAINS = False # _____________________________________________________________________________ # AlwaysLoggedInAsSuperUser diff --git a/src/inventory_project/settings/tests.py b/src/inventory_project/settings/tests.py index 9e788b5..caca43d 100644 --- a/src/inventory_project/settings/tests.py +++ b/src/inventory_project/settings/tests.py @@ -10,7 +10,7 @@ DATABASES = { } } -SECRET_KEY = 'No individual secret for tests ;)' +SECRET_KEY = 'No individual secret... But this settings should only be used in tests ;)' # Run the tests as on production: Without DBEUG: DEBUG = False diff --git a/src/inventory_project/tests/test_admin.py b/src/inventory_project/tests/test_admin.py index 43c4940..e1b6dee 100644 --- a/src/inventory_project/tests/test_admin.py +++ b/src/inventory_project/tests/test_admin.py @@ -2,7 +2,7 @@ import os import unittest from django.contrib.auth.models import User -from django.test import TestCase +from django.test import TestCase, override_settings from django_processinfo.models import ProcessInfo, SiteStatistics from django_tools.selenium.chromedriver import chromium_available from django_tools.selenium.django import ( @@ -22,14 +22,15 @@ class AdminAnonymousTests(TestCase): """ def test_login_en(self): - response = self.client.get("/admin/", HTTP_ACCEPT_LANGUAGE="en") - self.assertRedirects(response, expected_url="/admin/login/?next=/admin/") + response = self.client.get('/admin/', secure=True, HTTP_ACCEPT_LANGUAGE='en') + self.assertRedirects(response, expected_url='/admin/login/?next=/admin/', fetch_redirect_response=False) def test_login_de(self): - response = self.client.get("/admin/", HTTP_ACCEPT_LANGUAGE="de") - self.assertRedirects(response, expected_url="/admin/login/?next=/admin/") + response = self.client.get('/admin/', secure=True, HTTP_ACCEPT_LANGUAGE='de') + self.assertRedirects(response, expected_url='/admin/login/?next=/admin/', fetch_redirect_response=False) +@override_settings(SECURE_SSL_REDIRECT=False) class ProcessinfoAdminTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -87,6 +88,7 @@ class ProcessinfoAdminTestCase(TestCase): @unittest.skipIf('CI' in os.environ, 'Skip, selenium tests does not work on CI run!') @unittest.skipUnless(chromium_available(), "Skip because Chromium is not available!") +@override_settings(SECURE_SSL_REDIRECT=False) class AdminChromiumTests(SeleniumChromiumStaticLiveServerTestCase): def test_admin_login_page(self): self.driver.get(self.live_server_url + "/admin/login/") @@ -97,6 +99,7 @@ class AdminChromiumTests(SeleniumChromiumStaticLiveServerTestCase): @unittest.skipIf('CI' in os.environ, 'Skip, selenium tests does not work on CI run!') @unittest.skipUnless(firefox_available(), "Skip because Firefox is not available!") +@override_settings(SECURE_SSL_REDIRECT=False) class AdminFirefoxTests(SeleniumFirefoxStaticLiveServerTestCase): def test_admin_login_page(self): self.driver.get(self.live_server_url + "/admin/login/") diff --git a/src/inventory_project/tests/test_admin_item.py b/src/inventory_project/tests/test_admin_item.py index bda1ef6..79f0b71 100644 --- a/src/inventory_project/tests/test_admin_item.py +++ b/src/inventory_project/tests/test_admin_item.py @@ -7,7 +7,7 @@ from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin from bx_py_utils.test_utils.snapshot import assert_html_snapshot from django.contrib.auth.models import User from django.template.defaulttags import CsrfTokenNode, NowNode -from django.test import TestCase +from django.test import TestCase, override_settings from django.utils import timezone from django_tools.unittest_utils.mockup import ImageDummy from model_bakery import baker @@ -46,15 +46,42 @@ ITEM_FORM_DEFAULTS = { ITEM_FORM_DEFAULTS = tuple(ITEM_FORM_DEFAULTS.items()) -class AdminAnonymousTests(TestCase): +class AdminAnonymousTests(HtmlAssertionMixin, TestCase): def test_login(self): - response = self.client.get('/admin/inventory/itemmodel/add/', HTTP_ACCEPT_LANGUAGE='en') + # HTTP -> HTTPS redirect: + response = self.client.get('/admin/', HTTP_ACCEPT_LANGUAGE='en') self.assertRedirects( response, - expected_url='/admin/login/?next=/admin/inventory/itemmodel/add/' + expected_url='https://testserver/admin/', + status_code=301, # Permanent redirect + fetch_redirect_response=False ) + response = self.client.get( + path='/admin/inventory/itemmodel/add/', + secure=True, + HTTP_ACCEPT_LANGUAGE='en' + ) + self.assertRedirects( + response, + expected_url='/admin/login/?next=/admin/inventory/itemmodel/add/', + fetch_redirect_response=False + ) + with mock.patch.object(CsrfTokenNode, 'render', return_value='MockedCsrfTokenNode'): + response = self.client.get( + path='/admin/login/', + secure=True, + HTTP_ACCEPT_LANGUAGE='en' + ) + self.assert_html_parts(response, parts=( + f'Log in | PyInventory v{__version__}', + '', + '', + )) + assert_html_response_snapshot(response, validate=False) + +@override_settings(SECURE_SSL_REDIRECT=False) class AdminTestCase(HtmlAssertionMixin, TestCase): @classmethod def setUpTestData(cls): diff --git a/src/inventory_project/tests/test_admin_item_login_1.snapshot.html b/src/inventory_project/tests/test_admin_item_login_1.snapshot.html new file mode 100644 index 0000000..e5d92a6 --- /dev/null +++ b/src/inventory_project/tests/test_admin_item_login_1.snapshot.html @@ -0,0 +1,81 @@ + + + + + Log in | PyInventory v0.12.0 + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+ + MockedCsrfTokenNode +
+

+ + +

+

+ + +

+
+
+ + + +
+ +
+
+ +
+
+
+ + + diff --git a/src/inventory_project/tests/test_admin_memo.py b/src/inventory_project/tests/test_admin_memo.py index ed1b3e7..54dcb1b 100644 --- a/src/inventory_project/tests/test_admin_memo.py +++ b/src/inventory_project/tests/test_admin_memo.py @@ -3,7 +3,7 @@ from unittest import mock from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin from django.contrib.auth.models import User from django.template.defaulttags import CsrfTokenNode, NowNode -from django.test import TestCase +from django.test import TestCase, override_settings from django_tools.unittest_utils.mockup import ImageDummy from model_bakery import baker @@ -15,13 +15,19 @@ from inventory_project.tests.temp_utils import assert_html_response_snapshot class AdminAnonymousTests(TestCase): def test_login(self): - response = self.client.get('/admin/inventory/memomodel/add/', HTTP_ACCEPT_LANGUAGE='en') + response = self.client.get( + '/admin/inventory/memomodel/add/', + secure=True, + HTTP_ACCEPT_LANGUAGE='en' + ) self.assertRedirects( response, - expected_url='/admin/login/?next=/admin/inventory/memomodel/add/' + expected_url='/admin/login/?next=/admin/inventory/memomodel/add/', + fetch_redirect_response=False, ) +@override_settings(SECURE_SSL_REDIRECT=False) class AdminTestCase(HtmlAssertionMixin, TestCase): @classmethod def setUpTestData(cls):