2
0
Эх сурвалжийг харах

Merge branch 'develop' into feature

jeremystretch 3 жил өмнө
parent
commit
1b88b36820

+ 1 - 1
base_requirements.txt

@@ -4,7 +4,7 @@ bleach
 
 
 # The Python web framework on which NetBox is built
 # The Python web framework on which NetBox is built
 # https://github.com/django/django
 # https://github.com/django/django
-Django
+Django<4.1
 
 
 # Django middleware which permits cross-domain API requests
 # Django middleware which permits cross-domain API requests
 # https://github.com/OttoYiu/django-cors-headers
 # https://github.com/OttoYiu/django-cors-headers

+ 12 - 1
docs/release-notes/version-3.2.md

@@ -1,6 +1,10 @@
 # NetBox v3.2
 # NetBox v3.2
 
 
-## v3.2.8 (FUTURE)
+## v3.2.9 (FUTURE)
+
+---
+
+## v3.2.8 (2022-08-08)
 
 
 ### Enhancements
 ### Enhancements
 
 
@@ -11,13 +15,20 @@
 * [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
 * [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
 * [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
 * [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
 * [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
 * [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
+* [#9906](https://github.com/netbox-community/netbox/issues/9906) - Include `color` attribute in front & rear port YAML import/export
 
 
 ### Bug Fixes
 ### Bug Fixes
 
 
+* [#9827](https://github.com/netbox-community/netbox/issues/9827) - Fix assignment of module bay position during bulk creation
 * [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
 * [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
 * [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
 * [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
 * [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
 * [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
 * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
 * [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
+* [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables
+* [#9948](https://github.com/netbox-community/netbox/issues/9948) - Fix TypeError exception when requesting API tokens list as non-authenticated user
+* [#9949](https://github.com/netbox-community/netbox/issues/9949) - Fix KeyError exception resulting from invalid API token provisioning request
+* [#9950](https://github.com/netbox-community/netbox/issues/9950) - Prevent redirection to arbitrary URLs via `next` parameter on login URL
+* [#9952](https://github.com/netbox-community/netbox/issues/9952) - Prevent InvalidMove when attempting to assign a nested child object as parent
 
 
 ---
 ---
 
 

+ 2 - 2
netbox/dcim/forms/object_import.py

@@ -156,7 +156,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
     class Meta:
     class Meta:
         model = FrontPortTemplate
         model = FrontPortTemplate
         fields = [
         fields = [
-            'device_type', 'module_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
+            'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label', 'description',
         ]
         ]
 
 
 
 
@@ -168,7 +168,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
     class Meta:
     class Meta:
         model = RearPortTemplate
         model = RearPortTemplate
         fields = [
         fields = [
-            'device_type', 'module_type', 'name', 'type', 'positions', 'label', 'description',
+            'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
         ]
         ]
 
 
 
 

+ 2 - 0
netbox/dcim/models/device_component_templates.py

@@ -478,6 +478,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
         return {
         return {
             'name': self.name,
             'name': self.name,
             'type': self.type,
             'type': self.type,
+            'color': self.color,
             'rear_port': self.rear_port.name,
             'rear_port': self.rear_port.name,
             'rear_port_position': self.rear_port_position,
             'rear_port_position': self.rear_port_position,
             'label': self.label,
             'label': self.label,
@@ -527,6 +528,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
         return {
         return {
             'name': self.name,
             'name': self.name,
             'type': self.type,
             'type': self.type,
+            'color': self.color,
             'positions': self.positions,
             'positions': self.positions,
             'label': self.label,
             'label': self.label,
             'description': self.description,
             'description': self.description,

+ 3 - 3
netbox/dcim/views.py

@@ -2721,6 +2721,7 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
     filterset = filtersets.DeviceFilterSet
     filterset = filtersets.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
+    patterned_fields = ('name', 'label', 'position')
 
 
 
 
 class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
 class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
@@ -3066,7 +3067,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
             if membership_form.is_valid():
             if membership_form.is_valid():
 
 
                 membership_form.save()
                 membership_form.save()
-                msg = 'Added member <a href="{}">{}</a>'.format(device.get_absolute_url(), escape(device))
+                msg = f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
                 messages.success(request, mark_safe(msg))
                 messages.success(request, mark_safe(msg))
 
 
                 if '_addanother' in request.POST:
                 if '_addanother' in request.POST:
@@ -3111,8 +3112,7 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
         # Protect master device from being removed
         # Protect master device from being removed
         virtual_chassis = VirtualChassis.objects.filter(master=device).first()
         virtual_chassis = VirtualChassis.objects.filter(master=device).first()
         if virtual_chassis is not None:
         if virtual_chassis is not None:
-            msg = 'Unable to remove master device {} from the virtual chassis.'.format(escape(device))
-            messages.error(request, mark_safe(msg))
+            messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
             return redirect(device.get_absolute_url())
             return redirect(device.get_absolute_url())
 
 
         if form.is_valid():
         if form.is_valid():

+ 2 - 2
netbox/netbox/models/__init__.py

@@ -109,9 +109,9 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
         super().clean()
         super().clean()
 
 
         # An MPTT model cannot be its own parent
         # An MPTT model cannot be its own parent
-        if self.pk and self.parent_id == self.pk:
+        if self.pk and self.parent and self.parent in self.get_descendants(include_self=True):
             raise ValidationError({
             raise ValidationError({
-                "parent": "Cannot assign self as parent."
+                "parent": f"Cannot assign self or child {self._meta.verbose_name} as parent."
             })
             })
 
 
 
 

+ 6 - 5
netbox/netbox/tables/columns.py

@@ -7,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser
 from django.db.models import DateField, DateTimeField
 from django.db.models import DateField, DateTimeField
 from django.template import Context, Template
 from django.template import Context, Template
 from django.urls import reverse
 from django.urls import reverse
+from django.utils.html import escape
 from django.utils.formats import date_format
 from django.utils.formats import date_format
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from django_tables2.columns import library
 from django_tables2.columns import library
@@ -428,8 +429,8 @@ class CustomFieldColumn(tables.Column):
     @staticmethod
     @staticmethod
     def _likify_item(item):
     def _likify_item(item):
         if hasattr(item, 'get_absolute_url'):
         if hasattr(item, 'get_absolute_url'):
-            return f'<a href="{item.get_absolute_url()}">{item}</a>'
-        return item
+            return f'<a href="{item.get_absolute_url()}">{escape(item)}</a>'
+        return escape(item)
 
 
     def render(self, value):
     def render(self, value):
         if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
@@ -437,13 +438,13 @@ class CustomFieldColumn(tables.Column):
         if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
             return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
             return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
         if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
-            return mark_safe(f'<a href="{value}">{value}</a>')
+            return mark_safe(f'<a href="{escape(value)}">{escape(value)}</a>')
         if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
             return ', '.join(v for v in value)
             return ', '.join(v for v in value)
         if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
         if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
-            return mark_safe(', '.join([
+            return mark_safe(', '.join(
                 self._likify_item(obj) for obj in self.customfield.deserialize(value)
                 self._likify_item(obj) for obj in self.customfield.deserialize(value)
-            ]))
+            ))
         if value is not None:
         if value is not None:
             obj = self.customfield.deserialize(value)
             obj = self.customfield.deserialize(value)
             return mark_safe(self._likify_item(obj))
             return mark_safe(self._likify_item(obj))

+ 9 - 8
netbox/netbox/views/generic/bulk_views.py

@@ -770,6 +770,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
     model_form = None
     model_form = None
     filterset = None
     filterset = None
     table = None
     table = None
+    patterned_fields = ('name', 'label')
 
 
     def get_required_permission(self):
     def get_required_permission(self):
         return f'dcim.add_{self.queryset.model._meta.model_name}'
         return f'dcim.add_{self.queryset.model._meta.model_name}'
@@ -805,16 +806,16 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
 
 
                         for obj in data['pk']:
                         for obj in data['pk']:
 
 
-                            names = data['name_pattern']
-                            labels = data['label_pattern'] if 'label_pattern' in data else None
-                            for i, name in enumerate(names):
-                                label = labels[i] if labels else None
-
+                            pattern_count = len(data[f'{self.patterned_fields[0]}_pattern'])
+                            for i in range(pattern_count):
                                 component_data = {
                                 component_data = {
-                                    self.parent_field: obj.pk,
-                                    'name': name,
-                                    'label': label
+                                    self.parent_field: obj.pk
                                 }
                                 }
+
+                                for field_name in self.patterned_fields:
+                                    if data.get(f'{field_name}_pattern'):
+                                        component_data[field_name] = data[f'{field_name}_pattern'][i]
+
                                 component_data.update(data)
                                 component_data.update(data)
                                 component_form = self.model_form(component_data)
                                 component_form = self.model_form(component_data)
                                 if component_form.is_valid():
                                 if component_form.is_valid():

+ 3 - 3
netbox/netbox/views/generic/object_views.py

@@ -389,10 +389,10 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
                 )
                 )
                 logger.info(f"{msg} {obj} (PK: {obj.pk})")
                 logger.info(f"{msg} {obj} (PK: {obj.pk})")
                 if hasattr(obj, 'get_absolute_url'):
                 if hasattr(obj, 'get_absolute_url'):
-                    msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
+                    msg = mark_safe(f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>')
                 else:
                 else:
-                    msg = '{} {}'.format(msg, escape(obj))
-                messages.success(request, mark_safe(msg))
+                    msg = f'{msg} {obj}'
+                messages.success(request, msg)
 
 
                 if '_addanother' in request.POST:
                 if '_addanother' in request.POST:
                     redirect_url = request.path
                     redirect_url = request.path

+ 7 - 5
netbox/users/api/views.py

@@ -58,6 +58,8 @@ class TokenViewSet(NetBoxModelViewSet):
         # Workaround for schema generation (drf_yasg)
         # Workaround for schema generation (drf_yasg)
         if getattr(self, 'swagger_fake_view', False):
         if getattr(self, 'swagger_fake_view', False):
             return queryset.none()
             return queryset.none()
+        if not self.request.user.is_authenticated:
+            return queryset.none()
         if self.request.user.is_superuser:
         if self.request.user.is_superuser:
             return queryset
             return queryset
         return queryset.filter(user=self.request.user)
         return queryset.filter(user=self.request.user)
@@ -74,11 +76,11 @@ class TokenProvisionView(APIView):
         serializer.is_valid()
         serializer.is_valid()
 
 
         # Authenticate the user account based on the provided credentials
         # Authenticate the user account based on the provided credentials
-        user = authenticate(
-            request=request,
-            username=serializer.data['username'],
-            password=serializer.data['password']
-        )
+        username = serializer.data.get('username')
+        password = serializer.data.get('password')
+        if not username or not password:
+            raise AuthenticationFailed("Username and password must be provided to provision a token.")
+        user = authenticate(request=request, username=username, password=password)
         if user is None:
         if user is None:
             raise AuthenticationFailed("Invalid username/password")
             raise AuthenticationFailed("Invalid username/password")
 
 

+ 2 - 1
netbox/users/views.py

@@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.decorators import method_decorator
 from django.utils.decorators import method_decorator
+from django.utils.http import url_has_allowed_host_and_scheme
 from django.views.decorators.debug import sensitive_post_parameters
 from django.views.decorators.debug import sensitive_post_parameters
 from django.views.generic import View
 from django.views.generic import View
 from social_core.backends.utils import load_backends
 from social_core.backends.utils import load_backends
@@ -92,7 +93,7 @@ class LoginView(View):
         data = request.POST if request.method == "POST" else request.GET
         data = request.POST if request.method == "POST" else request.GET
         redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
         redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
 
 
-        if redirect_url and redirect_url.startswith('/'):
+        if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
             logger.debug(f"Redirecting user to {redirect_url}")
             logger.debug(f"Redirecting user to {redirect_url}")
         else:
         else:
             if redirect_url:
             if redirect_url:

+ 2 - 2
netbox/utilities/templatetags/builtins/filters.py

@@ -86,8 +86,8 @@ def placeholder(value):
     """
     """
     if value not in ('', None):
     if value not in ('', None):
         return value
         return value
-    placeholder = '<span class="text-muted">&mdash;</span>'
-    return mark_safe(placeholder)
+
+    return mark_safe('<span class="text-muted">&mdash;</span>')
 
 
 
 
 @register.filter()
 @register.filter()

+ 1 - 3
netbox/utilities/templatetags/helpers.py

@@ -109,9 +109,7 @@ def annotated_date(date_value):
         long_ts = date(date_value, 'DATETIME_FORMAT')
         long_ts = date(date_value, 'DATETIME_FORMAT')
         short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
         short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
 
 
-    span = f'<span title="{long_ts}">{short_ts}</span>'
-
-    return mark_safe(span)
+    return mark_safe(f'<span title="{long_ts}">{short_ts}</span>')
 
 
 
 
 @register.simple_tag
 @register.simple_tag

+ 1 - 1
requirements.txt

@@ -26,7 +26,7 @@ netaddr==0.8.0
 Pillow==9.2.0
 Pillow==9.2.0
 psycopg2-binary==2.9.3
 psycopg2-binary==2.9.3
 PyYAML==6.0
 PyYAML==6.0
-sentry-sdk==1.9.0
+sentry-sdk==1.9.2
 social-auth-app-django==5.0.0
 social-auth-app-django==5.0.0
 social-auth-core==4.3.0
 social-auth-core==4.3.0
 svgwrite==1.4.3
 svgwrite==1.4.3