mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	[FR] Add proactive system check for site_url (#9895)
* [FR] Add proactive system check for site_url Fixes #7847 * fix enviroment for tests * fix admin tests and make them more robust * improve tests for admin actions * add test for all scenarios * use right http error code * fix error code * remove dependency on bundle * fix test
This commit is contained in:
		| @@ -5,7 +5,7 @@ import sys | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.middleware import PersistentRemoteUserMiddleware | ||||
| from django.http import HttpResponse | ||||
| from django.shortcuts import redirect | ||||
| from django.shortcuts import redirect, render | ||||
| from django.urls import resolve, reverse_lazy | ||||
| from django.utils.deprecation import MiddlewareMixin | ||||
|  | ||||
| @@ -213,3 +213,42 @@ class InvenTreeRequestCacheMiddleware(MiddlewareMixin): | ||||
|         """Clear the cache object.""" | ||||
|         delete_session_cache() | ||||
|         return response | ||||
|  | ||||
|  | ||||
| class InvenTreeHostSettingsMiddleware(MiddlewareMixin): | ||||
|     """Middleware to check the host settings. | ||||
|  | ||||
|     Especially SITE_URL, trusted_origins. | ||||
|     """ | ||||
|  | ||||
|     def process_request(self, request): | ||||
|         """Check the host settings.""" | ||||
|         # Debug setups do not enforce these checks so we ignore that case | ||||
|         if settings.DEBUG: | ||||
|             return None | ||||
|  | ||||
|         # Handle commonly ignored paths that might also work without a correct setup (api, auth) | ||||
|         path = request.path_info | ||||
|         if path in urls or any(path.startswith(p) for p in paths_ignore): | ||||
|             return None | ||||
|  | ||||
|         # Ensure that the settings are set correctly with the current request | ||||
|         accessed_scheme = request._current_scheme_host | ||||
|         if accessed_scheme and not accessed_scheme.startswith(settings.SITE_URL): | ||||
|             msg = f'INVE-E7: The used path `{accessed_scheme}` does not match the SITE_URL `{settings.SITE_URL}`' | ||||
|             logger.error(msg) | ||||
|             return render( | ||||
|                 request, 'config_error.html', {'error_message': msg}, status=500 | ||||
|             ) | ||||
|  | ||||
|         # Check trusted origins | ||||
|         if not any( | ||||
|             accessed_scheme.startswith(origin) | ||||
|             for origin in settings.CSRF_TRUSTED_ORIGINS | ||||
|         ): | ||||
|             msg = f'INVE-E7: The used path `{accessed_scheme}` is not in the TRUSTED_ORIGINS' | ||||
|             logger.error(msg) | ||||
|             return render( | ||||
|                 request, 'config_error.html', {'error_message': msg}, status=500 | ||||
|             ) | ||||
|         return None | ||||
|   | ||||
| @@ -340,6 +340,7 @@ MIDDLEWARE = CONFIG.get( | ||||
|         'maintenance_mode.middleware.MaintenanceModeMiddleware', | ||||
|         'InvenTree.middleware.InvenTreeExceptionProcessor',  # Error reporting | ||||
|         'InvenTree.middleware.InvenTreeRequestCacheMiddleware',  # Request caching | ||||
|         'InvenTree.middleware.InvenTreeHostSettingsMiddleware',  # Ensuring correct hosting/security settings | ||||
|         'django_structlog.middlewares.RequestMiddleware',  # Structured logging | ||||
|     ], | ||||
| ) | ||||
|   | ||||
| @@ -87,3 +87,49 @@ class MiddlewareTests(InvenTreeTestCase): | ||||
|         except Http404: | ||||
|             log_error('testpath') | ||||
|         check(1) | ||||
|  | ||||
|     def test_site_url_checks(self): | ||||
|         """Test that the site URL check is correctly working.""" | ||||
|         # correctly set | ||||
|         with self.settings( | ||||
|             SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver'] | ||||
|         ): | ||||
|             response = self.client.get(reverse('web')) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertNotContains(response, 'INVE-E7') | ||||
|             self.assertContains(response, 'window.INVENTREE_SETTINGS') | ||||
|  | ||||
|         # wrongly set site URL | ||||
|         with self.settings(SITE_URL='https://example.com'): | ||||
|             response = self.client.get(reverse('web')) | ||||
|             self.assertEqual(response.status_code, 500) | ||||
|             self.assertContains( | ||||
|                 response, | ||||
|                 'INVE-E7: The used path `http://testserver` does not match', | ||||
|                 status_code=500, | ||||
|             ) | ||||
|             self.assertNotContains( | ||||
|                 response, 'window.INVENTREE_SETTINGS', status_code=500 | ||||
|             ) | ||||
|  | ||||
|         # wrongly set but in debug -> is ignored | ||||
|         with self.settings(SITE_URL='https://example.com', DEBUG=True): | ||||
|             response = self.client.get(reverse('web')) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertNotContains(response, 'INVE-E7') | ||||
|             self.assertContains(response, 'window.INVENTREE_SETTINGS') | ||||
|  | ||||
|         # wrongly set cors | ||||
|         with self.settings( | ||||
|             SITE_URL='http://testserver', | ||||
|             CORS_ORIGIN_ALLOW_ALL=False, | ||||
|             CSRF_TRUSTED_ORIGINS=['https://example.com'], | ||||
|         ): | ||||
|             response = self.client.get(reverse('web')) | ||||
|             self.assertEqual(response.status_code, 500) | ||||
|             self.assertContains( | ||||
|                 response, 'is not in the TRUSTED_ORIGINS', status_code=500 | ||||
|             ) | ||||
|             self.assertNotContains( | ||||
|                 response, 'window.INVENTREE_SETTINGS', status_code=500 | ||||
|             ) | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| import os | ||||
|  | ||||
| from django.test import override_settings | ||||
| from django.urls import reverse | ||||
|  | ||||
| from InvenTree.unit_test import InvenTreeTestCase | ||||
| @@ -13,6 +14,9 @@ class ViewTests(InvenTreeTestCase): | ||||
|     username = 'test_user' | ||||
|     password = 'test_pass' | ||||
|  | ||||
|     @override_settings( | ||||
|         SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver'] | ||||
|     ) | ||||
|     def test_api_doc(self): | ||||
|         """Test that the api-doc view works.""" | ||||
|         api_url = os.path.join(reverse('index'), 'api-doc') + '/' | ||||
| @@ -20,6 +24,9 @@ class ViewTests(InvenTreeTestCase): | ||||
|         response = self.client.get(api_url) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     @override_settings( | ||||
|         SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver'] | ||||
|     ) | ||||
|     def test_index_redirect(self): | ||||
|         """Top-level URL should redirect to "index" page.""" | ||||
|         response = self.client.get('/') | ||||
|   | ||||
| @@ -1809,6 +1809,9 @@ class URLCompatibilityTest(InvenTreeTestCase): | ||||
|         ('/stock/item/1/', '/web/stock/item/1/'), | ||||
|     ] | ||||
|  | ||||
|     @override_settings( | ||||
|         SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver'] | ||||
|     ) | ||||
|     def test_legacy_urls(self): | ||||
|         """Test legacy URLs.""" | ||||
|         for old_url, new_url in self.URL_MAPPINGS: | ||||
|   | ||||
| @@ -15,7 +15,7 @@ from django.contrib.auth.models import Group, Permission, User | ||||
| from django.db import connections, models | ||||
| from django.http.response import StreamingHttpResponse | ||||
| from django.test import TestCase | ||||
| from django.test.utils import CaptureQueriesContext | ||||
| from django.test.utils import CaptureQueriesContext, override_settings | ||||
| from django.urls import reverse | ||||
|  | ||||
| from djmoney.contrib.exchange.models import ExchangeBackend, Rate | ||||
| @@ -659,6 +659,9 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase): | ||||
|         self.assertEqual(b, b | a) | ||||
|  | ||||
|  | ||||
| @override_settings( | ||||
|     SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver'] | ||||
| ) | ||||
| class AdminTestCase(InvenTreeAPITestCase): | ||||
|     """Tests for the admin interface integration.""" | ||||
|  | ||||
| @@ -682,6 +685,7 @@ class AdminTestCase(InvenTreeAPITestCase): | ||||
|             reverse(f'admin:{app_app}_{app_mdl}_change', kwargs={'object_id': obj.pk}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertContains(response, 'Django site admin') | ||||
|  | ||||
|         return obj | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| """Unit tests for action plugins.""" | ||||
|  | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.test import override_settings | ||||
|  | ||||
| from InvenTree.unit_test import InvenTreeTestCase | ||||
| from plugin import registry | ||||
| @@ -9,6 +10,9 @@ from plugin import registry | ||||
| class SampleIntegrationPluginTests(InvenTreeTestCase): | ||||
|     """Tests for SampleIntegrationPlugin.""" | ||||
|  | ||||
|     @override_settings( | ||||
|         SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver'] | ||||
|     ) | ||||
|     def test_view(self): | ||||
|         """Check the function of the custom  sample plugin.""" | ||||
|         from common.models import InvenTreeSetting | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| """Tests for general API tests for the plugin app.""" | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.test import override_settings | ||||
| from django.urls import reverse | ||||
|  | ||||
| from rest_framework.exceptions import NotFound | ||||
| @@ -150,6 +151,9 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): | ||||
|             str(response.data['detail']), | ||||
|         ) | ||||
|  | ||||
|     @override_settings( | ||||
|         SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver'] | ||||
|     ) | ||||
|     def test_admin_action(self): | ||||
|         """Test the PluginConfig action commands.""" | ||||
|         url = reverse('admin:plugin_pluginconfig_changelist') | ||||
| @@ -168,6 +172,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): | ||||
|             follow=True, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertContains(response, 'Select Plugin Configuration to change') | ||||
|  | ||||
|         # deactivate plugin - deactivate again -> nothing will happen but the nothing 'changed' function is triggered | ||||
|         response = self.client.post( | ||||
| @@ -180,6 +185,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): | ||||
|             follow=True, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertContains(response, 'Select Plugin Configuration to change') | ||||
|  | ||||
|         # activate plugin | ||||
|         response = self.client.post( | ||||
| @@ -192,6 +198,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): | ||||
|             follow=True, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertContains(response, 'Select Plugin Configuration to change') | ||||
|  | ||||
|         # save to deactivate a plugin | ||||
|         response = self.client.post( | ||||
| @@ -200,6 +207,9 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): | ||||
|             follow=True, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertContains( | ||||
|             response, '(not active) | Change Plugin Configuration | Django site admin' | ||||
|         ) | ||||
|  | ||||
|     def test_model(self): | ||||
|         """Test the PluginConfig model.""" | ||||
|   | ||||
							
								
								
									
										13
									
								
								src/backend/InvenTree/templates/config_error.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/backend/InvenTree/templates/config_error.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| {% extends "base.html" %} | ||||
| {% load i18n %} | ||||
| {% load inventree_extras %} | ||||
|  | ||||
| {% block page_title %} | ||||
| {% inventree_title %} | {% trans "Configuration Error" %} | ||||
| {% endblock page_title %} | ||||
|  | ||||
| {% block content %} | ||||
| <h3>{% trans "Configuration Error" %}</h3> | ||||
| <p>{% blocktrans %}The {{ inventree_title }} server raised a configuration error{% endblocktrans %}</p> | ||||
| <p>{{ error_message }}</p> | ||||
| {% endblock content %} | ||||
		Reference in New Issue
	
	Block a user