Compare commits

...

4 Commits

Author SHA1 Message Date
Jason Novinger
9fd86469ab Merge 68ec84ff70 into 875e3e7979 2025-12-15 21:11:33 +00:00
Jason Novinger
68ec84ff70 Address PR feedback
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
2025-12-15 15:11:15 -06:00
Jason Novinger
4cf0a1045c Address PR feedback 2025-12-15 15:11:00 -06:00
Jason Novinger
e81e192d2e Closes #20929: Require render_config permission for UI config rendering
- Modified `ObjectRenderConfigView.has_permission()` to require both view and render_config permissions
- Added `remove_permissions()` test helper to remove permissions from existing ObjectPermission objects
- Added regression tests for Device and VirtualMachine render-config permission enforcement

The `render_config` permission action was introduced in #16681 for API endpoints. This extends PR_7604_description
to the UI render-config tabs, preventing users from viewing rendered configurations without explicit permission.
2025-12-12 13:16:28 -06:00
5 changed files with 58 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ from core.models import ObjectType
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.models import ConfigTemplate
from ipam.models import ASN, RIR, VLAN, VRF from ipam.models import ASN, RIR, VLAN, VRF
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
from tenancy.models import Tenant from tenancy.models import Tenant
@@ -2339,6 +2340,28 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
url = reverse('dcim:device_inventory', kwargs={'pk': device.pk}) url = reverse('dcim:device_inventory', kwargs={'pk': device.pk})
self.assertHttpStatus(self.client.get(url), 200) self.assertHttpStatus(self.client.get(url), 200)
def test_device_renderconfig(self):
configtemplate = ConfigTemplate.objects.create(
name='Test Config Template',
template_code='Config for device {{ device.name }}'
)
device = Device.objects.first()
device.config_template = configtemplate
device.save()
url = reverse('dcim:device_render-config', kwargs={'pk': device.pk})
# User with only view permission should NOT be able to render config
self.add_permissions('dcim.view_device')
self.assertHttpStatus(self.client.get(url), 403)
# With render_config permission added should be able to render config
self.add_permissions('dcim.render_config_device')
self.assertHttpStatus(self.client.get(url), 200)
# With view permission removed should NOT be able to render config
self.remove_permissions('dcim.view_device')
self.assertHttpStatus(self.client.get(url), 403)
class ModuleTestCase( class ModuleTestCase(
# Module does not support bulk renaming (no name field) or # Module does not support bulk renaming (no name field) or

View File

@@ -2682,6 +2682,7 @@ class DeviceConfigContextView(ObjectConfigContextView):
class DeviceRenderConfigView(ObjectRenderConfigView): class DeviceRenderConfigView(ObjectRenderConfigView):
queryset = Device.objects.all() queryset = Device.objects.all()
base_template = 'dcim/device/base.html' base_template = 'dcim/device/base.html'
additional_permissions = ['dcim.render_config_device']
tab = ViewTab( tab = ViewTab(
label=_('Render Config'), label=_('Render Config'),
weight=2100, weight=2100,

View File

@@ -67,6 +67,16 @@ class TestCase(_TestCase):
obj_perm.users.add(self.user) obj_perm.users.add(self.user)
obj_perm.object_types.add(object_type) obj_perm.object_types.add(object_type)
def remove_permissions(self, *names):
"""
Remove a set of permissions from the test user. Accepts permission names in the form <app>.<action>_<model>.
"""
for name in names:
object_type, action = resolve_permission_type(name)
ObjectPermission.objects.filter(
actions__contains=[action], object_types=object_type, users=self.user
).delete()
# #
# Custom assertions # Custom assertions
# #

View File

@@ -4,6 +4,7 @@ from django.urls import reverse
from dcim.choices import InterfaceModeChoices from dcim.choices import InterfaceModeChoices
from dcim.models import DeviceRole, Platform, Site from dcim.models import DeviceRole, Platform, Site
from extras.models import ConfigTemplate
from ipam.models import VLAN, VRF from ipam.models import VLAN, VRF
from utilities.testing import ViewTestCases, create_tags, create_test_device, create_test_virtualmachine from utilities.testing import ViewTestCases, create_tags, create_test_device, create_test_virtualmachine
from virtualization.choices import * from virtualization.choices import *
@@ -326,6 +327,28 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
url = reverse('virtualization:virtualmachine_interfaces', kwargs={'pk': virtualmachine.pk}) url = reverse('virtualization:virtualmachine_interfaces', kwargs={'pk': virtualmachine.pk})
self.assertHttpStatus(self.client.get(url), 200) self.assertHttpStatus(self.client.get(url), 200)
def test_virtualmachine_renderconfig(self):
configtemplate = ConfigTemplate.objects.create(
name='Test Config Template',
template_code='Config for VM {{ virtualmachine.name }}'
)
vm = VirtualMachine.objects.first()
vm.config_template = configtemplate
vm.save()
url = reverse('virtualization:virtualmachine_render-config', kwargs={'pk': vm.pk})
# User with only view permission should NOT be able to render config
self.add_permissions('virtualization.view_virtualmachine')
self.assertHttpStatus(self.client.get(url), 403)
# With render_config permission added should be able to render config
self.add_permissions('virtualization.render_config_virtualmachine')
self.assertHttpStatus(self.client.get(url), 200)
# With view permission removed should NOT be able to render config
self.remove_permissions('virtualization.view_virtualmachine')
self.assertHttpStatus(self.client.get(url), 403)
class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = VMInterface model = VMInterface

View File

@@ -405,6 +405,7 @@ class VirtualMachineConfigContextView(ObjectConfigContextView):
class VirtualMachineRenderConfigView(ObjectRenderConfigView): class VirtualMachineRenderConfigView(ObjectRenderConfigView):
queryset = VirtualMachine.objects.all() queryset = VirtualMachine.objects.all()
base_template = 'virtualization/virtualmachine/base.html' base_template = 'virtualization/virtualmachine/base.html'
additional_permissions = ['virtualization.render_config_virtualmachine']
tab = ViewTab( tab = ViewTab(
label=_('Render Config'), label=_('Render Config'),
weight=2100, weight=2100,