Compare commits

...

19 Commits

Author SHA1 Message Date
Jeremy Stretch
f867cb3ae0 Merge pull request #12354 from netbox-community/develop
Release v3.4.9
2023-04-26 14:54:08 -04:00
jeremystretch
a49fdad5e1 Release v3.4.9 2023-04-26 14:33:23 -04:00
Arthur
1ad029712e #11902 validate device on inventory item import 2023-04-26 14:22:37 -04:00
jeremystretch
d87235af2f Closes #12337: Enable anonymized reporting of census data 2023-04-26 10:44:56 -04:00
jeremystretch
99af126fac Closes #11386: Introduce CSRF_COOKIE_SECURE, SECURE_SSL_REDIRECT, and SESSION_COOKIE_SECURE configuration parameters 2023-04-25 16:29:01 -04:00
Austin de Coup-Crank
adb9673f09 Fixes #11623: obfuscate Wi-Fi PSKs (#12244)
* Fixes #11623: obfuscate Wi-Fi PSKs

* yarn linting fixes

* include static files
2023-04-24 12:13:28 -04:00
PieterL75
b693123f6e 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 <pieter.lambrecht@sentia.com>
Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-04-24 12:01:33 -04:00
jeremystretch
e7663b7e39 Mark Provider.account as deprecated 2023-04-21 16:21:04 -04:00
jeremystretch
053be952ba Fixes #12238: Improve error message for API token IP prefix validation failures 2023-04-21 16:06:33 -04:00
jeremystretch
390619ca99 Changelog for #11383, #12205, #12226, #12255 2023-04-21 15:40:34 -04:00
jeremystretch
b1130ff9b6 Add an issue template for deprecations 2023-04-21 15:38:27 -04:00
Darek
89fa546a14 Merge pull request from GHSA-92x4-vfjf-rmf7 2023-04-21 15:08:04 -04:00
jeremystretch
c8988bac8a Add graphics 2023-04-21 13:46:07 -04:00
Arthur Hanson
38a0ed5e24 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
2023-04-21 12:36:11 -04:00
Janik H
12bb0ec1fe Fix typo in api token auth 2023-04-21 10:01:13 -04:00
Austin de Coup-Crank
8b7ee0a0db 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
2023-04-20 17:04:47 -04:00
Luke Anderson
ab3531558a 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 <jstretch@netboxlabs.com>
2023-04-20 15:49:54 -04:00
jeremystretch
164b2a5016 Fixes #12270: Fix pre-population of list values when creating a saved filter 2023-04-19 17:41:38 -04:00
jeremystretch
7b374e4cf6 Fixes #12296: Fix 'mark connected' form field for bulk editing front & rear ports 2023-04-19 17:25:32 -04:00
34 changed files with 375 additions and 45 deletions

View File

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

24
.github/ISSUE_TEMPLATE/deprecation.yaml vendored Normal file
View File

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

View File

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

View File

@@ -29,9 +29,18 @@ as the cornerstone for network automation in thousands of organizations.
## Getting Started
<div align="center">
[![NetBox logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy1.png)](https://github.com/netbox-community/netbox)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Docker logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy2.png)](https://github.com/netbox-community/netbox-docker)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![NetBox Labs logo](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/deploy/deploy3.png)](https://netboxlabs.com/netbox-cloud/)
</div>
* 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -638,7 +638,7 @@ $ curl -X POST \
https://netbox/api/users/tokens/provision/ \
--data '{
"username": "hankhill",
"password": "I<3C3H8",
"password": "I<3C3H8"
}'
```

View File

@@ -1,6 +1,24 @@
# NetBox v3.4
## v3.4.9 (FUTURE)
## 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
---

View File

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

View File

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

View File

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

View File

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

View File

@@ -740,6 +740,7 @@ class RackView(generic.ObjectView):
'next_rack': next_rack,
'prev_rack': prev_rack,
'svg_extra': svg_extra,
'peer_racks': peer_racks,
}

View File

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

View File

@@ -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 = {}

View File

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

View File

@@ -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.9-dev'
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
#

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -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<SecretState>, 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<HTMLButtonElement>('button.toggle-secret')) {
button.addEventListener(
'click',
event => {
handleSecretToggle(secretState, event.currentTarget as HTMLButtonElement);
},
false,
);
}
}

View File

@@ -1,3 +1,4 @@
export * from './objectDepth';
export * from './rackImages';
export * from './previousPkCheck';
export * from './secret';

View File

@@ -0,0 +1,6 @@
import { createState } from '../state';
export const secretState = createState<{ hidden: boolean }>(
{ hidden: true },
{ persist: true, key: 'netbox-secret' },
);

View File

@@ -30,7 +30,14 @@
</td>
</tr>
<tr>
<th scope="row">Account</th>
<th scope="row">
Account <i
class="mdi mdi-alert-box text-warning"
data-bs-toggle="tooltip"
data-bs-placement="right"
title="This field has been deprecated, and will be removed in NetBox v3.5."
></i>
</th>
<td>{{ object.account|placeholder }}</td>
</tr>
<tr>

View File

@@ -18,12 +18,31 @@
{% endblock %}
{% block extra_controls %}
<a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not prev_rack %} disabled{% endif %}">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
</a>
<a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not next_rack %} disabled{% endif %}">
<i class="mdi mdi-chevron-right" aria-hidden="true"></i> Next
</a>
<div class="btn-group" role="group" aria-label="RackNavigation">
{% if prev_rack %}
<a href="{{ prev_rack.get_absolute_url }}" class="btn btn-sm btn-primary">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> {{ prev_rack }}
</a>
{% endif %}
{% if peer_racks %}
<div class="btn-group" role="group">
<button class="btn btn-sm btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">{{ object }}</button>
<ul class="dropdown-menu">
{% for peer_rack in peer_racks %}
<li><a class="dropdown-item{% if peer_rack.pk == object.pk %} active{% endif %}" href="{{ peer_rack.get_absolute_url }}">{{ peer_rack }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if next_rack %}
<a href="{{ next_rack.get_absolute_url }}" class="btn btn-sm btn-primary">
{{ next_rack }} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
</a>
{% endif %}
</div>
{% endblock %}
{% block content %}

View File

@@ -14,7 +14,12 @@
</tr>
<tr>
<th scope="row">PSK</th>
<td class="font-monospace">{{ object.auth_psk|placeholder }}</td>
<td>
<span id="secret" class="font-monospace" data-secret="{{ object.auth_psk }}">{{ object.auth_psk|placeholder }}</span>
{% if object.auth_psk %}
<button type="button" class="btn btn-sm btn-primary toggle-secret float-end" data-bs-toggle="button">Show Secret</button>
{% endif %}
</td>
</tr>
</table>
</div>

View File

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

View File

@@ -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}&parameters={quote(parameters)}"

View File

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

View File

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