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
This commit is contained in:
Jason Novinger
2025-10-12 21:51:34 -05:00
parent 8e72e75d61
commit e57f9beced
3 changed files with 39 additions and 2 deletions

View File

@@ -92,4 +92,8 @@ http://netbox:8000/api/extras/config-templates/123/render/ \
``` ```
!!! note "Permissions" !!! 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

View File

@@ -1,5 +1,6 @@
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from django_rq.queues import get_connection from django_rq.queues import get_connection
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status from rest_framework import status
@@ -16,12 +17,13 @@ from rq import Worker
from extras import filtersets from extras import filtersets
from extras.jobs import ScriptJob from extras.jobs import ScriptJob
from extras.models import * 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.features import SyncedDataMixin
from netbox.api.metadata import ContentTypeMetadata from netbox.api.metadata import ContentTypeMetadata
from netbox.api.renderers import TextRenderer from netbox.api.renderers import TextRenderer
from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet
from utilities.exceptions import RQWorkerNotRunningException from utilities.exceptions import RQWorkerNotRunningException
from utilities.permissions import get_permission_for_model
from utilities.request import copy_safe_request from utilities.request import copy_safe_request
from . import serializers from . import serializers
from .mixins import ConfigTemplateRenderMixin from .mixins import ConfigTemplateRenderMixin
@@ -238,13 +240,26 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
serializer_class = serializers.ConfigTemplateSerializer serializer_class = serializers.ConfigTemplateSerializer
filterset_class = filtersets.ConfigTemplateFilterSet 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]) @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
def render(self, request, pk): def render(self, request, pk):
""" """
Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data, 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. 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() 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 context = request.data
return self.render_configtemplate(request, configtemplate, context) return self.render_configtemplate(request, configtemplate, context)

View File

@@ -3,6 +3,7 @@ import datetime
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from rest_framework import status
from core.choices import ManagedFileRootPathChoices from core.choices import ManagedFileRootPathChoices
from core.events import * from core.events import *
@@ -854,6 +855,23 @@ class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
) )
ConfigTemplate.objects.bulk_create(config_templates) 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): class ScriptTest(APITestCase):