2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 11:10:54 +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:
Matthias Mair
2025-07-01 01:12:48 +02:00
committed by GitHub
parent 83b524f808
commit 92c92d60e7
10 changed files with 135 additions and 2 deletions

View File

@ -44,6 +44,12 @@ This might be caused by an addition or removal of models to the code base. Runni
The scopes used for oAuth permissions have an issue and do not match the rulesets. The scopes used for oAuth permissions have an issue and do not match the rulesets.
This might be caused by an addition or removal of models to the code base or changes to the rulesets. Running the test suit should surface more logs with the error code indicating the exact infractions. This might be caused by an addition or removal of models to the code base or changes to the rulesets. Running the test suit should surface more logs with the error code indicating the exact infractions.
#### INVE-E7
**The host used does not match settings - Backend**
The settings for SITE_URL and ALLOWED_HOSTS do not match the host used to access the server. This might lead to issues with CSRF protection, CORS and other security features.
The settings must be adjusted.
### INVE-W (InvenTree Warning) ### INVE-W (InvenTree Warning)
Warnings - These are non-critical errors which should be addressed when possible. Warnings - These are non-critical errors which should be addressed when possible.

View File

@ -5,7 +5,7 @@ import sys
from django.conf import settings from django.conf import settings
from django.contrib.auth.middleware import PersistentRemoteUserMiddleware from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
from django.http import HttpResponse 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.urls import resolve, reverse_lazy
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
@ -213,3 +213,42 @@ class InvenTreeRequestCacheMiddleware(MiddlewareMixin):
"""Clear the cache object.""" """Clear the cache object."""
delete_session_cache() delete_session_cache()
return response 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

View File

@ -340,6 +340,7 @@ MIDDLEWARE = CONFIG.get(
'maintenance_mode.middleware.MaintenanceModeMiddleware', 'maintenance_mode.middleware.MaintenanceModeMiddleware',
'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting 'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting
'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching 'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching
'InvenTree.middleware.InvenTreeHostSettingsMiddleware', # Ensuring correct hosting/security settings
'django_structlog.middlewares.RequestMiddleware', # Structured logging 'django_structlog.middlewares.RequestMiddleware', # Structured logging
], ],
) )

View File

@ -87,3 +87,49 @@ class MiddlewareTests(InvenTreeTestCase):
except Http404: except Http404:
log_error('testpath') log_error('testpath')
check(1) 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
)

View File

@ -2,6 +2,7 @@
import os import os
from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from InvenTree.unit_test import InvenTreeTestCase from InvenTree.unit_test import InvenTreeTestCase
@ -13,6 +14,9 @@ class ViewTests(InvenTreeTestCase):
username = 'test_user' username = 'test_user'
password = 'test_pass' password = 'test_pass'
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
def test_api_doc(self): def test_api_doc(self):
"""Test that the api-doc view works.""" """Test that the api-doc view works."""
api_url = os.path.join(reverse('index'), 'api-doc') + '/' api_url = os.path.join(reverse('index'), 'api-doc') + '/'
@ -20,6 +24,9 @@ class ViewTests(InvenTreeTestCase):
response = self.client.get(api_url) response = self.client.get(api_url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
def test_index_redirect(self): def test_index_redirect(self):
"""Top-level URL should redirect to "index" page.""" """Top-level URL should redirect to "index" page."""
response = self.client.get('/') response = self.client.get('/')

View File

@ -1809,6 +1809,9 @@ class URLCompatibilityTest(InvenTreeTestCase):
('/stock/item/1/', '/web/stock/item/1/'), ('/stock/item/1/', '/web/stock/item/1/'),
] ]
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
def test_legacy_urls(self): def test_legacy_urls(self):
"""Test legacy URLs.""" """Test legacy URLs."""
for old_url, new_url in self.URL_MAPPINGS: for old_url, new_url in self.URL_MAPPINGS:

View File

@ -15,7 +15,7 @@ from django.contrib.auth.models import Group, Permission, User
from django.db import connections, models from django.db import connections, models
from django.http.response import StreamingHttpResponse from django.http.response import StreamingHttpResponse
from django.test import TestCase 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 django.urls import reverse
from djmoney.contrib.exchange.models import ExchangeBackend, Rate from djmoney.contrib.exchange.models import ExchangeBackend, Rate
@ -659,6 +659,9 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
self.assertEqual(b, b | a) self.assertEqual(b, b | a)
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
class AdminTestCase(InvenTreeAPITestCase): class AdminTestCase(InvenTreeAPITestCase):
"""Tests for the admin interface integration.""" """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}) reverse(f'admin:{app_app}_{app_mdl}_change', kwargs={'object_id': obj.pk})
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Django site admin')
return obj return obj

View File

@ -1,6 +1,7 @@
"""Unit tests for action plugins.""" """Unit tests for action plugins."""
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import override_settings
from InvenTree.unit_test import InvenTreeTestCase from InvenTree.unit_test import InvenTreeTestCase
from plugin import registry from plugin import registry
@ -9,6 +10,9 @@ from plugin import registry
class SampleIntegrationPluginTests(InvenTreeTestCase): class SampleIntegrationPluginTests(InvenTreeTestCase):
"""Tests for SampleIntegrationPlugin.""" """Tests for SampleIntegrationPlugin."""
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
def test_view(self): def test_view(self):
"""Check the function of the custom sample plugin.""" """Check the function of the custom sample plugin."""
from common.models import InvenTreeSetting from common.models import InvenTreeSetting

View File

@ -1,6 +1,7 @@
"""Tests for general API tests for the plugin app.""" """Tests for general API tests for the plugin app."""
from django.conf import settings from django.conf import settings
from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from rest_framework.exceptions import NotFound from rest_framework.exceptions import NotFound
@ -150,6 +151,9 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
str(response.data['detail']), str(response.data['detail']),
) )
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
def test_admin_action(self): def test_admin_action(self):
"""Test the PluginConfig action commands.""" """Test the PluginConfig action commands."""
url = reverse('admin:plugin_pluginconfig_changelist') url = reverse('admin:plugin_pluginconfig_changelist')
@ -168,6 +172,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
follow=True, follow=True,
) )
self.assertEqual(response.status_code, 200) 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 # deactivate plugin - deactivate again -> nothing will happen but the nothing 'changed' function is triggered
response = self.client.post( response = self.client.post(
@ -180,6 +185,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
follow=True, follow=True,
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Select Plugin Configuration to change')
# activate plugin # activate plugin
response = self.client.post( response = self.client.post(
@ -192,6 +198,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
follow=True, follow=True,
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Select Plugin Configuration to change')
# save to deactivate a plugin # save to deactivate a plugin
response = self.client.post( response = self.client.post(
@ -200,6 +207,9 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
follow=True, follow=True,
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(
response, '(not active) | Change Plugin Configuration | Django site admin'
)
def test_model(self): def test_model(self):
"""Test the PluginConfig model.""" """Test the PluginConfig model."""

View 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 %}