Просмотр исходного кода

Fixes #23467: Restrict parent objects in bulk component creation (#23468)

Jeremy Stretch 2 дней назад
Родитель
Сommit
350ac040cc
2 измененных файлов с 61 добавлено и 5 удалено
  1. 51 1
      netbox/dcim/tests/test_views.py
  2. 10 4
      netbox/netbox/views/generic/bulk_views.py

+ 51 - 1
netbox/dcim/tests/test_views.py

@@ -9,6 +9,7 @@ from zoneinfo import ZoneInfo
 
 
 import yaml
 import yaml
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.messages import get_messages
 from django.db import connection
 from django.db import connection
 from django.http import QueryDict, StreamingHttpResponse
 from django.http import QueryDict, StreamingHttpResponse
 from django.test import override_settings, tag
 from django.test import override_settings, tag
@@ -4234,6 +4235,55 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         self.client.post(self._get_url('bulk_delete'), data)
         self.client.post(self._get_url('bulk_delete'), data)
         self.assertEqual(device.interfaces.count(), 4)  # Child & parent were both deleted
         self.assertEqual(device.interfaces.count(), 4)  # Child & parent were both deleted
 
 
+    @tag('regression')  # Issue #23467
+    def test_bulk_add_interfaces_excludes_restricted_devices(self):
+        """
+        Bulk-adding interfaces ignores devices the user is not permitted to view.
+        """
+        visible_device = Device.objects.get(name='Device 1')
+        site = Site.objects.create(name='Site 2', slug='site-2')
+        hidden_device = create_test_device('Hidden Device', site=site)
+
+        obj_perm = ObjectPermission(
+            name='Test permission',
+            actions=['view'],
+            constraints={'site__slug': 'site-1'},
+        )
+        obj_perm.save()
+        obj_perm.users.add(self.user)
+        obj_perm.object_types.add(ObjectType.objects.get_for_model(Device))
+        self.add_permissions('dcim.add_interface')
+        url = reverse('dcim:device_bulk_add_interface')
+
+        # The selected objects table omits the hidden device
+        response = self.client.post(url, data=post_data({'pk': [visible_device.pk, hidden_device.pk]}))
+        self.assertHttpStatus(response, 200)
+        self.assertEqual([row.record for row in response.context['table'].rows], [visible_device])
+        self.assertNotContains(response, hidden_device.name)
+        self.assertNotIn(hidden_device, response.context['form'].fields['pk'].queryset)
+        self.assertEqual(response.context['form']['pk'].value(), [visible_device.pk])
+
+        # Selecting all filtered devices matches only permitted devices
+        response = self.client.post(f'{url}?site_id={site.pk}', data=post_data({'_all': True}))
+        self.assertHttpStatus(response, 302)
+        self.assertEqual(
+            [str(message) for message in get_messages(response.wsgi_request)],
+            ['No devices were selected.']
+        )
+
+        # Creation skips the hidden device
+        response = self.client.post(url, data=post_data({
+            'pk': [visible_device.pk, hidden_device.pk],
+            'name': 'Bulk Interface',
+            'type': InterfaceTypeChoices.TYPE_1GE_FIXED,
+            '_create': True,
+        }))
+        self.assertHttpStatus(response, 302)
+        self.assertEqual(
+            list(Interface.objects.filter(name='Bulk Interface').values_list('device', flat=True)),
+            [visible_device.pk]
+        )
+
     def test_rename_select_all_spans_pages(self):
     def test_rename_select_all_spans_pages(self):
         """
         """
         Tests the bulk rename functionality for interfaces spanning multiple pages in the UI.
         Tests the bulk rename functionality for interfaces spanning multiple pages in the UI.
@@ -4724,7 +4774,7 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
         """
         """
         Bulk-adding module bays expands the name pattern per device and applies enabled to every new bay.
         Bulk-adding module bays expands the name pattern per device and applies enabled to every new bay.
         """
         """
-        self.add_permissions('dcim.add_modulebay')
+        self.add_permissions('dcim.add_modulebay', 'dcim.view_device')
         device1 = Device.objects.get(name='Device 1')
         device1 = Device.objects.get(name='Device 1')
         device2 = create_test_device('Device 2')
         device2 = create_test_device('Device 2')
         initial_count = self._get_queryset().count()
         initial_count = self._get_queryset().count()

+ 10 - 4
netbox/netbox/views/generic/bulk_views.py

@@ -1334,15 +1334,16 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
         logger = logging.getLogger('netbox.views.BulkComponentCreateView')
         logger = logging.getLogger('netbox.views.BulkComponentCreateView')
         parent_model_name = self.parent_model._meta.verbose_name_plural
         parent_model_name = self.parent_model._meta.verbose_name_plural
         model_name = self.queryset.model._meta.verbose_name_plural
         model_name = self.queryset.model._meta.verbose_name_plural
+        parent_queryset = self.parent_model.objects.restrict(request.user, 'view')
 
 
         # Are we editing *all* objects in the queryset or just a selected subset?
         # Are we editing *all* objects in the queryset or just a selected subset?
         if request.POST.get('_all') and self.filterset is not None:
         if request.POST.get('_all') and self.filterset is not None:
-            queryset = self.filterset(request.GET, self.parent_model.objects.only('pk'), request=request).qs
+            queryset = self.filterset(request.GET, parent_queryset.only('pk'), request=request).qs
             pk_list = [obj.pk for obj in queryset]
             pk_list = [obj.pk for obj in queryset]
         else:
         else:
             pk_list = [int(pk) for pk in request.POST.getlist('pk')]
             pk_list = [int(pk) for pk in request.POST.getlist('pk')]
 
 
-        selected_objects = self.parent_model.objects.filter(pk__in=pk_list)
+        selected_objects = parent_queryset.filter(pk__in=pk_list)
         if not selected_objects:
         if not selected_objects:
             messages.warning(
             messages.warning(
                 request,
                 request,
@@ -1352,7 +1353,11 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
         table = self.table(selected_objects, orderable=False)
         table = self.table(selected_objects, orderable=False)
 
 
         if '_create' in request.POST:
         if '_create' in request.POST:
-            form = self.form(request.POST)
+            # Drop any selected parent objects which the user is not permitted to view
+            post_data = request.POST.copy()
+            post_data.setlist('pk', [obj.pk for obj in selected_objects])
+            form = self.form(post_data)
+            restrict_form_fields(form, request.user)
 
 
             if form.is_valid():
             if form.is_valid():
                 logger.debug("Form validation was successful")
                 logger.debug("Form validation was successful")
@@ -1445,7 +1450,8 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
                 logger.debug("Form validation failed")
                 logger.debug("Form validation failed")
 
 
         else:
         else:
-            form = self.form(initial={'pk': pk_list})
+            form = self.form(initial={'pk': [obj.pk for obj in selected_objects]})
+            restrict_form_fields(form, request.user)
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'form': form,
             'form': form,