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

Draft
wants to merge 12 commits into
base: feature
Choose a base branch
from
Draft
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 @@ -231,8 +232,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
9 changes: 6 additions & 3 deletions netbox/dcim/forms/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ 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')
interface_mode = get_field_value(self, parent_field)
tagged_vlans = get_field_value(self, 'tagged_vlans') if 'tagged_vlans' in self.fields.keys() else []

# Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
Expand All @@ -51,8 +52,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 @@ -894,6 +894,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
30 changes: 29 additions & 1 deletion 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 All @@ -11,7 +13,7 @@
from netbox.api.serializers import GenericObjectSerializer
from tenancy.models import Tenant
from users.models import User
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_warnings
from virtualization.models import Cluster, ClusterType
from wireless.choices import WirelessChannelChoices
from wireless.models import WirelessLAN
Expand Down Expand Up @@ -1718,6 +1720,32 @@ 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')
url = self._get_list_url()
initial_count = self._get_queryset().count()

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
143 changes: 141 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,129 @@ 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)
form.is_valid()
self.assertTrue(form.is_valid())

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

data = {
'device': self.device.pk,
'name': 'Ethernet 1/9',
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
'mode': InterfaceModeChoices.MODE_ACCESS,
}
form = InterfaceForm(data, instance=self.interface)
self.assertFalse(form.is_valid())
self.interface.tagged_vlans.clear()
Loading