diff --git a/netbox/extras/views.py b/netbox/extras/views.py index c902b1499..6544abdad 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1200,7 +1200,7 @@ class ScriptResultView(TableMixin, generic.ObjectView): # Markdown # -class RenderMarkdownView(View): +class RenderMarkdownView(LoginRequiredMixin, View): def post(self, request): form = forms.RenderMarkdownForm(request.POST) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index 58c70451c..3d94734ee 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -1,6 +1,5 @@ import logging import uuid -from urllib import parse from django.conf import settings from django.contrib import auth, messages @@ -33,16 +32,6 @@ class CoreMiddleware: # 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 event_tracking context manager and process the request. with event_tracking(request): response = self.get_response(request) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 7c586e109..42ae8cb3e 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -502,15 +502,6 @@ EXEMPT_EXCLUDE_MODELS = ( ('users', 'user'), ) -# All URLs starting with a string listed here are exempt from login enforcement -AUTH_EXEMPT_PATHS = ( - f'/{BASE_PATH}api/', - f'/{BASE_PATH}graphql/', - f'/{BASE_PATH}login/', - f'/{BASE_PATH}oauth/', - f'/{BASE_PATH}metrics', -) - # All URLs starting with a string listed here are exempt from maintenance mode enforcement MAINTENANCE_EXEMPT_PATHS = ( f'/{BASE_PATH}admin/', diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index 9d898be2f..821d87e17 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -1,3 +1,4 @@ +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.contrib import messages from django.db import transaction @@ -12,7 +13,7 @@ from extras.forms import JournalEntryForm from extras.models import JournalEntry from extras.tables import JournalEntryTable from utilities.permissions import get_permission_for_model -from utilities.views import GetReturnURLMixin, ViewTab +from utilities.views import ConditionalLoginRequiredMixin, GetReturnURLMixin, ViewTab from .base import BaseMultiObjectView __all__ = ( @@ -24,7 +25,7 @@ __all__ = ( ) -class ObjectChangeLogView(View): +class ObjectChangeLogView(ConditionalLoginRequiredMixin, View): """ Present a history of changes made to a particular object. The model class must be passed as a keyword argument when referencing this view in a URL path. For example: @@ -77,7 +78,7 @@ class ObjectChangeLogView(View): }) -class ObjectJournalView(View): +class ObjectJournalView(ConditionalLoginRequiredMixin, View): """ Show all journal entries for an object. The model class must be passed as a keyword argument when referencing this view in a URL path. For example: @@ -138,7 +139,7 @@ class ObjectJournalView(View): }) -class ObjectJobsView(View): +class ObjectJobsView(ConditionalLoginRequiredMixin, View): """ Render a list of all Job assigned to an object. For example: @@ -191,7 +192,7 @@ class ObjectJobsView(View): }) -class ObjectSyncDataView(View): +class ObjectSyncDataView(LoginRequiredMixin, View): def post(self, request, model, **kwargs): """ diff --git a/netbox/netbox/views/htmx.py b/netbox/netbox/views/htmx.py index 04ddcb06b..b7894e36c 100644 --- a/netbox/netbox/views/htmx.py +++ b/netbox/netbox/views/htmx.py @@ -1,3 +1,4 @@ +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.http import Http404 @@ -6,7 +7,7 @@ from django.utils.module_loading import import_string from django.views.generic import View -class ObjectSelectorView(View): +class ObjectSelectorView(LoginRequiredMixin, View): template_name = 'htmx/object_selector.html' def get(self, request): diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index 569fcf728..c584e99e4 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -19,6 +19,7 @@ from netbox.search.backends import search_backend from netbox.tables import SearchTable from utilities.htmx import htmx_partial from utilities.paginator import EnhancedPaginator, get_paginate_count +from utilities.views import ConditionalLoginRequiredMixin __all__ = ( 'HomeView', @@ -28,7 +29,7 @@ __all__ = ( Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count')) -class HomeView(View): +class HomeView(ConditionalLoginRequiredMixin, View): template_name = 'home.html' def get(self, request): @@ -62,7 +63,7 @@ class HomeView(View): }) -class SearchView(View): +class SearchView(ConditionalLoginRequiredMixin, View): def get(self, request): results = [] diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 1cd3b1765..f7181ea92 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -1,5 +1,6 @@ from typing import Iterable +from django.conf import settings from django.contrib.auth.mixins import AccessMixin from django.core.exceptions import ImproperlyConfigured from django.urls import reverse @@ -13,6 +14,7 @@ from utilities.relations import get_related_models from .permissions import resolve_permission __all__ = ( + 'ConditionalLoginRequiredMixin', 'ContentTypePermissionRequiredMixin', 'GetRelatedModelsMixin', 'GetReturnURLMixin', @@ -27,10 +29,20 @@ __all__ = ( # View Mixins # -class ContentTypePermissionRequiredMixin(AccessMixin): +class ConditionalLoginRequiredMixin(AccessMixin): + """ + Similar to Django's LoginRequiredMixin, but enforces authentication only if LOGIN_REQUIRED is True. + """ + def dispatch(self, request, *args, **kwargs): + if settings.LOGIN_REQUIRED and not request.user.is_authenticated: + return self.handle_no_permission() + return super().dispatch(request, *args, **kwargs) + + +class ContentTypePermissionRequiredMixin(ConditionalLoginRequiredMixin): """ Similar to Django's built-in PermissionRequiredMixin, but extended to check model-level permission assignments. - This is related to ObjectPermissionRequiredMixin, except that is does not enforce object-level permissions, + This is related to ObjectPermissionRequiredMixin, except that it does not enforce object-level permissions, and fits within NetBox's custom permission enforcement system. additional_permissions: An optional iterable of statically declared permissions to evaluate in addition to those @@ -63,7 +75,7 @@ class ContentTypePermissionRequiredMixin(AccessMixin): return super().dispatch(request, *args, **kwargs) -class ObjectPermissionRequiredMixin(AccessMixin): +class ObjectPermissionRequiredMixin(ConditionalLoginRequiredMixin): """ Similar to Django's built-in PermissionRequiredMixin, but extended to check for both model-level and object-level permission assignments. If the user has only object-level permissions assigned, the view's queryset is filtered