diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 69939d6e5..77286c97d 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -172,6 +172,9 @@ To exempt _all_ models from view permission enforcement, set the following. (Not EXEMPT_VIEW_PERMISSIONS = ['*'] ``` +!!! note + Using a wildcard will not affect certain potentially sensitive models, such as user permissions. If there is a need to exempt these models, they must be specified individually. + --- ## ENFORCE_GLOBAL_UNIQUE diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index a485fe9df..757b186cc 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -1,9 +1,11 @@ from django.contrib.auth.models import Group, User from django.contrib.contenttypes.models import ContentType +from django.test import override_settings from django.urls import reverse +from rest_framework import status from users.models import ObjectPermission -from utilities.testing import APIViewTestCases, APITestCase +from utilities.testing import APIViewTestCases, APITestCase, disable_warnings class AppTest(APITestCase): @@ -72,3 +74,17 @@ class ObjectPermissionTest(APIViewTestCases.APIViewTestCase): 'constraints': {'name': 'TEST6'}, }, ] + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_list_objects_anonymous(self): + # Endpoint should never be exposed via EXEMPT_VIEW_PERMISSIONS + url = self._get_list_url() + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_403_FORBIDDEN) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_get_object_anonymous(self): + # Endpoint should never be exposed via EXEMPT_VIEW_PERMISSIONS + url = self._get_detail_url(self._get_queryset().first()) + with disable_warnings('django.request'): + self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_403_FORBIDDEN) diff --git a/netbox/utilities/permissions.py b/netbox/utilities/permissions.py index 44c34942f..0321fc6a7 100644 --- a/netbox/utilities/permissions.py +++ b/netbox/utilities/permissions.py @@ -1,6 +1,12 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType +# Exclude potentially sensitive models from wild view exemption. These may still be exempted +# by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter. +EXEMPT_EXCLUDE_MODELS = ( + ('users', 'objectpermission'), +) + def get_permission_for_model(model, action): """ @@ -63,11 +69,11 @@ def permission_is_exempt(name): if action == 'view': if ( - # All models are exempt from view permission enforcement - '*' in settings.EXEMPT_VIEW_PERMISSIONS + # All models (excluding those in EXEMPT_EXCLUDE_MODELS) are exempt from view permission enforcement + '*' in settings.EXEMPT_VIEW_PERMISSIONS and (app_label, model_name) not in EXEMPT_EXCLUDE_MODELS ) or ( # This specific model is exempt from view permission enforcement - '{}.{}'.format(app_label, model_name) in settings.EXEMPT_VIEW_PERMISSIONS + f'{app_label}.{model_name}' in settings.EXEMPT_VIEW_PERMISSIONS ): return True diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 6bd9770d4..b1b1d9f55 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -103,14 +103,15 @@ class APIViewTestCases: url = self._get_list_url() response = self.client.get(url, **self.header) - self.assertEqual(len(response.data['results']), self._get_queryset().count()) self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(len(response.data['results']), self._get_queryset().count()) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + @override_settings(EXEMPT_VIEW_PERMISSIONS=[]) def test_list_objects_brief(self): """ - GET a list of objects using the "brief" parameter as an unauthenticated user. + GET a list of objects using the "brief" parameter. """ + self.add_permissions(f'{self.model._meta.app_label}.view_{self.model._meta.model_name}') url = f'{self._get_list_url()}?brief=1' response = self.client.get(url, **self.header)