mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-01 03:00: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:
@ -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