mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
f49e4ee512
24
.github/ISSUE_TEMPLATE/deprecation.yaml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/deprecation.yaml
vendored
Normal 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
|
11
README.md
11
README.md
@ -29,9 +29,18 @@ as the cornerstone for network automation in thousands of organizations.
|
||||
|
||||
## Getting Started
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/netbox-community/netbox)
|
||||
|
||||
[](https://github.com/netbox-community/netbox-docker)
|
||||
|
||||
[](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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -638,7 +638,7 @@ $ curl -X POST \
|
||||
https://netbox/api/users/tokens/provision/ \
|
||||
--data '{
|
||||
"username": "hankhill",
|
||||
"password": "I<3C3H8",
|
||||
"password": "I<3C3H8"
|
||||
}'
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
@ -1310,6 +1310,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')),
|
||||
@ -1321,6 +1326,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')),
|
||||
|
@ -947,7 +947,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]):
|
||||
|
@ -80,11 +80,25 @@ class ComponentTemplateModel(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):
|
||||
"""
|
||||
@ -119,12 +133,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:
|
||||
@ -136,11 +144,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(
|
||||
|
@ -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."
|
||||
})
|
||||
|
@ -687,6 +687,7 @@ class RackView(generic.ObjectView):
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
'svg_extra': svg_extra,
|
||||
'peer_racks': peer_racks,
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
@ -19,12 +20,13 @@ from extras.choices import *
|
||||
from extras.conditions import ConditionSet
|
||||
from extras.constants import *
|
||||
from extras.utils import FeatureQuery, image_upload
|
||||
from netbox.config import get_config
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import (
|
||||
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
|
||||
)
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import render_jinja2
|
||||
from utilities.utils import clean_html, render_jinja2
|
||||
|
||||
__all__ = (
|
||||
'ConfigRevision',
|
||||
@ -278,6 +280,18 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
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,
|
||||
|
@ -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 = {}
|
||||
|
@ -139,7 +139,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)
|
||||
|
@ -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,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')
|
||||
@ -115,6 +118,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', [])
|
||||
@ -126,6 +132,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)
|
||||
@ -133,6 +140,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
|
||||
#
|
||||
|
@ -215,7 +215,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
|
||||
|
@ -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,
|
||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -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();
|
||||
}
|
||||
|
77
netbox/project-static/src/buttons/secretToggle.ts
Normal file
77
netbox/project-static/src/buttons/secretToggle.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export * from './objectDepth';
|
||||
export * from './rackImages';
|
||||
export * from './previousPkCheck';
|
||||
export * from './secret';
|
||||
|
6
netbox/project-static/src/stores/secret.ts
Normal file
6
netbox/project-static/src/stores/secret.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { createState } from '../state';
|
||||
|
||||
export const secretState = createState<{ hidden: boolean }>(
|
||||
{ hidden: true },
|
||||
{ persist: true, key: 'netbox-secret' },
|
||||
);
|
@ -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>
|
||||
|
@ -14,10 +14,29 @@
|
||||
{% 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
|
||||
<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>
|
||||
<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
|
||||
{% 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 %}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
@ -105,7 +106,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. '
|
||||
|
@ -323,7 +323,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)}"
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
from django.forms import PasswordInput
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Device, Interface, Location, Site
|
||||
@ -59,6 +60,12 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
|
||||
'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
|
||||
'description', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'auth_psk': PasswordInput(
|
||||
render_value=True,
|
||||
attrs={'data-toggle': 'password'}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class WirelessLinkForm(TenancyForm, NetBoxModelForm):
|
||||
@ -159,6 +166,12 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
|
||||
'status', 'ssid', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description',
|
||||
'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'auth_psk': PasswordInput(
|
||||
render_value=True,
|
||||
attrs={'data-toggle': 'password'}
|
||||
),
|
||||
}
|
||||
labels = {
|
||||
'auth_type': 'Type',
|
||||
'auth_cipher': 'Cipher',
|
||||
|
@ -23,15 +23,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
|
||||
|
Loading…
Reference in New Issue
Block a user