mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-23 20:12:42 -06:00
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'
This commit is contained in:
@@ -1487,7 +1487,6 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
device.save()
|
device.save()
|
||||||
|
|
||||||
self.add_permissions('dcim.render_config_device')
|
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/'
|
url = reverse('dcim-api:device-detail', kwargs={'pk': device.pk}) + 'render-config/'
|
||||||
response = self.client.post(url, {}, format='json', **self.header)
|
response = self.client.post(url, {}, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
@@ -1504,10 +1503,9 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
device.save()
|
device.save()
|
||||||
|
|
||||||
# No permissions added - user has no render_config permission
|
# 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/'
|
url = reverse('dcim-api:device-detail', kwargs={'pk': device.pk}) + 'render-config/'
|
||||||
response = self.client.post(url, {}, format='json', **self.header)
|
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):
|
class ModuleTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from rest_framework.renderers import JSONRenderer
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||||
|
|
||||||
|
from netbox.api.authentication import TokenWritePermission
|
||||||
from netbox.api.renderers import TextRenderer
|
from netbox.api.renderers import TextRenderer
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from .serializers import ConfigTemplateSerializer
|
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.
|
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])
|
@action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer])
|
||||||
def render_config(self, request, pk):
|
def render_config(self, request, pk):
|
||||||
"""
|
"""
|
||||||
Resolve and render the preferred ConfigTemplate for this Device.
|
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()
|
instance = self.get_object()
|
||||||
|
|
||||||
# Check render_config permission
|
# Check render_config permission
|
||||||
|
|||||||
@@ -164,6 +164,17 @@ class TokenPermissions(DjangoObjectPermissions):
|
|||||||
return super().has_object_permission(request, view, obj)
|
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):
|
class IsAuthenticatedOrLoginNotRequired(BasePermission):
|
||||||
"""
|
"""
|
||||||
Returns True if the user is authenticated or LOGIN_REQUIRED is False.
|
Returns True if the user is authenticated or LOGIN_REQUIRED is False.
|
||||||
|
|||||||
@@ -282,7 +282,6 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
|||||||
vm.save()
|
vm.save()
|
||||||
|
|
||||||
self.add_permissions('virtualization.render_config_virtualmachine')
|
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/'
|
url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': vm.pk}) + 'render-config/'
|
||||||
response = self.client.post(url, {}, format='json', **self.header)
|
response = self.client.post(url, {}, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
@@ -299,10 +298,9 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
|||||||
vm.save()
|
vm.save()
|
||||||
|
|
||||||
# No permissions added - user has no render_config permission
|
# 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/'
|
url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': vm.pk}) + 'render-config/'
|
||||||
response = self.client.post(url, {}, format='json', **self.header)
|
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):
|
class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user