diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 2508590d9..b3b99d804 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -627,8 +627,8 @@ class DeviceTypeDeleteView(ObjectDeleteView): default_return_url = 'dcim:devicetype_list' -class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView): - permission_required = [ +class DeviceTypeImportView(ObjectImportView): + additional_permissions = [ 'dcim.add_devicetype', 'dcim.add_consoleporttemplate', 'dcim.add_consoleserverporttemplate', @@ -639,7 +639,7 @@ class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView): 'dcim.add_rearporttemplate', 'dcim.add_devicebaytemplate', ] - model = DeviceType + queryset = DeviceType.objects.all() model_form = forms.DeviceTypeImportForm related_object_forms = OrderedDict(( ('console-ports', forms.ConsolePortTemplateImportForm), diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index e4161077c..e448f2934 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -571,21 +571,29 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): }) -class ObjectImportView(GetReturnURLMixin, View): +class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): """ Import a single object (YAML or JSON format). + + queryset: Base queryset for the objects being created + model_form: The ModelForm used to create individual objects + related_object_forms: A dictionary mapping of forms to be used for the creation of related (child) objects + template_name: The name of the template """ - model = None + queryset = None model_form = None related_object_forms = dict() template_name = 'utilities/obj_import.html' + def get_required_permission(self): + return get_permission_for_model(self.queryset.model, 'add') + def get(self, request): form = ImportForm() return render(request, self.template_name, { 'form': form, - 'obj_type': self.model._meta.verbose_name, + 'obj_type': self.queryset.model._meta.verbose_name, 'return_url': self.get_return_url(request), }) @@ -615,12 +623,17 @@ class ObjectImportView(GetReturnURLMixin, View): # Save the primary object obj = model_form.save() + + # Enforce object-level permissions + self.queryset.get(pk=obj.pk) + logger.debug(f"Created {obj} (PK: {obj.pk})") # Iterate through the related object forms (if any), validating and saving each instance. for field_name, related_object_form in self.related_object_forms.items(): logger.debug("Processing form for related objects: {related_object_form}") + related_obj_pks = [] for i, rel_obj_data in enumerate(data.get(field_name, list())): f = related_object_form(obj, rel_obj_data) @@ -630,7 +643,8 @@ class ObjectImportView(GetReturnURLMixin, View): f.data[subfield_name] = field.initial if f.is_valid(): - f.save() + related_obj = f.save() + related_obj_pks.append(related_obj.pk) else: # Replicate errors on the related object form to the primary form for display for subfield_name, errors in f.errors.items(): @@ -639,9 +653,19 @@ class ObjectImportView(GetReturnURLMixin, View): model_form.add_error(None, err_msg) raise AbortTransaction() + # 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 + except AbortTransaction: pass + except ObjectDoesNotExist: + msg = "Object creation failed due to object-level permissions violation" + logger.debug(msg) + form.add_error(None, msg) + if not model_form.errors: logger.info(f"Import object {obj} (PK: {obj.pk})") messages.success(request, mark_safe('Imported object: {}'.format( @@ -673,7 +697,7 @@ class ObjectImportView(GetReturnURLMixin, View): return render(request, self.template_name, { 'form': form, - 'obj_type': self.model._meta.verbose_name, + 'obj_type': self.queryset.model._meta.verbose_name, 'return_url': self.get_return_url(request), })