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/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 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/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 diff --git a/base_requirements.txt b/base_requirements.txt index f3303e6e3..3cce9251b 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -1,57 +1,57 @@ # HTML sanitizer -# https://github.com/mozilla/bleach +# https://github.com/mozilla/bleach/blob/main/CHANGES bleach<6.0 # 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 @@ -59,27 +59,27 @@ 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 # Swagger/OpenAPI schema generation for REST APIs -# https://github.com/axnsan12/drf-yasg +# https://drf-yasg.readthedocs.io/en/stable/changelog.html drf-yasg[validation] # 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 @@ -88,50 +88,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/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/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/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/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/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" }' ``` diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index a01b24cc4..bcb3a9ad2 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -1,5 +1,27 @@ # NetBox v3.4 +## v3.4.9 (2023-04-26) + +### 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 +* [#12337](https://github.com/netbox-community/netbox/issues/12337) - Enable anonymized reporting of census data + +### 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 +* [#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) ### Enhancements 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')), 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]): 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." }) 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/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, 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 cfdbfd1e2..03970ff75 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 @@ -24,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.4.8' +VERSION = '3.4.9' # Hostname HOSTNAME = platform.node() @@ -78,10 +79,12 @@ 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', []) 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') @@ -113,6 +116,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', []) @@ -124,6 +130,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) @@ -131,6 +138,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') @@ -493,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 # 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 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, diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index f430604f9..e88aa3ad7 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 753576bc3..59bc99bd3 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ 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/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 }} 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 %} 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/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. ' 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)}" 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', 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