diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 54f4f7eae..a0af66c42 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.4.7 + placeholder: v3.4.8 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index c9bc56ffc..a26ee7bc1 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.4.7 + placeholder: v3.4.8 validations: required: true - type: dropdown diff --git a/base_requirements.txt b/base_requirements.txt index 02de205fa..cf81a3350 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -1,5 +1,5 @@ # HTML sanitizer -# https://github.com/mozilla/bleach +# https://github.com/mozilla/bleach/blob/main/CHANGES bleach<6.0 # Python client for Amazon AWS API @@ -7,55 +7,55 @@ bleach<6.0 boto3 # The Python web framework on which NetBox is built -# https://github.com/django/django +# https://docs.djangoproject.com/en/stable/releases/ Django<4.2 # Django middleware which permits cross-domain API requests -# https://github.com/OttoYiu/django-cors-headers +# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst django-cors-headers # Runtime UI tool for debugging Django -# https://github.com/jazzband/django-debug-toolbar +# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst django-debug-toolbar # Library for writing reusable URL query filters -# https://github.com/carltongibson/django-filter +# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst django-filter # Django debug toolbar extension with support for GraphiQL -# https://github.com/flavors/django-graphiql-debug-toolbar/ +# https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst django-graphiql-debug-toolbar # Modified Preorder Tree Traversal (recursive nesting of objects) -# https://github.com/django-mptt/django-mptt +# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst django-mptt # Context managers for PostgreSQL advisory locks -# https://github.com/Xof/django-pglocks +# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt django-pglocks # Prometheus metrics library for Django -# https://github.com/korfuri/django-prometheus +# https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md django-prometheus # Django caching backend using Redis -# https://github.com/jazzband/django-redis +# https://github.com/jazzband/django-redis/blob/master/CHANGELOG.rst django-redis # Django extensions for Rich (terminal text rendering) -# https://github.com/adamchainz/django-rich +# https://github.com/adamchainz/django-rich/blob/main/CHANGELOG.rst django-rich # Django integration for RQ (Reqis queuing) -# https://github.com/rq/django-rq +# https://github.com/rq/django-rq/blob/master/CHANGELOG.md django-rq # Abstraction models for rendering and paginating HTML tables -# https://github.com/jieter/django-tables2 +# https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md django-tables2 # User-defined tags for objects -# https://github.com/alex/django-taggit +# https://github.com/jazzband/django-taggit/blob/master/CHANGELOG.rst django-taggit # A Django field for representing time zones @@ -63,11 +63,11 @@ django-taggit django-timezone-field # A REST API framework for Django projects -# https://github.com/encode/django-rest-framework +# https://www.django-rest-framework.org/community/release-notes/ djangorestframework # Sane and flexible OpenAPI 3 schema generation for Django REST framework. -# https://github.com/tfranzel/drf-spectacular +# https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst drf-spectacular # Serve self-contained distribution builds of Swagger UI and Redoc with Django. @@ -75,23 +75,23 @@ drf-spectacular drf-spectacular-sidecar # RSS feed parser -# https://github.com/kurtmckee/feedparser +# https://github.com/kurtmckee/feedparser/blob/develop/CHANGELOG.rst feedparser # Django wrapper for Graphene (GraphQL support) -# https://github.com/graphql-python/graphene-django +# https://github.com/graphql-python/graphene-django/releases graphene_django # WSGI HTTP server -# https://gunicorn.org/ +# https://docs.gunicorn.org/en/latest/news.html gunicorn # Platform-agnostic template rendering engine -# https://github.com/pallets/jinja +# https://jinja.palletsprojects.com/changes/ Jinja2 # Simple markup language for rendering HTML -# https://github.com/Python-Markdown/markdown +# https://python-markdown.github.io/change_log/ # mkdocs currently requires Markdown v3.3 Markdown<3.4 @@ -100,50 +100,50 @@ Markdown<3.4 markdown-include # MkDocs Material theme (for documentation build) -# https://github.com/squidfunk/mkdocs-material +# https://squidfunk.github.io/mkdocs-material/changelog/ mkdocs-material # Introspection for embedded code -# https://github.com/mkdocstrings/mkdocstrings +# https://github.com/mkdocstrings/mkdocstrings/blob/master/CHANGELOG.md mkdocstrings[python-legacy] # Library for manipulating IP prefixes and addresses -# https://github.com/netaddr/netaddr +# https://github.com/netaddr/netaddr/blob/master/CHANGELOG netaddr # Fork of PIL (Python Imaging Library) for image processing -# https://github.com/python-pillow/Pillow +# https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst Pillow # PostgreSQL database adapter for Python -# https://github.com/psycopg/psycopg2 +# https://www.psycopg.org/docs/news.html psycopg2-binary # YAML rendering library -# https://github.com/yaml/pyyaml +# https://github.com/yaml/pyyaml/blob/master/CHANGES PyYAML # Sentry SDK -# https://github.com/getsentry/sentry-python +# https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md sentry-sdk # Social authentication framework -# https://github.com/python-social-auth/social-core +# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md social-auth-core # Django app for social-auth-core -# https://github.com/python-social-auth/social-app-django +# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md # See https://github.com/python-social-auth/social-app-django/issues/429 social-auth-app-django==5.0.0 # SVG image rendering (used for rack elevations) -# https://github.com/mozman/svgwrite +# hhttps://github.com/mozman/svgwrite/blob/master/NEWS.rst svgwrite # Tabular dataset library (for table-based exports) -# https://github.com/jazzband/tablib +# https://github.com/jazzband/tablib/blob/master/HISTORY.md tablib # Timezone data (required by django-timezone-field on Python 3.9+) -# https://github.com/python/tzdata +# https://github.com/python/tzdata/blob/master/NEWS.md tzdata diff --git a/docs/configuration/development.md b/docs/configuration/development.md index 3af56b0e3..1579f2cdb 100644 --- a/docs/configuration/development.md +++ b/docs/configuration/development.md @@ -18,4 +18,4 @@ interface. Default: False -This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Set this to `True` **only** if you are actively developing the NetBox code base. +This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Additionally, enabling this setting disables the debug warning banner in the UI. Set this to `True` **only** if you are actively developing the NetBox code base. diff --git a/docs/customization/reports.md b/docs/customization/reports.md index 9db436961..fab12bf7d 100644 --- a/docs/customization/reports.md +++ b/docs/customization/reports.md @@ -130,7 +130,7 @@ Once you have created a report, it will appear in the reports list. Initially, r !!! note To run a report, a user must be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below. - ![Adding the run action to a permission](/media/admin_ui_run_permission.png) + ![Adding the run action to a permission](../media/admin_ui_run_permission.png) ### Via the Web UI diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md index 501c69aa3..f52a92c52 100644 --- a/docs/integrations/rest-api.md +++ b/docs/integrations/rest-api.md @@ -586,6 +586,15 @@ Additionally, a token can be set to expire at a specific time. This can be usefu Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.) +#### Creating Tokens for Other Users + +It is possible to provision authentication tokens for other users via the REST API. To do, so the requesting user must have the `users.grant_token` permission assigned. While all users have inherent permission to create their own tokens, this permission is required to enable the creation of tokens for other users. + +![Adding the grant action to a permission](../media/admin_ui_grant_permission.png) + +!!! warning "Exercise Caution" + The ability to create tokens on behalf of other users enables the requestor to access the created token. This ability is intended e.g. for the provisioning of tokens by automated services, and should be used with extreme caution to avoid a security compromise. + ### Authenticating to the API An authentication token is attached to a request by setting the `Authorization` header to the string `Token` followed by a space and the user's token: diff --git a/docs/media/admin_ui_grant_permission.png b/docs/media/admin_ui_grant_permission.png new file mode 100644 index 000000000..2b82dcca2 Binary files /dev/null and b/docs/media/admin_ui_grant_permission.png differ diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 5f086bca4..9b470481a 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -1,16 +1,30 @@ # NetBox v3.4 -## v3.4.8 (FUTURE) +## v3.4.9 (FUTURE) + +--- + +## v3.4.8 (2023-04-12) ### Enhancements +* [#10414](https://github.com/netbox-community/netbox/issues/10414) - Enable general purpose image attachments for device types +* [#10600](https://github.com/netbox-community/netbox/issues/10600) - Allow custom object fields to reference a user or group +* [#11015](https://github.com/netbox-community/netbox/issues/11015) - Remove unit from commit rate column header in circuits table +* [#11431](https://github.com/netbox-community/netbox/issues/11431) - Disallow changing custom field type after creation +* [#11453](https://github.com/netbox-community/netbox/issues/11453) - Display a warning banner when `DEBUG` is enabled * [#12007](https://github.com/netbox-community/netbox/issues/12007) - Enable filtering of VM Interfaces by assigned VLAN * [#12095](https://github.com/netbox-community/netbox/issues/12095) - Specify UTF-8 encoding for default export template MIME type +* [#12207](https://github.com/netbox-community/netbox/issues/12207) - Introduce the `grant_token` permission for controlling the creation of API tokens on behalf of other users ### Bug Fixes +* [#10221](https://github.com/netbox-community/netbox/issues/10221) - Validate generic foreign key relations assigned via REST API requests +* [#11432](https://github.com/netbox-community/netbox/issues/11432) - Prevent existing components & component templates from being reassigned to different devices/device types via the REST API +* [#11454](https://github.com/netbox-community/netbox/issues/11454) - Raise validation error if generic foreign key assignment does not specify both object type and ID * [#11746](https://github.com/netbox-community/netbox/issues/11746) - Fix cleanup of object data when deleting a custom field * [#12011](https://github.com/netbox-community/netbox/issues/12011) - Fix KeyError exception when attempting to add module bays in bulk +* [#12040](https://github.com/netbox-community/netbox/issues/12040) - Display relevant UI tab upon bulk import validation failure * [#12074](https://github.com/netbox-community/netbox/issues/12074) - Fix the automatic assignment of racks to devices via the REST API * [#12084](https://github.com/netbox-community/netbox/issues/12084) - Fix exception when attempting to create a saved filter for applied filters * [#12087](https://github.com/netbox-community/netbox/issues/12087) - Fix bulk editing of many-to-many relationships @@ -18,6 +32,7 @@ * [#12118](https://github.com/netbox-community/netbox/issues/12118) - Fix instantiation of nested inventory item templates when creating a device * [#12184](https://github.com/netbox-community/netbox/issues/12184) - Fix filtered bulk deletion for various models * [#12190](https://github.com/netbox-community/netbox/issues/12190) - Fix form layout for plugin textarea fields +* [#12227](https://github.com/netbox-community/netbox/issues/12227) - Fix tenant assignment on bulk import of L2VPNs --- diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index e8bdf6a92..7b1accdc3 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -64,7 +64,9 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): template_code=CIRCUITTERMINATION_LINK, verbose_name='Side Z' ) - commit_rate = CommitRateColumn() + commit_rate = CommitRateColumn( + verbose_name='Commit Rate' + ) comments = columns.MarkdownColumn() tags = columns.TagColumn( url_name='circuits:circuit_list' diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 5d4d3a933..5f5804a92 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1694,12 +1694,14 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): field_name='terminations__termination_type' ) termination_a_id = MultiValueNumberFilter( + method='filter_by_cable_end_a', field_name='terminations__termination_id' ) termination_b_type = ContentTypeFilter( field_name='terminations__termination_type' ) termination_b_id = MultiValueNumberFilter( + method='filter_by_cable_end_b', field_name='terminations__termination_id' ) type = django_filters.MultipleChoiceFilter( @@ -1757,6 +1759,18 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet): # Supported objects: device, rack, location, site return queryset.filter(**{f'terminations___{name}__in': value}).distinct() + def filter_by_cable_end(self, queryset, name, value, side): + # Filter by termination id and cable_end type + return queryset.filter(**{f'{name}__in': value, 'terminations__cable_end': side}).distinct() + + def filter_by_cable_end_a(self, queryset, name, value): + # Filter by termination id and cable_end type + return self.filter_by_cable_end(queryset, name, value, CableEndChoices.SIDE_A) + + def filter_by_cable_end_b(self, queryset, name, value): + # Filter by termination id and cable_end type + return self.filter_by_cable_end(queryset, name, value, CableEndChoices.SIDE_B) + class CableTerminationFilterSet(BaseFilterSet): termination_type = ContentTypeFilter() diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 3da081c06..f0ca6f8b5 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -119,6 +119,12 @@ class ModularComponentTemplateModel(ComponentTemplateModel): ), ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Cache the original DeviceType ID for reference under clean() + self._original_device_type = self.device_type_id + def to_objectchange(self, action): objectchange = super().to_objectchange(action) if self.device_type is not None: @@ -130,6 +136,11 @@ class ModularComponentTemplateModel(ComponentTemplateModel): def clean(self): super().clean() + if self.pk is not None and self._original_device_type != self.device_type_id: + raise ValidationError({ + "device_type": "Component templates cannot be moved to a different device type." + }) + # A component template must belong to a DeviceType *or* to a ModuleType if self.device_type and self.module_type: raise ValidationError( diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index b497a90d2..ff6fdbfed 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -78,6 +78,12 @@ class ComponentModel(NetBoxModel): ), ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Cache the original Device ID for reference under clean() + self._original_device = self.device_id + def __str__(self): if self.label: return f"{self.name} ({self.label})" @@ -88,6 +94,14 @@ class ComponentModel(NetBoxModel): objectchange.related_object = self.device return objectchange + def clean(self): + super().clean() + + if self.pk is not None and self._original_device != self.device_id: + raise ValidationError({ + "device": "Components cannot be moved to a different device." + }) + @property def parent_object(self): return self.device diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index e0eceea1f..02c68c10a 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -128,6 +128,10 @@ class DeviceType(PrimaryModel, WeightMixin): blank=True ) + images = GenericRelation( + to='extras.ImageAttachment' + ) + clone_fields = ( 'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit' ) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 4054f3bd4..6391b294d 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -104,6 +104,12 @@ class CustomFieldSerializer(ValidatedModelSerializer): 'last_updated', ] + def validate_type(self, value): + if self.instance and self.instance.type != value: + raise serializers.ValidationError('Changing the type of custom fields is not supported.') + + return value + @extend_schema_field(OpenApiTypes.STR) def get_data_type(self, obj): types = CustomFieldTypeChoices diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 7ab7a000e..c199d2b53 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -1,6 +1,7 @@ import json from django import forms +from django.db.models import Q from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ @@ -39,7 +40,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): object_type = ContentTypeChoiceField( queryset=ContentType.objects.all(), # TODO: Come up with a canonical way to register suitable models - limit_choices_to=FeatureQuery('webhooks'), + limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']), required=False, help_text=_("Type of the related object (for object/multi-object fields only)") ) @@ -63,6 +64,13 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): ) } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Disable changing the type of a CustomField as it almost universally causes errors if custom field data is already present. + if self.instance.pk: + self.fields['type'].disabled = True + class CustomLinkForm(BootstrapMixin, forms.ModelForm): content_types = ContentTypeMultipleChoiceField( diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 6e5d22531..b59481a36 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -103,6 +103,11 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + update_data = { + 'content_types': ['dcim.device'], + 'name': 'New_Name', + 'description': 'New description', + } @classmethod def setUpTestData(cls): diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 0f9a53ee7..67463ade8 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -456,7 +456,8 @@ class L2VPNImportForm(NetBoxModelImportForm): class Meta: model = L2VPN - fields = ('identifier', 'name', 'slug', 'type', 'description', 'comments', 'tags') + fields = ('identifier', 'name', 'slug', 'tenant', 'type', 'description', + 'comments', 'tags') class L2VPNTerminationImportForm(NetBoxModelImportForm): diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index db8179fdc..fe25bb837 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey from django.core.validators import ValidationError from django.db import models from mptt.models import MPTTModel, TreeForeignKey @@ -58,6 +59,33 @@ class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model): class Meta: abstract = True + def clean(self): + """ + Validate the model for GenericForeignKey fields to ensure that the content type and object ID exist. + """ + super().clean() + + for field in self._meta.get_fields(): + if isinstance(field, GenericForeignKey): + ct_value = getattr(self, field.ct_field) + fk_value = getattr(self, field.fk_field) + + if ct_value is None and fk_value is not None: + raise ValidationError({ + field.ct_field: "This field cannot be null.", + }) + if fk_value is None and ct_value is not None: + raise ValidationError({ + field.fk_field: "This field cannot be null.", + }) + + if ct_value and fk_value: + klass = getattr(self, field.ct_field).model_class() + if not klass.objects.filter(pk=fk_value).exists(): + raise ValidationError({ + field.fk_field: f"Related object not found using the provided value: {fk_value}." + }) + class PrimaryModel(NetBoxModel): """ diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index 2fc6d0d98..6b247d81a 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -70,10 +70,17 @@ Blocks: {% endif %} + {% if settings.DEBUG and not settings.DEVELOPER %} + + {% endif %} + {% if config.MAINTENANCE_MODE %} {% endif %} diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 984898caa..9d1865573 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -99,6 +99,7 @@ {% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/comments.html' %} + {% include 'inc/panels/image_attachments.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/generic/bulk_import.html b/netbox/templates/generic/bulk_import.html index c81923ace..4396585b2 100644 --- a/netbox/templates/generic/bulk_import.html +++ b/netbox/templates/generic/bulk_import.html @@ -15,17 +15,17 @@ Context: {% block tabs %}