Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes: #15924 - Prevent API payload from allowing tagged_vlans while interface mode is set to tagged-all #17211

Open
wants to merge 17 commits into
base: feature
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion netbox/dcim/api/serializers_/device_components.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.utils.translation import gettext as _
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
Expand Down Expand Up @@ -232,8 +233,26 @@ class Meta:

def validate(self, data):

# Validate many-to-many VLAN assignments
if not self.nested:

# Validate 802.1q mode and vlan(s)
mode = None
tagged_vlans = []

if self.instance:
mode = data.get('mode') if 'mode' in data.keys() else self.instance.mode
tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else \
self.instance.tagged_vlans.all()
else:
mode = data.get('mode', None)
tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else None

if mode != InterfaceModeChoices.MODE_TAGGED and tagged_vlans:
raise serializers.ValidationError({
'tagged_vlans': _("Interface mode does not support tagged vlans")
})

# Validate many-to-many VLAN assignments
device = self.instance.device if self.instance else data.get('device')
for vlan in data.get('tagged_vlans', []):
if vlan.site not in [device.site, None]:
Expand Down
12 changes: 9 additions & 3 deletions netbox/dcim/forms/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ def clean(self):
super().clean()

parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
tagged_vlans = self.cleaned_data.get('tagged_vlans')
if 'tagged_vlans' in self.fields.keys():
tagged_vlans = self.cleaned_data.get('tagged_vlans') if self.is_bound else \
self.get_initial_for_field(self.fields['tagged_vlans'], 'tagged_vlans')
else:
tagged_vlans = []

# Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
Expand All @@ -52,8 +56,10 @@ def clean(self):
})

# Remove all tagged VLAN assignments from "tagged all" interfaces
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
self.cleaned_data['tagged_vlans'] = []
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL and tagged_vlans:
raise forms.ValidationError({
'mode': _("An tagged-all interface cannot have tagged VLANs assigned.")
})

# Validate tagged VLANs; must be a global VLAN or in the same site
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans:
Expand Down
2 changes: 2 additions & 0 deletions netbox/dcim/models/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,8 @@ def clean(self):
raise ValidationError({'rf_channel_width': _("Cannot specify custom width with channel selected.")})

# VLAN validation
if not self.mode and self.untagged_vlan:
raise ValidationError({'untagged_vlan': _("Interface mode does not support an untagged vlan.")})

# Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
Expand Down
26 changes: 26 additions & 0 deletions netbox/dcim/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from django.test import override_settings
from django.urls import reverse
from django.utils.translation import gettext as _
Expand Down Expand Up @@ -1775,6 +1777,30 @@ def test_bulk_delete_child_interfaces(self):
self.client.delete(self._get_list_url(), data, format='json', **self.header)
self.assertEqual(device.interfaces.count(), 2) # Child & parent were both deleted

def test_create_child_interfaces_mode_invalid_data(self):
"""
POST a single object without permission.
"""
self.add_permissions('dcim.add_interface')

device = Device.objects.first()
vlans = VLAN.objects.all()[0:3]

create_data = {
'device': device.pk,
'name': 'Untagged Interface 1',
'type': InterfaceTypeChoices.TYPE_1GE_FIXED,
'mode': InterfaceModeChoices.MODE_ACCESS,
'tagged_vlans': [vlans[0].pk, vlans[1].pk],
'untagged_vlan': vlans[2].pk,
}

response = self.client.post(self._get_list_url(), create_data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
content = json.loads(response.content)
self.assertIn('tagged_vlans', content)
self.assertIsNone(content.get('data'))


class FrontPortTest(APIViewTestCases.APIViewTestCase):
model = FrontPort
Expand Down
117 changes: 115 additions & 2 deletions netbox/dcim/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from django.test import TestCase

from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices
from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices, InterfaceModeChoices
from dcim.forms import *
from dcim.models import *
from ipam.models import VLAN
from utilities.testing import create_test_device
from virtualization.models import Cluster, ClusterGroup, ClusterType

Expand Down Expand Up @@ -117,11 +118,23 @@ def test_non_racked_device_with_position(self):
self.assertIn('position', form.errors)


class LabelTestCase(TestCase):
class InterfaceTestCase(TestCase):

@classmethod
def setUpTestData(cls):
cls.device = create_test_device('Device 1')
cls.vlans = (
VLAN(name='VLAN 1', vid=1),
VLAN(name='VLAN 2', vid=2),
VLAN(name='VLAN 3', vid=3),
)
VLAN.objects.bulk_create(cls.vlans)
cls.interface = Interface.objects.create(
device=cls.device,
name='Interface 1',
type=InterfaceTypeChoices.TYPE_1GE_GBIC,
mode=InterfaceModeChoices.MODE_TAGGED,
)

def test_interface_label_count_valid(self):
"""
Expand Down Expand Up @@ -151,3 +164,103 @@ def test_interface_label_count_mismatch(self):

self.assertFalse(form.is_valid())
self.assertIn('label', form.errors)

def test_create_interface_mode_valid_data(self):
"""
Test that saving valid interface mode and tagged/untagged vlans works properly
"""

# Validate access mode
data = {
'device': self.device.pk,
'name': 'ethernet1/1',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_ACCESS,
'untagged_vlan': self.vlans[0].pk
}
form = InterfaceCreateForm(data)

self.assertTrue(form.is_valid())

# Validate tagged vlans
data = {
'device': self.device.pk,
'name': 'ethernet1/2',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_TAGGED,
'untagged_vlan': self.vlans[0].pk,
'tagged_vlans': [self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceCreateForm(data)
self.assertTrue(form.is_valid())

# Validate tagged vlans
data = {
'device': self.device.pk,
'name': 'ethernet1/3',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_TAGGED_ALL,
'untagged_vlan': self.vlans[0].pk,
}
form = InterfaceCreateForm(data)
self.assertTrue(form.is_valid())

def test_create_interface_mode_access_invalid_data(self):
"""
Test that saving invalid interface mode and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'ethernet1/4',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_ACCESS,
'untagged_vlan': self.vlans[0].pk,
'tagged_vlans': [self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceCreateForm(data)

self.assertTrue(form.is_valid())

def test_edit_interface_mode_access_invalid_data(self):
"""
Test that saving invalid interface mode and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'Ethernet 1/5',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_ACCESS,
'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk]
}
form = InterfaceForm(data, instance=self.interface)

self.assertTrue(form.is_valid())

def test_create_interface_mode_tagged_all_invalid_data(self):
"""
Test that saving invalid interface mode and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'ethernet1/6',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_TAGGED_ALL,
'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2]]
}
form = InterfaceCreateForm(data)

self.assertTrue(form.is_valid())

def test_edit_interface_mode_tagged_all_invalid_data(self):
"""
Test that saving invalid interface mode and tagged/untagged vlans works properly
"""
data = {
'device': self.device.pk,
'name': 'Ethernet 1/7',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_TAGGED_ALL,
'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2]]
}
form = InterfaceForm(data, instance=self.interface)
self.assertTrue(form.is_valid())
Loading