mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-12 23:02:17 -06:00
Compare commits
1 Commits
main
...
20239-plug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7621def544 |
16
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
16
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
@@ -1,26 +1,20 @@
|
|||||||
---
|
---
|
||||||
name: ⚠️ Deprecation
|
name: 🗑️ Deprecation
|
||||||
type: Deprecation
|
type: Deprecation
|
||||||
description: Designation of a feature or behavior that will be removed in a future release
|
description: The removal of an existing feature or resource
|
||||||
labels: ["netbox", "type: deprecation"]
|
labels: ["netbox", "type: deprecation"]
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Deprecated Functionality
|
label: Proposed Changes
|
||||||
description: >
|
description: >
|
||||||
Describe the feature(s) and/or behavior that is being flagged for deprecation.
|
Describe in detail the proposed changes. What is being removed?
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Scheduled removal
|
|
||||||
description: In what future release will the deprecated functionality be removed?
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Justification
|
label: Justification
|
||||||
description: Please provide justification for the deprecation.
|
description: Please provide justification for the proposed change(s).
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
20
.github/ISSUE_TEMPLATE/07-feature_removal.yaml
vendored
20
.github/ISSUE_TEMPLATE/07-feature_removal.yaml
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: 🗑️ Feature Removal
|
|
||||||
type: Removal
|
|
||||||
description: The removal of a deprecated feature or resource
|
|
||||||
labels: ["netbox", "type: removal"]
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Deprecation Issue
|
|
||||||
description: Specify the issue in which this deprecation was announced.
|
|
||||||
placeholder: "#1234"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Summary of Changes
|
|
||||||
description: >
|
|
||||||
List all changes necessary to remove the deprecated feature or resource.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.12
|
python-version: 3.11
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt install -y gettext
|
run: sudo apt install -y gettext
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,8 +9,7 @@ yarn-error.log*
|
|||||||
/netbox/netbox/configuration.py
|
/netbox/netbox/configuration.py
|
||||||
/netbox/netbox/ldap_config.py
|
/netbox/netbox/ldap_config.py
|
||||||
/netbox/local/*
|
/netbox/local/*
|
||||||
/netbox/media/*
|
/netbox/media
|
||||||
!/netbox/media/.gitkeep
|
|
||||||
/netbox/reports/*
|
/netbox/reports/*
|
||||||
!/netbox/reports/__init__.py
|
!/netbox/reports/__init__.py
|
||||||
/netbox/scripts/*
|
/netbox/scripts/*
|
||||||
|
|||||||
@@ -259,13 +259,11 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
|
|||||||
module_bays = []
|
module_bays = []
|
||||||
modules = []
|
modules = []
|
||||||
while module:
|
while module:
|
||||||
module_module_bay = getattr(module, "module_bay", None)
|
if module.pk in modules or module.module_bay.pk in module_bays:
|
||||||
if module.pk in modules or (module_module_bay and module_module_bay.pk in module_bays):
|
|
||||||
raise ValidationError(_("A module bay cannot belong to a module installed within it."))
|
raise ValidationError(_("A module bay cannot belong to a module installed within it."))
|
||||||
modules.append(module.pk)
|
modules.append(module.pk)
|
||||||
if module_module_bay:
|
module_bays.append(module.module_bay.pk)
|
||||||
module_bays.append(module_module_bay.pk)
|
module = module.module_bay.module if module.module_bay else None
|
||||||
module = module_module_bay.module if module_module_bay else None
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
is_new = self.pk is None
|
is_new = self.pk is None
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ VPN_MENU = Menu(
|
|||||||
label=_('L2VPNs'),
|
label=_('L2VPNs'),
|
||||||
items=(
|
items=(
|
||||||
get_model_item('vpn', 'l2vpn', _('L2VPNs')),
|
get_model_item('vpn', 'l2vpn', _('L2VPNs')),
|
||||||
get_model_item('vpn', 'l2vpntermination', _('L2VPN Terminations')),
|
get_model_item('vpn', 'l2vpntermination', _('Terminations')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ class PluginMenuItem:
|
|||||||
Alternatively, a pre-generated url can be set on the object which will be rendered literally.
|
Alternatively, a pre-generated url can be set on the object which will be rendered literally.
|
||||||
Buttons are each specified as a list of PluginMenuButton instances.
|
Buttons are each specified as a list of PluginMenuButton instances.
|
||||||
"""
|
"""
|
||||||
permissions = []
|
|
||||||
buttons = []
|
|
||||||
_url = None
|
_url = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -54,10 +52,14 @@ class PluginMenuItem:
|
|||||||
if type(permissions) not in (list, tuple):
|
if type(permissions) not in (list, tuple):
|
||||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
|
else:
|
||||||
|
self.permissions = []
|
||||||
if buttons is not None:
|
if buttons is not None:
|
||||||
if type(buttons) not in (list, tuple):
|
if type(buttons) not in (list, tuple):
|
||||||
raise TypeError(_("Buttons must be passed as a tuple or list."))
|
raise TypeError(_("Buttons must be passed as a tuple or list."))
|
||||||
self.buttons = buttons
|
self.buttons = buttons
|
||||||
|
else:
|
||||||
|
self.buttons = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
@@ -74,7 +76,6 @@ class PluginMenuButton:
|
|||||||
ButtonColorChoices.
|
ButtonColorChoices.
|
||||||
"""
|
"""
|
||||||
color = ButtonColorChoices.DEFAULT
|
color = ButtonColorChoices.DEFAULT
|
||||||
permissions = []
|
|
||||||
_url = None
|
_url = None
|
||||||
|
|
||||||
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
||||||
@@ -87,6 +88,8 @@ class PluginMenuButton:
|
|||||||
if type(permissions) not in (list, tuple):
|
if type(permissions) not in (list, tuple):
|
||||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
|
else:
|
||||||
|
self.permissions = []
|
||||||
if color is not None:
|
if color is not None:
|
||||||
if color not in ButtonColorChoices.values():
|
if color not in ButtonColorChoices.values():
|
||||||
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
|
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from netbox.tests.dummy_plugin import config as dummy_config
|
|||||||
from netbox.tests.dummy_plugin.data_backends import DummyBackend
|
from netbox.tests.dummy_plugin.data_backends import DummyBackend
|
||||||
from netbox.tests.dummy_plugin.jobs import DummySystemJob
|
from netbox.tests.dummy_plugin.jobs import DummySystemJob
|
||||||
from netbox.tests.dummy_plugin.webhook_callbacks import set_context
|
from netbox.tests.dummy_plugin.webhook_callbacks import set_context
|
||||||
from netbox.plugins.navigation import PluginMenu
|
from netbox.plugins.navigation import PluginMenu, PluginMenuItem, PluginMenuButton
|
||||||
from netbox.plugins.utils import get_plugin_config
|
from netbox.plugins.utils import get_plugin_config
|
||||||
from netbox.graphql.schema import Query
|
from netbox.graphql.schema import Query
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
@@ -227,3 +227,46 @@ class PluginTest(TestCase):
|
|||||||
Test the registration of webhook callbacks.
|
Test the registration of webhook callbacks.
|
||||||
"""
|
"""
|
||||||
self.assertIn(set_context, registry['webhook_callbacks'])
|
self.assertIn(set_context, registry['webhook_callbacks'])
|
||||||
|
|
||||||
|
|
||||||
|
class PluginNavigationTest(TestCase):
|
||||||
|
|
||||||
|
def test_plugin_menu_item_independent_permissions(self):
|
||||||
|
item1 = PluginMenuItem(link='test1', link_text='Test 1')
|
||||||
|
item1.permissions.append('leaked_permission')
|
||||||
|
|
||||||
|
item2 = PluginMenuItem(link='test2', link_text='Test 2')
|
||||||
|
|
||||||
|
self.assertIsNot(item1.permissions, item2.permissions)
|
||||||
|
self.assertEqual(item1.permissions, ['leaked_permission'])
|
||||||
|
self.assertEqual(item2.permissions, [])
|
||||||
|
|
||||||
|
def test_plugin_menu_item_independent_buttons(self):
|
||||||
|
item1 = PluginMenuItem(link='test1', link_text='Test 1')
|
||||||
|
button = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
|
||||||
|
item1.buttons.append(button)
|
||||||
|
|
||||||
|
item2 = PluginMenuItem(link='test2', link_text='Test 2')
|
||||||
|
|
||||||
|
self.assertIsNot(item1.buttons, item2.buttons)
|
||||||
|
self.assertEqual(len(item1.buttons), 1)
|
||||||
|
self.assertEqual(item1.buttons[0], button)
|
||||||
|
self.assertEqual(item2.buttons, [])
|
||||||
|
|
||||||
|
def test_plugin_menu_button_independent_permissions(self):
|
||||||
|
button1 = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
|
||||||
|
button1.permissions.append('leaked_permission')
|
||||||
|
|
||||||
|
button2 = PluginMenuButton(link='button2', title='Button 2', icon_class='mdi-test')
|
||||||
|
|
||||||
|
self.assertIsNot(button1.permissions, button2.permissions)
|
||||||
|
self.assertEqual(button1.permissions, ['leaked_permission'])
|
||||||
|
self.assertEqual(button2.permissions, [])
|
||||||
|
|
||||||
|
def test_explicit_permissions_remain_independent(self):
|
||||||
|
item1 = PluginMenuItem(link='test1', link_text='Test 1', permissions=['explicit_permission'])
|
||||||
|
item2 = PluginMenuItem(link='test2', link_text='Test 2', permissions=['different_permission'])
|
||||||
|
|
||||||
|
self.assertIsNot(item1.permissions, item2.permissions)
|
||||||
|
self.assertEqual(item1.permissions, ['explicit_permission'])
|
||||||
|
self.assertEqual(item2.permissions, ['different_permission'])
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -123,7 +123,7 @@ class UserTokenForm(forms.ModelForm):
|
|||||||
token = forms.CharField(
|
token = forms.CharField(
|
||||||
label=_('Token'),
|
label=_('Token'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'Tokens must be at least 40 characters in length. <strong>Be sure to record your token</strong> prior to '
|
'Tokens must be at least 40 characters in length. <strong>Be sure to record your key</strong> prior to '
|
||||||
'submitting this form, as it will no longer be accessible once the token has been created.'
|
'submitting this form, as it will no longer be accessible once the token has been created.'
|
||||||
),
|
),
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class Token(models.Model):
|
|||||||
write_enabled = models.BooleanField(
|
write_enabled = models.BooleanField(
|
||||||
verbose_name=_('write enabled'),
|
verbose_name=_('write enabled'),
|
||||||
default=True,
|
default=True,
|
||||||
help_text=_('Permit create/update/delete operations using this token')
|
help_text=_('Permit create/update/delete operations using this key')
|
||||||
)
|
)
|
||||||
# For legacy v1 tokens, this field stores the plaintext 40-char token value. Not used for v2.
|
# For legacy v1 tokens, this field stores the plaintext 40-char token value. Not used for v2.
|
||||||
plaintext = models.CharField(
|
plaintext = models.CharField(
|
||||||
@@ -213,9 +213,6 @@ class Token(models.Model):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.version == TokenVersionChoices.V2 and not settings.API_TOKEN_PEPPERS:
|
|
||||||
raise ValidationError(_("Unable to save v2 tokens: API_TOKEN_PEPPERS is not defined."))
|
|
||||||
|
|
||||||
if self._state.adding:
|
if self._state.adding:
|
||||||
if self.pepper_id is not None and self.pepper_id not in settings.API_TOKEN_PEPPERS:
|
if self.pepper_id is not None and self.pepper_id not in settings.API_TOKEN_PEPPERS:
|
||||||
raise ValidationError(_(
|
raise ValidationError(_(
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from users.choices import TokenVersionChoices
|
|
||||||
from users.models import User, Token
|
from users.models import User, Token
|
||||||
from utilities.testing import create_test_user
|
from utilities.testing import create_test_user
|
||||||
|
|
||||||
@@ -95,15 +94,6 @@ class TokenTest(TestCase):
|
|||||||
token.refresh_from_db()
|
token.refresh_from_db()
|
||||||
self.assertEqual(token.description, 'New Description')
|
self.assertEqual(token.description, 'New Description')
|
||||||
|
|
||||||
@override_settings(API_TOKEN_PEPPERS={})
|
|
||||||
def test_v2_without_peppers_configured(self):
|
|
||||||
"""
|
|
||||||
Attempting to save a v2 token without API_TOKEN_PEPPERS defined should raise a ValidationError.
|
|
||||||
"""
|
|
||||||
token = Token(version=TokenVersionChoices.V2)
|
|
||||||
with self.assertRaises(ValidationError):
|
|
||||||
token.clean()
|
|
||||||
|
|
||||||
|
|
||||||
class UserConfigTest(TestCase):
|
class UserConfigTest(TestCase):
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user