瀏覽代碼

Fixes #23611: Require component template permissions only when importing components

Arthur 9 小時之前
父節點
當前提交
ec84434ab1
共有 3 個文件被更改,包括 70 次插入 和 32 次删除
  1. 52 0
      netbox/dcim/tests/test_views.py
  2. 0 28
      netbox/dcim/views.py
  3. 18 4
      netbox/netbox/views/generic/bulk_views.py

+ 52 - 0
netbox/dcim/tests/test_views.py

@@ -1270,6 +1270,58 @@ inventory-items:
         ii1 = InventoryItemTemplate.objects.first()
         ii1 = InventoryItemTemplate.objects.first()
         self.assertEqual(ii1.name, 'Inventory Item 1')
         self.assertEqual(ii1.name, 'Inventory Item 1')
 
 
+    def test_import_objects_without_component_permissions(self):
+        """
+        Importing a device type requires only add_devicetype unless component templates are included.
+        """
+        Manufacturer.objects.create(name='Generic', slug='generic')
+        self.add_permissions('dcim.view_manufacturer', 'dcim.add_devicetype')
+        url = reverse('dcim:devicetype_bulk_import')
+
+        self.assertHttpStatus(self.client.get(url), 200)
+
+        import_data = "manufacturer: Generic\nmodel: TEST-2000\nslug: test-2000\nu_height: 1\n"
+        response = self.client.post(url, data={'data': import_data, 'format': 'yaml'})
+        self.assertHttpStatus(response, 302)
+        self.assertTrue(DeviceType.objects.filter(model='TEST-2000').exists())
+
+        # Including component templates requires the corresponding add permission
+        import_data = (
+            "manufacturer: Generic\nmodel: TEST-3000\nslug: test-3000\nu_height: 1\n"
+            "console-ports:\n  - name: Console Port 1\n    type: de-9\n"
+        )
+        response = self.client.post(url, data={'data': import_data, 'format': 'yaml'})
+        self.assertHttpStatus(response, 200)
+        self.assertContains(response, 'dcim.add_consoleporttemplate')
+        self.assertFalse(DeviceType.objects.filter(model='TEST-3000').exists())
+        self.assertFalse(ConsolePortTemplate.objects.exists())
+
+    def test_import_objects_component_object_permissions(self):
+        """
+        Object-level constraints on component template permissions are enforced during import.
+        """
+        Manufacturer.objects.create(name='Generic', slug='generic')
+        self.add_permissions('dcim.view_manufacturer', 'dcim.add_devicetype')
+        obj_perm = ObjectPermission(name='Test permission', actions=['add'], constraints={'name': 'Console Port 1'})
+        obj_perm.save()
+        obj_perm.users.add(self.user)
+        obj_perm.object_types.add(ObjectType.objects.get_for_model(ConsolePortTemplate))
+        url = reverse('dcim:devicetype_bulk_import')
+
+        import_data = (
+            "manufacturer: Generic\nmodel: TEST-2000\nslug: test-2000\nu_height: 1\n"
+            "console-ports:\n  - name: Console Port 2\n    type: de-9\n"
+        )
+        response = self.client.post(url, data={'data': import_data, 'format': 'yaml'})
+        self.assertHttpStatus(response, 200)
+        self.assertContains(response, 'permissions violation')
+        self.assertFalse(DeviceType.objects.filter(model='TEST-2000').exists())
+
+        import_data = import_data.replace('Console Port 2', 'Console Port 1')
+        response = self.client.post(url, data={'data': import_data, 'format': 'yaml'})
+        self.assertHttpStatus(response, 302)
+        self.assertEqual(DeviceType.objects.get(model='TEST-2000').consoleporttemplates.count(), 1)
+
     def test_bulk_yaml_export_module_bay_types_query_count_is_constant(self):
     def test_bulk_yaml_export_module_bay_types_query_count_is_constant(self):
         """Query count shouldn't scale with bay count -- module_bay_types is prefetched."""
         """Query count shouldn't scale with bay count -- module_bay_types is prefetched."""
         manufacturer = Manufacturer.objects.create(name='Export Query Manufacturer', slug='export-query-mfr')
         manufacturer = Manufacturer.objects.create(name='Export Query Manufacturer', slug='export-query-mfr')

+ 0 - 28
netbox/dcim/views.py

@@ -1678,21 +1678,6 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
 
 
 @register_model_view(DeviceType, 'bulk_import', path='import', detail=False)
 @register_model_view(DeviceType, 'bulk_import', path='import', detail=False)
 class DeviceTypeImportView(generic.BulkImportView):
 class DeviceTypeImportView(generic.BulkImportView):
-    additional_permissions = [
-        'dcim.add_devicetype',
-        'dcim.add_consoleporttemplate',
-        'dcim.add_consoleserverporttemplate',
-        'dcim.add_powerporttemplate',
-        'dcim.add_poweroutlettemplate',
-        'dcim.add_coolingintaketemplate',
-        'dcim.add_coolingoutflowtemplate',
-        'dcim.add_interfacetemplate',
-        'dcim.add_frontporttemplate',
-        'dcim.add_rearporttemplate',
-        'dcim.add_modulebaytemplate',
-        'dcim.add_devicebaytemplate',
-        'dcim.add_inventoryitemtemplate',
-    ]
     queryset = DeviceType.objects.all()
     queryset = DeviceType.objects.all()
     model_form = forms.DeviceTypeImportForm
     model_form = forms.DeviceTypeImportForm
     related_object_forms = {
     related_object_forms = {
@@ -2146,19 +2131,6 @@ class ModuleTypeModuleBaysView(ModuleTypeComponentsView):
 
 
 @register_model_view(ModuleType, 'bulk_import', path='import', detail=False)
 @register_model_view(ModuleType, 'bulk_import', path='import', detail=False)
 class ModuleTypeImportView(generic.BulkImportView):
 class ModuleTypeImportView(generic.BulkImportView):
-    additional_permissions = [
-        'dcim.add_moduletype',
-        'dcim.add_consoleporttemplate',
-        'dcim.add_consoleserverporttemplate',
-        'dcim.add_powerporttemplate',
-        'dcim.add_poweroutlettemplate',
-        'dcim.add_coolingintaketemplate',
-        'dcim.add_coolingoutflowtemplate',
-        'dcim.add_interfacetemplate',
-        'dcim.add_frontporttemplate',
-        'dcim.add_rearporttemplate',
-        'dcim.add_modulebaytemplate',
-    ]
     queryset = ModuleType.objects.all()
     queryset = ModuleType.objects.all()
     model_form = forms.ModuleTypeImportForm
     model_form = forms.ModuleTypeImportForm
     related_object_forms = {
     related_object_forms = {

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

@@ -8,7 +8,7 @@ from types import SimpleNamespace
 from django.conf import settings
 from django.conf import settings
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRel
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRel
-from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured, ObjectDoesNotExist, ValidationError
+from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured, ValidationError
 from django.db import IntegrityError, router, transaction
 from django.db import IntegrityError, router, transaction
 from django.db.models import ManyToManyField, ProtectedError, RestrictedError
 from django.db.models import ManyToManyField, ProtectedError, RestrictedError
 from django.db.models.fields.reverse_related import ManyToManyRel
 from django.db.models.fields.reverse_related import ManyToManyRel
@@ -38,6 +38,7 @@ from utilities.jobs import is_background_request, process_request_as_job
 from utilities.permissions import get_permission_for_model
 from utilities.permissions import get_permission_for_model
 from utilities.query import reapply_model_ordering
 from utilities.query import reapply_model_ordering
 from utilities.querydict import normalize_querydict
 from utilities.querydict import normalize_querydict
+from utilities.querysets import RestrictedQuerySet
 from utilities.request import safe_for_redirect
 from utilities.request import safe_for_redirect
 from utilities.string import title
 from utilities.string import title
 from utilities.tables import get_table_configs
 from utilities.tables import get_table_configs
@@ -540,6 +541,18 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
                     )
                     )
                 )
                 )
 
 
+            # Require permission to create each type of related object being imported
+            model = related_object_form.Meta.model
+            restricted = isinstance(model.objects.all(), RestrictedQuerySet)
+            permission = get_permission_for_model(model, 'add')
+            if related_objects and restricted and not request.user.has_perm(permission):
+                raise ValidationError(
+                    self._compile_form_errors(
+                        {field_name: [_("Permission {permission} is required.").format(permission=permission)]},
+                        index=parent_idx
+                    )
+                )
+
             related_obj_pks = []
             related_obj_pks = []
             for i, rel_obj_data in enumerate(related_objects, start=1):
             for i, rel_obj_data in enumerate(related_objects, start=1):
                 if not isinstance(rel_obj_data, dict):
                 if not isinstance(rel_obj_data, dict):
@@ -567,9 +580,10 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
                     )
                     )
 
 
             # Enforce object-level permissions on related objects
             # Enforce object-level permissions on related objects
-            model = related_object_form.Meta.model
-            if model.objects.filter(pk__in=related_obj_pks).count() != len(related_obj_pks):
-                raise ObjectDoesNotExist
+            if restricted and related_obj_pks:
+                permitted = model.objects.restrict(request.user, 'add').filter(pk__in=related_obj_pks)
+                if permitted.count() != len(related_obj_pks):
+                    raise PermissionsViolation()
 
 
         if is_background_request(request):
         if is_background_request(request):
             request.job.logger.info(f'{_action} {obj}')
             request.job.logger.info(f'{_action} {obj}')