From 8e72e75d61790e455a5a917068f6c29e9f5c6a81 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Sun, 12 Oct 2025 21:40:11 -0500 Subject: [PATCH] Remove add permission requirement for render_config endpoint Remove the add permission requirement from the render-config API endpoint while maintaining token write_enabled enforcement as specified in #16681. Changes: - Add TokenWritePermission class to check token write ability without requiring specific model permissions - Override get_permissions() in RenderConfigMixin to use TokenWritePermission instead of TokenPermissions for render_config action - Replace queryset restriction: use render_config instead of add - Remove add permissions from tests - render_config permission now sufficient - Update tests to expect 404 when permission denied (NetBox standard pattern) Per #16681: 'requirement for write permission makes sense for API calls (because we're accepting and processing arbitrary user data), the specific permission for creating devices does not' --- netbox/dcim/tests/test_api.py | 4 +--- netbox/extras/api/mixins.py | 9 +++++++++ netbox/netbox/api/authentication.py | 11 +++++++++++ netbox/virtualization/tests/test_api.py | 4 +--- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index e06ea0dbf..e2e5b9070 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1487,7 +1487,6 @@ class DeviceTest(APIViewTestCases.APIViewTestCase): device.save() self.add_permissions('dcim.render_config_device') - self.add_permissions('dcim.add_device') url = reverse('dcim-api:device-detail', kwargs={'pk': device.pk}) + 'render-config/' response = self.client.post(url, {}, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) @@ -1504,10 +1503,9 @@ class DeviceTest(APIViewTestCases.APIViewTestCase): device.save() # No permissions added - user has no render_config permission - self.add_permissions('dcim.add_device') url = reverse('dcim-api:device-detail', kwargs={'pk': device.pk}) + 'render-config/' response = self.client.post(url, {}, format='json', **self.header) - self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN) + self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND) class ModuleTest(APIViewTestCases.APIViewTestCase): diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index 75929b261..c49bf0252 100644 --- a/netbox/extras/api/mixins.py +++ b/netbox/extras/api/mixins.py @@ -6,6 +6,7 @@ from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.status import HTTP_400_BAD_REQUEST +from netbox.api.authentication import TokenWritePermission from netbox.api.renderers import TextRenderer from utilities.permissions import get_permission_for_model from .serializers import ConfigTemplateSerializer @@ -67,11 +68,19 @@ class RenderConfigMixin(ConfigTemplateRenderMixin): """ Provides a /render-config/ endpoint for REST API views whose model may have a ConfigTemplate assigned. """ + + def get_permissions(self): + # For render_config action, check only token write ability (not model permissions) + if self.action == 'render_config': + return [TokenWritePermission()] + return super().get_permissions() + @action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer]) def render_config(self, request, pk): """ Resolve and render the preferred ConfigTemplate for this Device. """ + self.queryset = self.queryset.model.objects.all().restrict(request.user, 'render_config') instance = self.get_object() # Check render_config permission diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index daa512ee0..7397f99ba 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -164,6 +164,17 @@ class TokenPermissions(DjangoObjectPermissions): return super().has_object_permission(request, view, obj) +class TokenWritePermission(BasePermission): + """ + Verify the token has write_enabled for unsafe methods, without requiring specific model permissions. + Used for custom actions that accept user data but don't map to standard CRUD operations. + """ + def has_permission(self, request, view): + if isinstance(request.auth, Token): + return request.method in SAFE_METHODS or request.auth.write_enabled + return True + + class IsAuthenticatedOrLoginNotRequired(BasePermission): """ Returns True if the user is authenticated or LOGIN_REQUIRED is False. diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 6d763e714..0e9f45eb8 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -282,7 +282,6 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase): vm.save() self.add_permissions('virtualization.render_config_virtualmachine') - self.add_permissions('virtualization.add_virtualmachine') url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': vm.pk}) + 'render-config/' response = self.client.post(url, {}, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) @@ -299,10 +298,9 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase): vm.save() # No permissions added - user has no render_config permission - self.add_permissions('virtualization.add_virtualmachine') url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': vm.pk}) + 'render-config/' response = self.client.post(url, {}, format='json', **self.header) - self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN) + self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND) class VMInterfaceTest(APIViewTestCases.APIViewTestCase):