Merge branch 'develop' into feature

This commit is contained in:
jeremystretch 2022-08-08 15:39:03 -04:00
commit 1b88b36820
14 changed files with 53 additions and 37 deletions

View File

@ -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

View File

@ -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
--- ---

View File

@ -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',
] ]

View File

@ -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,

View File

@ -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, f'Unable to remove master device {device} from the virtual chassis.')
messages.error(request, mark_safe(msg))
return redirect(device.get_absolute_url()) return redirect(device.get_absolute_url())
if form.is_valid(): if form.is_valid():

View File

@ -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."
}) })

View File

@ -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 f'<a href="{item.get_absolute_url()}">{escape(item)}</a>'
return item 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))

View File

@ -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'] pattern_count = len(data[f'{self.patterned_fields[0]}_pattern'])
labels = data['label_pattern'] if 'label_pattern' in data else None for i in range(pattern_count):
for i, name in enumerate(names):
label = labels[i] if labels else None
component_data = { component_data = {
self.parent_field: obj.pk, self.parent_field: obj.pk
'name': name,
'label': label
} }
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():

View File

@ -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)) msg = f'{msg} {obj}'
messages.success(request, mark_safe(msg)) messages.success(request, msg)
if '_addanother' in request.POST: if '_addanother' in request.POST:
redirect_url = request.path redirect_url = request.path

View File

@ -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( username = serializer.data.get('username')
request=request, password = serializer.data.get('password')
username=serializer.data['username'], if not username or not password:
password=serializer.data['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")

View File

@ -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:

View File

@ -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()

View File

@ -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(f'<span title="{long_ts}">{short_ts}</span>')
return mark_safe(span)
@register.simple_tag @register.simple_tag

View File

@ -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