From 7b374e4cf6f6d4611b79a521640097b4988bc2f2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 19 Apr 2023 17:25:32 -0400 Subject: [PATCH 01/18] Fixes #12296: Fix 'mark connected' form field for bulk editing front & rear ports --- docs/release-notes/version-3.4.md | 4 ++++ netbox/dcim/forms/bulk_edit.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 9b470481a..f8a92e6bf 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -2,6 +2,10 @@ ## v3.4.9 (FUTURE) +### Bug Fixes + +* [#12296](https://github.com/netbox-community/netbox/issues/12296) - Fix "mark connected" form field for bulk editing front & rear ports + --- ## v3.4.8 (2023-04-12) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 24bd3e62d..9388f5cd3 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1324,6 +1324,11 @@ class FrontPortBulkEditForm( form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']), ComponentBulkEditForm ): + mark_connected = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect + ) + model = FrontPort fieldsets = ( (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')), @@ -1335,6 +1340,11 @@ class RearPortBulkEditForm( form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']), ComponentBulkEditForm ): + mark_connected = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect + ) + model = RearPort fieldsets = ( (None, ('module', 'type', 'label', 'color', 'description', 'mark_connected')), From 164b2a50163133b36a4a9755e38c658048434fd9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 19 Apr 2023 17:41:38 -0400 Subject: [PATCH 02/18] Fixes #12270: Fix pre-population of list values when creating a saved filter --- docs/release-notes/version-3.4.md | 1 + netbox/utilities/templatetags/helpers.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index f8a92e6bf..60f09c2b5 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#12270](https://github.com/netbox-community/netbox/issues/12270) - Fix pre-population of list values when creating a saved filter * [#12296](https://github.com/netbox-community/netbox/issues/12296) - Fix "mark connected" form field for bulk editing front & rear ports --- diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 6cdf33dd1..471413bf0 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -322,7 +322,7 @@ def applied_filters(context, model, form, query_params): save_link = None if user.has_perm('extras.add_savedfilter') and 'filter_id' not in context['request'].GET: content_type = ContentType.objects.get_for_model(model).pk - parameters = json.dumps(context['request'].GET) + parameters = json.dumps(dict(context['request'].GET.lists())) url = reverse('extras:savedfilter_add') save_link = f"{url}?content_types={content_type}¶meters={quote(parameters)}" From ab3531558aff902b760eadf03eb6e6ddb8492aff Mon Sep 17 00:00:00 2001 From: Luke Anderson Date: Fri, 21 Apr 2023 05:19:54 +0930 Subject: [PATCH 03/18] Closes #12226: Add Profile Data Headers to Remote Authentication Middleware (#12253) * Closes #12226: Add Profile Data Headers to Remote Authentication Middleware * Tweak documentation --------- Co-authored-by: jeremystretch --- .../administration/authentication/overview.md | 2 ++ docs/configuration/remote-authentication.md | 24 +++++++++++++++++++ netbox/netbox/configuration_example.py | 3 +++ netbox/netbox/middleware.py | 12 +++++++++- netbox/netbox/settings.py | 3 +++ netbox/netbox/tests/test_authentication.py | 23 ++++++++++++++++++ 6 files changed, 66 insertions(+), 1 deletion(-) diff --git a/docs/administration/authentication/overview.md b/docs/administration/authentication/overview.md index fca9eab5e..8a8b8f60b 100644 --- a/docs/administration/authentication/overview.md +++ b/docs/administration/authentication/overview.md @@ -26,6 +26,8 @@ REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend' Another option for remote authentication in NetBox is to enable HTTP header-based user assignment. The front end HTTP server (e.g. nginx or Apache) performs client authentication as a process external to NetBox, and passes information about the authenticated user via HTTP headers. By default, the user is assigned via the `REMOTE_USER` header, but this can be customized via the `REMOTE_AUTH_HEADER` configuration parameter. +Optionally, user profile information can be supplied by `REMOTE_USER_FIRST_NAME`, `REMOTE_USER_LAST_NAME` and `REMOTE_USER_EMAIL` headers. These are saved to the users profile during the authentication process. These headers can be customized like the `REMOTE_USER` header. + ### Single Sign-On (SSO) ```python diff --git a/docs/configuration/remote-authentication.md b/docs/configuration/remote-authentication.md index 1fda8d0d3..fd95adef5 100644 --- a/docs/configuration/remote-authentication.md +++ b/docs/configuration/remote-authentication.md @@ -79,6 +79,30 @@ When remote user authentication is in use, this is the name of the HTTP header w --- +## REMOTE_AUTH_USER_EMAIL + +Default: `'HTTP_REMOTE_USER_EMAIL'` + +When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the email address of the currently authenticated user. For example, to use the request header `X-Remote-User-Email` it needs to be set to `HTTP_X_REMOTE_USER_EMAIL`. (Requires `REMOTE_AUTH_ENABLED`.) + +--- + +## REMOTE_AUTH_USER_FIRST_NAME + +Default: `'HTTP_REMOTE_USER_FIRST_NAME'` + +When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the first name of the currently authenticated user. For example, to use the request header `X-Remote-User-First-Name` it needs to be set to `HTTP_X_REMOTE_USER_FIRST_NAME`. (Requires `REMOTE_AUTH_ENABLED`.) + +--- + +## REMOTE_AUTH_USER_LAST_NAME + +Default: `'HTTP_REMOTE_USER_LAST_NAME'` + +When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the last name of the currently authenticated user. For example, to use the request header `X-Remote-User-Last-Name` it needs to be set to `HTTP_X_REMOTE_USER_LAST_NAME`. (Requires `REMOTE_AUTH_ENABLED`.) + +--- + ## REMOTE_AUTH_SUPERUSER_GROUPS Default: `[]` (Empty list) diff --git a/netbox/netbox/configuration_example.py b/netbox/netbox/configuration_example.py index 92f6133a3..4878ec520 100644 --- a/netbox/netbox/configuration_example.py +++ b/netbox/netbox/configuration_example.py @@ -193,6 +193,9 @@ PLUGINS = [] REMOTE_AUTH_ENABLED = False REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend' REMOTE_AUTH_HEADER = 'HTTP_REMOTE_USER' +REMOTE_AUTH_USER_FIRST_NAME = 'HTTP_REMOTE_USER_FIRST_NAME' +REMOTE_AUTH_USER_LAST_NAME = 'HTTP_REMOTE_USER_LAST_NAME' +REMOTE_AUTH_USER_EMAIL = 'HTTP_REMOTE_USER_EMAIL' REMOTE_AUTH_AUTO_CREATE_USER = True REMOTE_AUTH_DEFAULT_GROUPS = [] REMOTE_AUTH_DEFAULT_PERMISSIONS = {} diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index edf88a234..689a705be 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -87,7 +87,17 @@ class RemoteUserMiddleware(RemoteUserMiddleware_): else: user = auth.authenticate(request, remote_user=username) if user: - # User is valid. Set request.user and persist user in the session + # User is valid. + # Update the User's Profile if set by request headers + if settings.REMOTE_AUTH_USER_FIRST_NAME in request.META: + user.first_name = request.META[settings.REMOTE_AUTH_USER_FIRST_NAME] + if settings.REMOTE_AUTH_USER_LAST_NAME in request.META: + user.last_name = request.META[settings.REMOTE_AUTH_USER_LAST_NAME] + if settings.REMOTE_AUTH_USER_EMAIL in request.META: + user.email = request.META[settings.REMOTE_AUTH_USER_EMAIL] + user.save() + + # Set request.user and persist user in the session # by logging the user in. request.user = user auth.login(request, user) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 9e9ae5528..f88fc19eb 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -113,6 +113,9 @@ REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS' REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {}) REMOTE_AUTH_ENABLED = getattr(configuration, 'REMOTE_AUTH_ENABLED', False) REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER') +REMOTE_AUTH_USER_FIRST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_FIRST_NAME', 'HTTP_REMOTE_USER_FIRST_NAME') +REMOTE_AUTH_USER_LAST_NAME = getattr(configuration, 'REMOTE_AUTH_USER_LAST_NAME', 'HTTP_REMOTE_USER_LAST_NAME') +REMOTE_AUTH_USER_EMAIL = getattr(configuration, 'REMOTE_AUTH_USER_EMAIL', 'HTTP_REMOTE_USER_EMAIL') REMOTE_AUTH_GROUP_HEADER = getattr(configuration, 'REMOTE_AUTH_GROUP_HEADER', 'HTTP_REMOTE_USER_GROUP') REMOTE_AUTH_GROUP_SYNC_ENABLED = getattr(configuration, 'REMOTE_AUTH_GROUP_SYNC_ENABLED', False) REMOTE_AUTH_SUPERUSER_GROUPS = getattr(configuration, 'REMOTE_AUTH_SUPERUSER_GROUPS', []) diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index ef4554b4b..790cb4bd8 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -151,6 +151,29 @@ class ExternalAuthenticationTestCase(TestCase): self.assertEqual(int(self.client.session.get( '_auth_user_id')), self.user.pk, msg='Authentication failed') + @override_settings( + REMOTE_AUTH_ENABLED=True, + LOGIN_REQUIRED=True + ) + def test_remote_auth_user_profile(self): + """ + Test remote authentication with user profile details. + """ + headers = { + 'HTTP_REMOTE_USER': 'remoteuser1', + 'HTTP_REMOTE_USER_FIRST_NAME': 'John', + 'HTTP_REMOTE_USER_LAST_NAME': 'Smith', + 'HTTP_REMOTE_USER_EMAIL': 'johnsmith@example.com', + } + + response = self.client.get(reverse('home'), follow=True, **headers) + self.assertEqual(response.status_code, 200) + + self.user = User.objects.get(username='remoteuser1') + self.assertEqual(self.user.first_name, "John", msg='User first name was not updated') + self.assertEqual(self.user.last_name, "Smith", msg='User last name was not updated') + self.assertEqual(self.user.email, "johnsmith@example.com", msg='User email was not updated') + @override_settings( REMOTE_AUTH_ENABLED=True, REMOTE_AUTH_AUTO_CREATE_USER=True, From 8b7ee0a0db3e8347fb32bee64195ae47b5207c44 Mon Sep 17 00:00:00 2001 From: Austin de Coup-Crank <94914780+decoupca@users.noreply.github.com> Date: Thu, 20 Apr 2023 16:04:47 -0500 Subject: [PATCH 04/18] 11383 fix search order (#12251) * Fixes #11383: Sorting search by type doesn't work * Fixes #11383: Sorting search by type doesn't work; more reliable approach --- netbox/netbox/tables/tables.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 3a2e71084..3047719b7 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -200,7 +200,8 @@ class NetBoxTable(BaseTable): class SearchTable(tables.Table): object_type = columns.ContentTypeColumn( - verbose_name=_('Type') + verbose_name=_('Type'), + order_by="object___meta__verbose_name", ) object = tables.Column( linkify=True From 12bb0ec1fe2ff79d082addfb180cfcaf6c7dc756 Mon Sep 17 00:00:00 2001 From: Janik H Date: Fri, 21 Apr 2023 14:42:01 +0200 Subject: [PATCH 05/18] Fix typo in api token auth --- docs/integrations/rest-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md index 28d9b55b6..09376ee46 100644 --- a/docs/integrations/rest-api.md +++ b/docs/integrations/rest-api.md @@ -638,7 +638,7 @@ $ curl -X POST \ https://netbox/api/users/tokens/provision/ \ --data '{ "username": "hankhill", - "password": "I<3C3H8", + "password": "I<3C3H8" }' ``` From 38a0ed5e246ce78562f0fd192787917723863a45 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Fri, 21 Apr 2023 09:36:11 -0700 Subject: [PATCH 06/18] 12255 inventory item device change (#12311) * #12255 allow inventory items to change devices * #12255 allow inventory item template to change devices * #12255 fix init * 12255 remove can_swtich from template model * 12255 change to check module list --- .../dcim/models/device_component_templates.py | 25 +++++++++++-------- netbox/dcim/models/device_components.py | 3 ++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 6ff58b0f0..c78300598 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -81,11 +81,25 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel): """ raise NotImplementedError() + 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) objectchange.related_object = self.device_type return objectchange + 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." + }) + class ModularComponentTemplateModel(ComponentTemplateModel): """ @@ -120,12 +134,6 @@ 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: @@ -137,11 +145,6 @@ 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 c30ce3a97..551a66b87 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -97,7 +97,8 @@ class ComponentModel(NetBoxModel): def clean(self): super().clean() - if self.pk is not None and self._original_device != self.device_id: + # Check list of Modules that allow device field to be changed + if (type(self) not in [InventoryItem]) and (self.pk is not None) and (self._original_device != self.device_id): raise ValidationError({ "device": "Components cannot be moved to a different device." }) From c8988bac8ad7a56abb1af47d64374f6b23d75ad9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 21 Apr 2023 13:46:07 -0400 Subject: [PATCH 07/18] Add graphics --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 99ad9a597..480f0f856 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,18 @@ as the cornerstone for network automation in thousands of organizations. ## Getting Started +
+ + [![NetBox logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy1.png)](https://github.com/netbox-community/netbox) +            + [![Docker logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy2.png)](https://github.com/netbox-community/netbox-docker) +            + [![NetBox Labs logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy3.png)](https://netboxlabs.com/netbox-cloud/) + +
+ * Just want to explore? Check out [our public demo](https://demo.netbox.dev/) right now! * The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction. -* Choose your deployment: [self-hosted](https://github.com/netbox-community/netbox), [Docker](https://github.com/netbox-community/netbox-docker), or [NetBox Cloud](https://netboxlabs.com/netbox-cloud/). * Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox! ## Get Involved From 89fa546a1481e56d14cfc4d666a55482b0c85fd1 Mon Sep 17 00:00:00 2001 From: Darek Date: Fri, 21 Apr 2023 12:08:04 -0700 Subject: [PATCH 08/18] Merge pull request from GHSA-92x4-vfjf-rmf7 --- netbox/extras/models/models.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 3cab6154d..718cba5c1 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -1,4 +1,5 @@ import json +import urllib.parse import uuid from django.conf import settings @@ -28,7 +29,7 @@ from netbox.models.features import ( CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin, ) from utilities.querysets import RestrictedQuerySet -from utilities.utils import render_jinja2 +from utilities.utils import clean_html, render_jinja2 __all__ = ( 'ConfigRevision', @@ -273,6 +274,18 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged link = render_jinja2(self.link_url, context) link_target = ' target="_blank"' if self.new_window else '' + # Sanitize link text + allowed_schemes = get_config().ALLOWED_URL_SCHEMES + text = clean_html(text, allowed_schemes) + + # Sanitize link + link = urllib.parse.quote_plus(link, safe='/:?&') + + # Verify link scheme is allowed + result = urllib.parse.urlparse(link) + if result.scheme and result.scheme not in allowed_schemes: + link = "" + return { 'text': text, 'link': link, From b1130ff9b68d33214114932a7aac9a0ac47ba86a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 21 Apr 2023 15:38:27 -0400 Subject: [PATCH 09/18] Add an issue template for deprecations --- .github/ISSUE_TEMPLATE/deprecation.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/deprecation.yaml diff --git a/.github/ISSUE_TEMPLATE/deprecation.yaml b/.github/ISSUE_TEMPLATE/deprecation.yaml new file mode 100644 index 000000000..27e13e5c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/deprecation.yaml @@ -0,0 +1,24 @@ +--- +name: 🗑️ Deprecation +description: The removal of an existing feature or resource +labels: ["type: deprecation"] +body: + - type: textarea + attributes: + label: Proposed Changes + description: > + Describe in detail the proposed changes. What is being removed? + validations: + required: true + - type: textarea + attributes: + label: Justification + description: Please provide justification for the proposed change(s). + validations: + required: true + - type: textarea + attributes: + label: Impact + description: List all areas of the application that will be affected by this change. + validations: + required: true From 390619ca9939c0b20ae69c89e3d21c1880ddef63 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 21 Apr 2023 15:40:34 -0400 Subject: [PATCH 10/18] Changelog for #11383, #12205, #12226, #12255 --- docs/release-notes/version-3.4.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 60f09c2b5..5903ec004 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -2,8 +2,15 @@ ## v3.4.9 (FUTURE) +### Enhancements + +* [#12205](https://github.com/netbox-community/netbox/issues/12205) - Sanitize rendered custom links to mitigate malicious links +* [#12226](https://github.com/netbox-community/netbox/issues/12226) - Enable setting user name & email values via remote authenticate headers + ### Bug Fixes +* [#11383](https://github.com/netbox-community/netbox/issues/11383) - Fix ordering of global search results by object type +* [#12255](https://github.com/netbox-community/netbox/issues/12255) - Restore the ability to move inventory items among devices * [#12270](https://github.com/netbox-community/netbox/issues/12270) - Fix pre-population of list values when creating a saved filter * [#12296](https://github.com/netbox-community/netbox/issues/12296) - Fix "mark connected" form field for bulk editing front & rear ports From 053be952ba6553cae6ff1480415fc23929081dfa Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 21 Apr 2023 16:06:33 -0400 Subject: [PATCH 11/18] Fixes #12238: Improve error message for API token IP prefix validation failures --- docs/release-notes/version-3.4.md | 1 + netbox/users/forms.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 5903ec004..cec86665c 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -10,6 +10,7 @@ ### Bug Fixes * [#11383](https://github.com/netbox-community/netbox/issues/11383) - Fix ordering of global search results by object type +* [#12238](https://github.com/netbox-community/netbox/issues/12238) - Improve error message for API token IP prefix validation failures * [#12255](https://github.com/netbox-community/netbox/issues/12255) - Restore the ability to move inventory items among devices * [#12270](https://github.com/netbox-community/netbox/issues/12270) - Fix pre-population of list values when creating a saved filter * [#12296](https://github.com/netbox-community/netbox/issues/12296) - Fix "mark connected" form field for bulk editing front & rear ports diff --git a/netbox/users/forms.py b/netbox/users/forms.py index e8647aa5f..c87d5868b 100644 --- a/netbox/users/forms.py +++ b/netbox/users/forms.py @@ -6,6 +6,7 @@ from django.utils.html import mark_safe from django.utils.translation import gettext as _ from ipam.formfields import IPNetworkFormField +from ipam.validators import prefix_validator from netbox.preferences import PREFERENCES from utilities.forms import BootstrapMixin, DateTimePicker, StaticSelect from utilities.utils import flatten_dict @@ -104,7 +105,7 @@ class TokenForm(BootstrapMixin, forms.ModelForm): help_text=_("If no key is provided, one will be generated automatically.") ) allowed_ips = SimpleArrayField( - base_field=IPNetworkFormField(), + base_field=IPNetworkFormField(validators=[prefix_validator]), required=False, label=_('Allowed IPs'), help_text=_('Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. ' From e7663b7e393a002aa32ba5ba2c31226a63eaa473 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 21 Apr 2023 16:21:04 -0400 Subject: [PATCH 12/18] Mark Provider.account as deprecated --- netbox/templates/circuits/provider.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 51f911350..51289a735 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -30,7 +30,14 @@ - Account + + Account + {{ object.account|placeholder }} From b693123f6e377ece5f233e6ba0584680669b4088 Mon Sep 17 00:00:00 2001 From: PieterL75 <74899468+PieterL75@users.noreply.github.com> Date: Mon, 24 Apr 2023 18:01:33 +0200 Subject: [PATCH 13/18] Fixes #10987: Show rack-list dropdown in rack (#11779) * Intial. 2 ways the racknavigation displayed * show active rack in dropdown * auto hide/show when viewport reduces * Dropdown only * Update links to use get_absolute_url() --------- Co-authored-by: Pieter Lambrecht Co-authored-by: jeremystretch --- netbox/dcim/views.py | 1 + netbox/templates/dcim/rack.html | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 5a6261eba..f955aba94 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -740,6 +740,7 @@ class RackView(generic.ObjectView): 'next_rack': next_rack, 'prev_rack': prev_rack, 'svg_extra': svg_extra, + 'peer_racks': peer_racks, } diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index e2cb1597e..2384ca4ee 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -18,12 +18,31 @@ {% endblock %} {% block extra_controls %} - - Previous - - - Next - +
+ {% if prev_rack %} + + {{ prev_rack }} + + {% endif %} + + {% if peer_racks %} +
+ + +
+ {% endif %} + + {% if next_rack %} + + {{ next_rack }} + + {% endif %} + +
{% endblock %} {% block content %} From adb9673f09b540fc9f75b9b381a429a6cdf23821 Mon Sep 17 00:00:00 2001 From: Austin de Coup-Crank <94914780+decoupca@users.noreply.github.com> Date: Mon, 24 Apr 2023 11:13:28 -0500 Subject: [PATCH 14/18] Fixes #11623: obfuscate Wi-Fi PSKs (#12244) * Fixes #11623: obfuscate Wi-Fi PSKs * yarn linting fixes * include static files --- netbox/project-static/dist/netbox.js | Bin 381466 -> 382141 bytes netbox/project-static/dist/netbox.js.map | Bin 354201 -> 354850 bytes netbox/project-static/src/buttons/index.ts | 2 + .../src/buttons/secretToggle.ts | 77 ++++++++++++++++++ netbox/project-static/src/stores/index.ts | 1 + netbox/project-static/src/stores/secret.ts | 6 ++ .../wireless/inc/authentication_attrs.html | 7 +- netbox/wireless/forms/model_forms.py | 9 ++ 8 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 netbox/project-static/src/buttons/secretToggle.ts create mode 100644 netbox/project-static/src/stores/secret.ts diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index f430604f967c52fa5ed13c20a60064e1cdff88ac..e88aa3ad77ac6981d246da19e085bd553c365545 100644 GIT binary patch delta 25965 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 D76Gzl delta 25660 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 delta 72 zcmZ2lkeJcOkQMnthvj+y~~~vh?#(xd3%>V%PA+O;H2#nnplc?S#q4S aoTiIRVzK7%baZvp@pjJG9y5vM*mVFj#v7FY diff --git a/netbox/project-static/src/buttons/index.ts b/netbox/project-static/src/buttons/index.ts index fe2ccaaef..6c1c0db0b 100644 --- a/netbox/project-static/src/buttons/index.ts +++ b/netbox/project-static/src/buttons/index.ts @@ -5,6 +5,7 @@ import { initReslug } from './reslug'; import { initSelectAll } from './selectAll'; import { initSelectMultiple } from './selectMultiple'; import { initMarkdownPreviews } from './markdownPreview'; +import { initSecretToggle } from './secretToggle'; export function initButtons(): void { for (const func of [ @@ -15,6 +16,7 @@ export function initButtons(): void { initSelectMultiple, initMoveButtons, initMarkdownPreviews, + initSecretToggle, ]) { func(); } diff --git a/netbox/project-static/src/buttons/secretToggle.ts b/netbox/project-static/src/buttons/secretToggle.ts new file mode 100644 index 000000000..057a324e4 --- /dev/null +++ b/netbox/project-static/src/buttons/secretToggle.ts @@ -0,0 +1,77 @@ +import { secretState } from '../stores'; +import { getElement, getElements, isTruthy } from '../util'; + +import type { StateManager } from '../state'; + +type SecretState = { hidden: boolean }; + +/** + * Change toggle button's text and attribute to reflect the current state. + * + * @param hidden `true` if the current state is hidden, `false` otherwise. + * @param button Toggle element. + */ +function toggleSecretButton(hidden: boolean, button: HTMLButtonElement): void { + button.setAttribute('data-secret-visibility', hidden ? 'hidden' : 'shown'); + button.innerText = hidden ? 'Show Secret' : 'Hide Secret'; +} + +/** + * Show secret. + */ +function showSecret(): void { + const secret = getElement('secret'); + if (isTruthy(secret)) { + const value = secret.getAttribute('data-secret'); + if (isTruthy(value)) { + secret.innerText = value; + } + } +} + +/** + * Hide secret. + */ +function hideSecret(): void { + const secret = getElement('secret'); + if (isTruthy(secret)) { + const value = secret.getAttribute('data-secret'); + if (isTruthy(value)) { + secret.innerText = '••••••••'; + } + } +} + +/** + * Update secret state and visualization when the button is clicked. + * + * @param state State instance. + * @param button Toggle element. + */ +function handleSecretToggle(state: StateManager, button: HTMLButtonElement): void { + state.set('hidden', !state.get('hidden')); + const hidden = state.get('hidden'); + + if (hidden) { + hideSecret(); + } else { + showSecret(); + } + toggleSecretButton(hidden, button); +} + +/** + * Initialize secret toggle button. + */ +export function initSecretToggle(): void { + hideSecret(); + for (const button of getElements('button.toggle-secret')) { + button.addEventListener( + 'click', + event => { + handleSecretToggle(secretState, event.currentTarget as HTMLButtonElement); + }, + false, + ); + } +} diff --git a/netbox/project-static/src/stores/index.ts b/netbox/project-static/src/stores/index.ts index d4644e619..2ae22ede0 100644 --- a/netbox/project-static/src/stores/index.ts +++ b/netbox/project-static/src/stores/index.ts @@ -1,3 +1,4 @@ export * from './objectDepth'; export * from './rackImages'; export * from './previousPkCheck'; +export * from './secret'; diff --git a/netbox/project-static/src/stores/secret.ts b/netbox/project-static/src/stores/secret.ts new file mode 100644 index 000000000..bfb3bea6b --- /dev/null +++ b/netbox/project-static/src/stores/secret.ts @@ -0,0 +1,6 @@ +import { createState } from '../state'; + +export const secretState = createState<{ hidden: boolean }>( + { hidden: true }, + { persist: true, key: 'netbox-secret' }, +); diff --git a/netbox/templates/wireless/inc/authentication_attrs.html b/netbox/templates/wireless/inc/authentication_attrs.html index ed4c7546c..08b2d9065 100644 --- a/netbox/templates/wireless/inc/authentication_attrs.html +++ b/netbox/templates/wireless/inc/authentication_attrs.html @@ -14,7 +14,12 @@ PSK - {{ object.auth_psk|placeholder }} + + {{ object.auth_psk|placeholder }} + {% if object.auth_psk %} + + {% endif %} + diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index 8b45b0116..01c64db1d 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -1,3 +1,4 @@ +from django.forms import PasswordInput from django.utils.translation import gettext as _ from dcim.models import Device, Interface, Location, Region, Site, SiteGroup from ipam.models import VLAN, VLANGroup @@ -101,6 +102,10 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm): 'status': StaticSelect, 'auth_type': StaticSelect, 'auth_cipher': StaticSelect, + 'auth_psk': PasswordInput( + render_value=True, + attrs={'data-toggle': 'password'} + ), } @@ -206,6 +211,10 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm): 'status': StaticSelect, 'auth_type': StaticSelect, 'auth_cipher': StaticSelect, + 'auth_psk': PasswordInput( + render_value=True, + attrs={'data-toggle': 'password'} + ), } labels = { 'auth_type': 'Type', From 99af126facf5400c0e5aee1ecc640ad7799e6fd5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 25 Apr 2023 16:29:01 -0400 Subject: [PATCH 15/18] Closes #11386: Introduce CSRF_COOKIE_SECURE, SECURE_SSL_REDIRECT, and SESSION_COOKIE_SECURE configuration parameters --- docs/configuration/security.md | 25 +++++++++++++++++++++++++ docs/release-notes/version-3.4.md | 3 +++ netbox/netbox/settings.py | 3 +++ 3 files changed, 31 insertions(+) diff --git a/docs/configuration/security.md b/docs/configuration/security.md index ae023b4d0..596de1461 100644 --- a/docs/configuration/security.md +++ b/docs/configuration/security.md @@ -67,6 +67,12 @@ The name of the cookie to use for the cross-site request forgery (CSRF) authenti --- +## CSRF_COOKIE_SECURE + +Default: False + +If true, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection. + --- ## CSRF_TRUSTED_ORIGINS @@ -145,6 +151,17 @@ The view name or URL to which a user is redirected after logging out. --- +## SECURE_SSL_REDIRECT + +Default: False + +If true, all non-HTTPS requests will be automatically redirected to use HTTPS. + +!!! warning + Ensure that your frontend HTTP daemon has been configured to forward the HTTP scheme correctly before enabling this option. An incorrectly configured frontend may result in a looping redirect. + +--- + ## SESSION_COOKIE_NAME Default: `sessionid` @@ -153,6 +170,14 @@ The name used for the session cookie. See the [Django documentation](https://doc --- +## SESSION_COOKIE_SECURE + +Default: False + +If true, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection. + +--- + ## SESSION_FILE_PATH Default: None diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index cec86665c..bb4fa1e8a 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -4,6 +4,9 @@ ### Enhancements +* [#10987](https://github.com/netbox-community/netbox/issues/10987) - Show peer racks as a dropdown list under rack view +* [#11386](https://github.com/netbox-community/netbox/issues/11386) - Introduce `CSRF_COOKIE_SECURE`, `SECURE_SSL_REDIRECT`, and `SESSION_COOKIE_SECURE` configuration parameters +* [#11623](https://github.com/netbox-community/netbox/issues/11623) - Hide PSK strings under wireless LAN & link views * [#12205](https://github.com/netbox-community/netbox/issues/12205) - Sanitize rendered custom links to mitigate malicious links * [#12226](https://github.com/netbox-community/netbox/issues/12226) - Enable setting user name & email values via remote authenticate headers diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index f88fc19eb..b21674e19 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -82,6 +82,7 @@ CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False) CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', []) CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken') +CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False) CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', []) DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') @@ -127,6 +128,7 @@ REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 're RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300) SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/') SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend') +SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False) SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', DEFAULT_SENTRY_DSN) SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False) SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0) @@ -134,6 +136,7 @@ SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {}) SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid') +SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False) SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') From d87235af2fc560731802ba475018b13a0c82af5c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 26 Apr 2023 10:44:56 -0400 Subject: [PATCH 16/18] Closes #12337: Enable anonymized reporting of census data --- docs/configuration/miscellaneous.md | 10 ++++++++++ docs/release-notes/version-3.4.md | 1 + netbox/netbox/settings.py | 22 +++++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md index 8550564d8..875308610 100644 --- a/docs/configuration/miscellaneous.md +++ b/docs/configuration/miscellaneous.md @@ -45,6 +45,16 @@ Sets content for the top banner in the user interface. --- +## CENSUS_REPORTING_ENABLED + +Default: True + +Enables anonymous census reporting. To opt out of census reporting, set this to False. + +This data enables the project maintainers to estimate how many NetBox deployments exist and track the adoption of new versions over time. Census reporting effects a single HTTP request each time a worker starts. The only data reported by this function are the NetBox version, Python version, and a pseudorandom unique identifier. + +--- + ## CHANGELOG_RETENTION !!! tip "Dynamic Configuration Parameter" diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index bb4fa1e8a..03ee453d5 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -9,6 +9,7 @@ * [#11623](https://github.com/netbox-community/netbox/issues/11623) - Hide PSK strings under wireless LAN & link views * [#12205](https://github.com/netbox-community/netbox/issues/12205) - Sanitize rendered custom links to mitigate malicious links * [#12226](https://github.com/netbox-community/netbox/issues/12226) - Enable setting user name & email values via remote authenticate headers +* [#12337](https://github.com/netbox-community/netbox/issues/12337) - Enable anonymized reporting of census data ### Bug Fixes diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b21674e19..9ec8b74b1 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -3,9 +3,10 @@ import importlib import importlib.util import os import platform +import requests import sys import warnings -from urllib.parse import urlsplit +from urllib.parse import urlencode, urlsplit import django import sentry_sdk @@ -78,6 +79,7 @@ BASE_PATH = getattr(configuration, 'BASE_PATH', '') if BASE_PATH: BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only CSRF_COOKIE_PATH = LANGUAGE_COOKIE_PATH = SESSION_COOKIE_PATH = f'/{BASE_PATH.rstrip("/")}' +CENSUS_REPORTING_ENABLED = getattr(configuration, 'CENSUS_REPORTING_ENABLED', True) CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False) CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', []) CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) @@ -499,6 +501,24 @@ if SENTRY_ENABLED: sentry_sdk.set_tag('netbox.deployment_id', DEPLOYMENT_ID) +# +# Census collection +# + +CENSUS_URL = 'https://census.netbox.dev/api/v1/' +CENSUS_PARAMS = { + 'version': VERSION, + 'python_version': sys.version.split()[0], + 'deployment_id': DEPLOYMENT_ID, +} +if CENSUS_REPORTING_ENABLED and not DEBUG and 'test' not in sys.argv: + try: + # Report anonymous census data + requests.get(f'{CENSUS_URL}?{urlencode(CENSUS_PARAMS)}', timeout=3, proxies=HTTP_PROXIES) + except requests.exceptions.RequestException: + pass + + # # Django social auth # From 1ad029712eacc2bf671c2d4dcdb30d45b5953669 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 25 Apr 2023 14:44:45 -0700 Subject: [PATCH 17/18] #11902 validate device on inventory item import --- netbox/dcim/forms/bulk_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index d29e8e250..af89c35c8 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -928,7 +928,7 @@ class InventoryItemImportForm(NetBoxModelImportForm): component_name = self.cleaned_data.get('component_name') device = self.cleaned_data.get("device") - if not device and hasattr(self, 'instance'): + if not device and hasattr(self, 'instance') and hasattr(self.instance, 'device'): device = self.instance.device if not all([device, content_type, component_name]): From a49fdad5e1a6fda573521483cd108219829c1503 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 26 Apr 2023 14:33:23 -0400 Subject: [PATCH 18/18] Release v3.4.9 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.4.md | 3 ++- netbox/netbox/settings.py | 2 +- requirements.txt | 6 +++--- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index a0af66c42..956b682b0 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.8 + placeholder: v3.4.9 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index a26ee7bc1..d3f337175 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.8 + placeholder: v3.4.9 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 03ee453d5..bcb3a9ad2 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -1,6 +1,6 @@ # NetBox v3.4 -## v3.4.9 (FUTURE) +## v3.4.9 (2023-04-26) ### Enhancements @@ -14,6 +14,7 @@ ### Bug Fixes * [#11383](https://github.com/netbox-community/netbox/issues/11383) - Fix ordering of global search results by object type +* [#11902](https://github.com/netbox-community/netbox/issues/11902) - Fix import of inventory items for devices with duplicated names * [#12238](https://github.com/netbox-community/netbox/issues/12238) - Improve error message for API token IP prefix validation failures * [#12255](https://github.com/netbox-community/netbox/issues/12255) - Restore the ability to move inventory items among devices * [#12270](https://github.com/netbox-community/netbox/issues/12270) - Fix pre-population of list values when creating a saved filter diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 9ec8b74b1..03970ff75 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.4.9-dev' +VERSION = '3.4.9' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index ce79ac1b8..993a55e8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,15 +19,15 @@ graphene-django==3.0.0 gunicorn==20.1.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.1.6 +mkdocs-material==9.1.8 mkdocstrings[python-legacy]==0.21.2 netaddr==0.8.0 Pillow==9.5.0 psycopg2-binary==2.9.6 PyYAML==6.0 -sentry-sdk==1.19.1 +sentry-sdk==1.21.0 social-auth-app-django==5.0.0 -social-auth-core[openidconnect]==4.4.1 +social-auth-core[openidconnect]==4.4.2 svgwrite==1.4.3 tablib==3.4.0 tzdata==2023.3