From c835ec5102b5ea01431bc017aea691f0939464e0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 15 Dec 2020 21:04:47 -0500 Subject: [PATCH] Fixes #5470: Fix exception when making OPTIONS request for a REST API list endpoint --- docs/release-notes/version-2.10.md | 1 + netbox/netbox/api/metadata.py | 37 +++++++++++++++++++++++++++++- netbox/netbox/settings.py | 1 + netbox/utilities/testing/api.py | 17 ++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 6c4433e37..3cf2b0d09 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -12,6 +12,7 @@ * [#5463](https://github.com/netbox-community/netbox/issues/5463) - Back-to-back Circuit Termination throws AttributeError exception * [#5465](https://github.com/netbox-community/netbox/issues/5465) - Correct return URL when disconnecting a cable from a device * [#5466](https://github.com/netbox-community/netbox/issues/5466) - Fix validation for required custom fields +* [#5470](https://github.com/netbox-community/netbox/issues/5470) - Fix exception when making `OPTIONS` request for a REST API list endpoint --- diff --git a/netbox/netbox/api/metadata.py b/netbox/netbox/api/metadata.py index 1d0397e4d..bc4ecf871 100644 --- a/netbox/netbox/api/metadata.py +++ b/netbox/netbox/api/metadata.py @@ -1,10 +1,45 @@ +from django.core.exceptions import PermissionDenied +from django.http import Http404 from django.utils.encoding import force_str +from rest_framework import exceptions from rest_framework.metadata import SimpleMetadata +from rest_framework.request import clone_request from netbox.api import ContentTypeField -class ContentTypeMetadata(SimpleMetadata): +class BulkOperationMetadata(SimpleMetadata): + + def determine_actions(self, request, view): + """ + Replace the stock determine_actions() method to assess object permissions only + when viewing a specific object. This is necessary to support OPTIONS requests + with bulk update in place (see #5470). + """ + actions = {} + for method in {'PUT', 'POST'} & set(view.allowed_methods): + view.request = clone_request(request, method) + try: + # Test global permissions + if hasattr(view, 'check_permissions'): + view.check_permissions(view.request) + # Test object permissions (if viewing a specific object) + if method == 'PUT' and view.lookup_url_kwarg and hasattr(view, 'get_object'): + view.get_object() + except (exceptions.APIException, PermissionDenied, Http404): + pass + else: + # If user has appropriate permissions for the view, include + # appropriate metadata about the fields that should be supplied. + serializer = view.get_serializer() + actions[method] = self.get_serializer_info(serializer) + finally: + view.request = request + + return actions + + +class ContentTypeMetadata(BulkOperationMetadata): def get_field_info(self, field): field_info = super().get_field_info(field) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 8c3c992e2..6dbc61018 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -467,6 +467,7 @@ REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', ), + 'DEFAULT_METADATA_CLASS': 'netbox.api.metadata.BulkOperationMetadata', 'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination', 'DEFAULT_PERMISSION_CLASSES': ( 'netbox.api.authentication.TokenPermissions', diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index f81722fa8..f4f4ffefe 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -109,6 +109,15 @@ class APIViewTestCases: url = self._get_detail_url(instance2) self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_404_NOT_FOUND) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_options_object(self): + """ + Make an OPTIONS request for a single object. + """ + url = self._get_detail_url(self._get_queryset().first()) + response = self.client.options(url, **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + class ListObjectsViewTestCase(APITestCase): brief_fields = [] @@ -174,6 +183,14 @@ class APIViewTestCases: self.assertHttpStatus(response, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 2) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_options_objects(self): + """ + Make an OPTIONS request for a list endpoint. + """ + response = self.client.options(self._get_list_url(), **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + class CreateObjectViewTestCase(APITestCase): create_data = [] validation_excluded_fields = []