Skip to content

Commit

Permalink
Closes #11489: Refactor & combine core middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Jan 13, 2023
1 parent d997fe9 commit 1890286
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 115 deletions.
169 changes: 60 additions & 109 deletions netbox/netbox/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_):
Expand Down Expand Up @@ -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)
8 changes: 2 additions & 6 deletions netbox/netbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]

Expand Down Expand Up @@ -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/',
Expand Down

0 comments on commit 1890286

Please sign in to comment.