Browse Source

Fixes #1285: Enforce model validation when creating/editing objects via the API

Jeremy Stretch 7 years ago
parent
commit
1f9806a480

+ 3 - 2
netbox/circuits/api/serializers.py

@@ -6,6 +6,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.api.serializers import NestedTenantSerializer
+from utilities.api import ModelValidationMixin
 
 
 #
@@ -44,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
 # Circuit types
 #
 
-class CircuitTypeSerializer(serializers.ModelSerializer):
+class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = CircuitType
@@ -110,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
         ]
 
 
-class WritableCircuitTerminationSerializer(serializers.ModelSerializer):
+class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = CircuitTermination

+ 28 - 22
netbox/dcim/api/serializers.py

@@ -13,7 +13,7 @@ from dcim.models import (
 )
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceFieldSerializer
+from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
 
 
 #
@@ -36,7 +36,7 @@ class RegionSerializer(serializers.ModelSerializer):
         fields = ['id', 'name', 'slug', 'parent']
 
 
-class WritableRegionSerializer(serializers.ModelSerializer):
+class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = Region
@@ -98,7 +98,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'slug']
 
 
-class WritableRackGroupSerializer(serializers.ModelSerializer):
+class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = RackGroup
@@ -109,7 +109,7 @@ class WritableRackGroupSerializer(serializers.ModelSerializer):
 # Rack roles
 #
 
-class RackRoleSerializer(serializers.ModelSerializer):
+class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = RackRole
@@ -174,6 +174,9 @@ class WritableRackSerializer(CustomFieldModelSerializer):
             validator.set_context(self)
             validator(data)
 
+        # Enforce model validation
+        super(WritableRackSerializer, self).validate(data)
+
         return data
 
 
@@ -211,7 +214,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
         fields = ['id', 'rack', 'units', 'created', 'user', 'description']
 
 
-class WritableRackReservationSerializer(serializers.ModelSerializer):
+class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = RackReservation
@@ -222,7 +225,7 @@ class WritableRackReservationSerializer(serializers.ModelSerializer):
 # Manufacturers
 #
 
-class ManufacturerSerializer(serializers.ModelSerializer):
+class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = Manufacturer
@@ -287,7 +290,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritableConsolePortTemplateSerializer(serializers.ModelSerializer):
+class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = ConsolePortTemplate
@@ -306,7 +309,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritableConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
+class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = ConsoleServerPortTemplate
@@ -325,7 +328,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritablePowerPortTemplateSerializer(serializers.ModelSerializer):
+class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = PowerPortTemplate
@@ -344,7 +347,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritablePowerOutletTemplateSerializer(serializers.ModelSerializer):
+class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = PowerOutletTemplate
@@ -364,7 +367,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
 
 
-class WritableInterfaceTemplateSerializer(serializers.ModelSerializer):
+class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = InterfaceTemplate
@@ -383,7 +386,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
+class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = DeviceBayTemplate
@@ -394,7 +397,7 @@ class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
 # Device roles
 #
 
-class DeviceRoleSerializer(serializers.ModelSerializer):
+class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = DeviceRole
@@ -413,7 +416,7 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
 # Platforms
 #
 
-class PlatformSerializer(serializers.ModelSerializer):
+class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = Platform
@@ -496,6 +499,9 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
             validator.set_context(self)
             validator(data)
 
+        # Enforce model validation
+        super(WritableDeviceSerializer, self).validate(data)
+
         return data
 
 
@@ -512,7 +518,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
         read_only_fields = ['connected_console']
 
 
-class WritableConsoleServerPortSerializer(serializers.ModelSerializer):
+class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = ConsoleServerPort
@@ -532,7 +538,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
         fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
 
 
-class WritableConsolePortSerializer(serializers.ModelSerializer):
+class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = ConsolePort
@@ -552,7 +558,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
         read_only_fields = ['connected_port']
 
 
-class WritablePowerOutletSerializer(serializers.ModelSerializer):
+class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = PowerOutlet
@@ -572,7 +578,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
         fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
 
 
-class WritablePowerPortSerializer(serializers.ModelSerializer):
+class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = PowerPort
@@ -630,7 +636,7 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
         ]
 
 
-class WritableInterfaceSerializer(serializers.ModelSerializer):
+class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = Interface
@@ -652,7 +658,7 @@ class DeviceBaySerializer(serializers.ModelSerializer):
         fields = ['id', 'device', 'name', 'installed_device']
 
 
-class WritableDeviceBaySerializer(serializers.ModelSerializer):
+class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = DeviceBay
@@ -675,7 +681,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
         ]
 
 
-class WritableInventoryItemSerializer(serializers.ModelSerializer):
+class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = InventoryItem
@@ -707,7 +713,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'connection_status']
 
 
-class WritableInterfaceConnectionSerializer(serializers.ModelSerializer):
+class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = InterfaceConnection

+ 10 - 0
netbox/extras/api/customfields.py

@@ -111,6 +111,16 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
                 defaults={'serialized_value': custom_field.serialize_value(value)},
             )
 
+    def validate(self, data):
+        """
+        Enforce model validation (see utilities.api.ModelValidationMixin)
+        """
+        model_data = data.copy()
+        model_data.pop('custom_fields', None)
+        instance = self.Meta.model(**model_data)
+        instance.clean()
+        return data
+
     def create(self, validated_data):
 
         custom_fields = validated_data.pop('custom_fields', None)

+ 5 - 2
netbox/extras/api/serializers.py

@@ -10,7 +10,7 @@ from extras.models import (
     ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
 )
 from users.api.serializers import NestedUserSerializer
-from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer
+from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin
 
 
 #
@@ -104,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
         return serializer(obj.parent, context={'request': self.context['request']}).data
 
 
-class WritableImageAttachmentSerializer(serializers.ModelSerializer):
+class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
     content_type = ContentTypeFieldSerializer()
 
     class Meta:
@@ -121,6 +121,9 @@ class WritableImageAttachmentSerializer(serializers.ModelSerializer):
                 "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
             )
 
+        # Enforce model validation
+        super(WritableImageAttachmentSerializer, self).validate(data)
+
         return data
 
 

+ 10 - 3
netbox/ipam/api/serializers.py

@@ -11,7 +11,7 @@ from ipam.models import (
     PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
 )
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceFieldSerializer
+from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
 
 
 #
@@ -45,7 +45,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
 # Roles
 #
 
-class RoleSerializer(serializers.ModelSerializer):
+class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = Role
@@ -64,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
 # RIRs
 #
 
-class RIRSerializer(serializers.ModelSerializer):
+class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = RIR
@@ -142,6 +142,9 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
                 validator.set_context(self)
                 validator(data)
 
+        # Enforce model validation
+        super(WritableVLANGroupSerializer, self).validate(data)
+
         return data
 
 
@@ -188,6 +191,9 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
                 validator.set_context(self)
                 validator(data)
 
+        # Enforce model validation
+        super(WritableVLANSerializer, self).validate(data)
+
         return data
 
 
@@ -297,6 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
         fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
 
 
+# TODO: Figure out how to use ModelValidationMixin with ManyToManyFields. Calling clean() yields a ValueError.
 class WritableServiceSerializer(serializers.ModelSerializer):
 
     class Meta:

+ 5 - 1
netbox/secrets/api/serializers.py

@@ -5,13 +5,14 @@ from rest_framework.validators import UniqueTogetherValidator
 
 from dcim.api.serializers import NestedDeviceSerializer
 from secrets.models import Secret, SecretRole
+from utilities.api import ModelValidationMixin
 
 
 #
 # SecretRoles
 #
 
-class SecretRoleSerializer(serializers.ModelSerializer):
+class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = SecretRole
@@ -55,4 +56,7 @@ class WritableSecretSerializer(serializers.ModelSerializer):
             validator.set_context(self)
             validator(data)
 
+        # Enforce model validation
+        super(WritableSecretSerializer, self).validate(data)
+
         return data

+ 2 - 1
netbox/tenancy/api/serializers.py

@@ -4,13 +4,14 @@ from rest_framework import serializers
 
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.models import Tenant, TenantGroup
+from utilities.api import ModelValidationMixin
 
 
 #
 # Tenant groups
 #
 
-class TenantGroupSerializer(serializers.ModelSerializer):
+class TenantGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
     class Meta:
         model = TenantGroup

+ 11 - 0
netbox/utilities/api.py

@@ -98,6 +98,17 @@ class ContentTypeFieldSerializer(Field):
             raise ValidationError("Invalid content type")
 
 
+class ModelValidationMixin(object):
+    """
+    Enforce a model's validation through clean() when validating serializer data. This is necessary to ensure we're
+    employing the same validation logic via both forms and the API.
+    """
+    def validate(self, attrs):
+        instance = self.Meta.model(**attrs)
+        instance.clean()
+        return attrs
+
+
 class WritableSerializerMixin(object):
     """
     Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT).