From e57f9beced579eb024454a887bc41fa03b6b6437 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Sun, 12 Oct 2025 21:51:34 -0500 Subject: [PATCH] Add render_config permission to ConfigTemplate render endpoint Extend render_config permission requirement to the ConfigTemplate render endpoint per issue comments. Changes: - Add TokenWritePermission check via get_permissions() override in ConfigTemplateViewSet - Restrict queryset to render_config permission in render() method - Add explicit render_config permission check - Add tests for ConfigTemplate.render() with and without permission - Update documentation to include ConfigTemplate endpoint --- docs/features/configuration-rendering.md | 6 +++++- netbox/extras/api/views.py | 17 ++++++++++++++++- netbox/extras/tests/test_api.py | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/features/configuration-rendering.md b/docs/features/configuration-rendering.md index 03e88395f..db3cc3205 100644 --- a/docs/features/configuration-rendering.md +++ b/docs/features/configuration-rendering.md @@ -92,4 +92,8 @@ http://netbox:8000/api/extras/config-templates/123/render/ \ ``` !!! note "Permissions" - Rendering device or virtual machine configurations via the REST API requires the `render_config` permission for the relevant object type. For example, to render a device's configuration via `/api/dcim/devices/{id}/render-config/`, a user must be assigned a permission for the "DCIM > Device" object type with the `render_config` action. + Rendering configuration templates via the REST API requires the `render_config` permission for the relevant object type: + + * To render a device's configuration via `/api/dcim/devices/{id}/render-config/`, assign a permission for "DCIM > Device" with the `render_config` action + * To render a virtual machine's configuration via `/api/virtualization/virtual-machines/{id}/render-config/`, assign a permission for "Virtualization > Virtual Machine" with the `render_config` action + * To render a config template directly via `/api/extras/config-templates/{id}/render/`, assign a permission for "Extras > Config Template" with the `render_config` action diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index f333d5dbf..9c8b6ca45 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,5 +1,6 @@ from django.http import Http404 from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ from django_rq.queues import get_connection from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import status @@ -16,12 +17,13 @@ from rq import Worker from extras import filtersets from extras.jobs import ScriptJob from extras.models import * -from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired +from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired, TokenWritePermission from netbox.api.features import SyncedDataMixin from netbox.api.metadata import ContentTypeMetadata from netbox.api.renderers import TextRenderer from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet from utilities.exceptions import RQWorkerNotRunningException +from utilities.permissions import get_permission_for_model from utilities.request import copy_safe_request from . import serializers from .mixins import ConfigTemplateRenderMixin @@ -238,13 +240,26 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo serializer_class = serializers.ConfigTemplateSerializer filterset_class = filtersets.ConfigTemplateFilterSet + def get_permissions(self): + # For render action, check only token write ability (not model permissions) + if self.action == 'render': + return [TokenWritePermission()] + return super().get_permissions() + @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer]) def render(self, request, pk): """ Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data, return the raw rendered content, rather than serialized JSON. """ + self.queryset = self.queryset.model.objects.all().restrict(request.user, 'render_config') configtemplate = self.get_object() + + # Check render_config permission + perm = get_permission_for_model(configtemplate, 'render_config') + if not request.user.has_perm(perm, obj=configtemplate): + raise PermissionDenied(_("This user does not have permission to render configuration templates.")) + context = request.data return self.render_configtemplate(request, configtemplate, context) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index d635916e4..70dfa96d9 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -3,6 +3,7 @@ import datetime from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.utils.timezone import make_aware, now +from rest_framework import status from core.choices import ManagedFileRootPathChoices from core.events import * @@ -854,6 +855,23 @@ class ConfigTemplateTest(APIViewTestCases.APIViewTestCase): ) ConfigTemplate.objects.bulk_create(config_templates) + def test_render(self): + configtemplate = ConfigTemplate.objects.first() + + self.add_permissions('extras.render_config_configtemplate') + url = reverse('extras-api:configtemplate-detail', kwargs={'pk': configtemplate.pk}) + 'render/' + response = self.client.post(url, {'foo': 'bar'}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data['content'], 'Foo: bar') + + def test_render_without_permission(self): + configtemplate = ConfigTemplate.objects.first() + + # No permissions added - user has no render_config permission + url = reverse('extras-api:configtemplate-detail', kwargs={'pk': configtemplate.pk}) + 'render/' + response = self.client.post(url, {'foo': 'bar'}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND) + class ScriptTest(APITestCase):