From 1890286959333393442d1d7255badb9a43473e1b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 13 Jan 2023 10:00:49 -0500 Subject: [PATCH] Closes #11489: Refactor & combine core middleware --- netbox/netbox/middleware.py | 169 +++++++++++++----------------------- netbox/netbox/settings.py | 8 +- 2 files changed, 62 insertions(+), 115 deletions(-) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index edf88a23469..0b1d774848c 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -14,24 +14,73 @@ from netbox.views import handler_500 from utilities.api import is_api_request, rest_api_server_error +__all__ = ( + 'CoreMiddleware', + 'RemoteUserMiddleware', +) + + +class CoreMiddleware: -class LoginRequiredMiddleware: - """ - If LOGIN_REQUIRED is True, redirect all non-authenticated users to the login page. - """ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): - # Redirect unauthenticated requests (except those exempted) to the login page if LOGIN_REQUIRED is true - if settings.LOGIN_REQUIRED and not request.user.is_authenticated: - # Redirect unauthenticated requests - if not request.path_info.startswith(settings.EXEMPT_PATHS): - login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}' - return HttpResponseRedirect(login_url) + # Assign a random unique ID to the request. This will be used for change logging. + request.id = uuid.uuid4() + + # Enforce the LOGIN_REQUIRED config parameter. If true, redirect all non-exempt unauthenticated requests + # to the login page. + if ( + settings.LOGIN_REQUIRED and + not request.user.is_authenticated and + not request.path_info.startswith(settings.AUTH_EXEMPT_PATHS) + ): + login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}' + return HttpResponseRedirect(login_url) + + # Enable the change_logging context manager and process the request. + with change_logging(request): + response = self.get_response(request) + + # If this is an API request, attach an HTTP header annotating the API version (e.g. '3.5'). + if is_api_request(request): + response['API-Version'] = settings.REST_FRAMEWORK_VERSION + + # Clear any cached dynamic config parameters after each request. + clear_config() + + return response - return self.get_response(request) + def process_exception(self, request, exception): + """ + Implement custom error handling logic for production deployments. + """ + # Don't catch exceptions when in debug mode + if settings.DEBUG: + return + + # Cleanly handle exceptions that occur from REST API requests + if is_api_request(request): + return rest_api_server_error(request) + + # Ignore Http404s (defer to Django's built-in 404 handling) + if isinstance(exception, Http404): + return + + # Determine the type of exception. If it's a common issue, return a custom error page with instructions. + custom_template = None + if isinstance(exception, ProgrammingError): + custom_template = 'exceptions/programming_error.html' + elif isinstance(exception, ImportError): + custom_template = 'exceptions/import_error.html' + elif isinstance(exception, PermissionError): + custom_template = 'exceptions/permission_error.html' + + # Return a custom error message, or fall back to Django's default 500 error handling + if custom_template: + return handler_500(request, template_name=custom_template) class RemoteUserMiddleware(RemoteUserMiddleware_): @@ -104,101 +153,3 @@ def _get_groups(self, request): groups = [] logger.debug(f"Groups are {groups}") return groups - - -class ObjectChangeMiddleware: - """ - This middleware performs three functions in response to an object being created, updated, or deleted: - - 1. Create an ObjectChange to reflect the modification to the object in the changelog. - 2. Enqueue any relevant webhooks. - 3. Increment the metric counter for the event type. - - The post_save and post_delete signals are employed to catch object modifications, however changes are recorded a bit - differently for each. Objects being saved are cached into thread-local storage for action *after* the response has - completed. This ensures that serialization of the object is performed only after any related objects (e.g. tags) - have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the - object is recorded before it (and any related objects) are actually deleted from the database. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - # Assign a random unique ID to the request. This will be used to associate multiple object changes made during - # the same request. - request.id = uuid.uuid4() - - # Process the request with change logging enabled - with change_logging(request): - response = self.get_response(request) - - return response - - -class APIVersionMiddleware: - """ - If the request is for an API endpoint, include the API version as a response header. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - if is_api_request(request): - response['API-Version'] = settings.REST_FRAMEWORK_VERSION - return response - - -class DynamicConfigMiddleware: - """ - Store the cached NetBox configuration in thread-local storage for the duration of the request. - """ - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - clear_config() - return response - - -class ExceptionHandlingMiddleware: - """ - Intercept certain exceptions which are likely indicative of installation issues and provide helpful instructions - to the user. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - return self.get_response(request) - - def process_exception(self, request, exception): - - # Handle exceptions that occur from REST API requests - # if is_api_request(request): - # return rest_api_server_error(request) - - # Don't catch exceptions when in debug mode - if settings.DEBUG: - return - - # Ignore Http404s (defer to Django's built-in 404 handling) - if isinstance(exception, Http404): - return - - # Determine the type of exception. If it's a common issue, return a custom error page with instructions. - custom_template = None - if isinstance(exception, ProgrammingError): - custom_template = 'exceptions/programming_error.html' - elif isinstance(exception, ImportError): - custom_template = 'exceptions/import_error.html' - elif isinstance(exception, PermissionError): - custom_template = 'exceptions/permission_error.html' - - # Return a custom error message, or fall back to Django's default 500 error handling - if custom_template: - return handler_500(request, template_name=custom_template) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 5350fae6bfd..8b2ec9730e2 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -357,12 +357,8 @@ def _setting(name, default=None): 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', - 'netbox.middleware.ExceptionHandlingMiddleware', 'netbox.middleware.RemoteUserMiddleware', - 'netbox.middleware.LoginRequiredMiddleware', - 'netbox.middleware.DynamicConfigMiddleware', - 'netbox.middleware.APIVersionMiddleware', - 'netbox.middleware.ObjectChangeMiddleware', + 'netbox.middleware.CoreMiddleware', 'django_prometheus.middleware.PrometheusAfterMiddleware', ] @@ -447,7 +443,7 @@ def _setting(name, default=None): ) # All URLs starting with a string listed here are exempt from login enforcement -EXEMPT_PATHS = ( +AUTH_EXEMPT_PATHS = ( f'/{BASE_PATH}api/', f'/{BASE_PATH}graphql/', f'/{BASE_PATH}login/',