mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-09 13:22:18 -06:00
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.
This commit is contained in:
@@ -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,29 @@ 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)
|
||||||
|
|
||||||
|
@tag('regression') # #20929
|
||||||
|
def test_device_renderconfig_permission(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
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from netbox.views.generic.mixins import TableMixin
|
|||||||
from utilities.forms import ConfirmationForm, get_field_value
|
from utilities.forms import ConfirmationForm, get_field_value
|
||||||
from utilities.htmx import htmx_partial, htmx_maybe_redirect_current_page
|
from utilities.htmx import htmx_partial, htmx_maybe_redirect_current_page
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
from utilities.querydict import normalize_querydict
|
from utilities.querydict import normalize_querydict
|
||||||
from utilities.request import copy_safe_request
|
from utilities.request import copy_safe_request
|
||||||
@@ -1082,6 +1083,14 @@ class ObjectRenderConfigView(generic.ObjectView):
|
|||||||
base_template = None
|
base_template = None
|
||||||
template_name = 'extras/object_render_config.html'
|
template_name = 'extras/object_render_config.html'
|
||||||
|
|
||||||
|
def has_permission(self):
|
||||||
|
if super().has_permission(): # enforce base required permission
|
||||||
|
perm = get_permission_for_model(self.queryset.model, 'render_config')
|
||||||
|
if self.request.user.has_perm(perm):
|
||||||
|
self.queryset = self.queryset.restrict(self.request.user, 'render_config')
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
instance = self.get_object(**kwargs)
|
instance = self.get_object(**kwargs)
|
||||||
context = self.get_extra_context(request, instance)
|
context = self.get_extra_context(request, instance)
|
||||||
|
|||||||
@@ -67,6 +67,18 @@ 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)
|
||||||
|
obj_perms = ObjectPermission.objects.filter(
|
||||||
|
actions__contains=[action], object_types=object_type, users=self.user
|
||||||
|
)
|
||||||
|
for obj_perm in obj_perms:
|
||||||
|
obj_perm.users.remove(self.user)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Custom assertions
|
# Custom assertions
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import override_settings
|
from django.test import override_settings, tag
|
||||||
from django.urls import reverse
|
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,29 @@ 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)
|
||||||
|
|
||||||
|
@tag('regression') # #20929
|
||||||
|
def test_virtualmachine_renderconfig_permission(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
|
||||||
|
|||||||
Reference in New Issue
Block a user