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

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

@@ -13,7 +13,7 @@ from dcim.models import (
 )
 )
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.api.serializers import NestedTenantSerializer
 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']
         fields = ['id', 'name', 'slug', 'parent']
 
 
 
 
-class WritableRegionSerializer(serializers.ModelSerializer):
+class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Region
         model = Region
@@ -98,7 +98,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'slug']
         fields = ['id', 'url', 'name', 'slug']
 
 
 
 
-class WritableRackGroupSerializer(serializers.ModelSerializer):
+class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = RackGroup
         model = RackGroup
@@ -109,7 +109,7 @@ class WritableRackGroupSerializer(serializers.ModelSerializer):
 # Rack roles
 # Rack roles
 #
 #
 
 
-class RackRoleSerializer(serializers.ModelSerializer):
+class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = RackRole
         model = RackRole
@@ -174,6 +174,9 @@ class WritableRackSerializer(CustomFieldModelSerializer):
             validator.set_context(self)
             validator.set_context(self)
             validator(data)
             validator(data)
 
 
+        # Enforce model validation
+        super(WritableRackSerializer, self).validate(data)
+
         return data
         return data
 
 
 
 
@@ -211,7 +214,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
         fields = ['id', 'rack', 'units', 'created', 'user', 'description']
         fields = ['id', 'rack', 'units', 'created', 'user', 'description']
 
 
 
 
-class WritableRackReservationSerializer(serializers.ModelSerializer):
+class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = RackReservation
         model = RackReservation
@@ -222,7 +225,7 @@ class WritableRackReservationSerializer(serializers.ModelSerializer):
 # Manufacturers
 # Manufacturers
 #
 #
 
 
-class ManufacturerSerializer(serializers.ModelSerializer):
+class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Manufacturer
         model = Manufacturer
@@ -287,7 +290,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritableConsolePortTemplateSerializer(serializers.ModelSerializer):
+class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = ConsolePortTemplate
         model = ConsolePortTemplate
@@ -306,7 +309,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritableConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
+class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = ConsoleServerPortTemplate
         model = ConsoleServerPortTemplate
@@ -325,7 +328,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritablePowerPortTemplateSerializer(serializers.ModelSerializer):
+class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = PowerPortTemplate
         model = PowerPortTemplate
@@ -344,7 +347,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritablePowerOutletTemplateSerializer(serializers.ModelSerializer):
+class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = PowerOutletTemplate
         model = PowerOutletTemplate
@@ -364,7 +367,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
         fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
 
 
 
 
-class WritableInterfaceTemplateSerializer(serializers.ModelSerializer):
+class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = InterfaceTemplate
         model = InterfaceTemplate
@@ -383,7 +386,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
+class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = DeviceBayTemplate
         model = DeviceBayTemplate
@@ -394,7 +397,7 @@ class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
 # Device roles
 # Device roles
 #
 #
 
 
-class DeviceRoleSerializer(serializers.ModelSerializer):
+class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = DeviceRole
         model = DeviceRole
@@ -413,7 +416,7 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
 # Platforms
 # Platforms
 #
 #
 
 
-class PlatformSerializer(serializers.ModelSerializer):
+class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Platform
         model = Platform
@@ -496,6 +499,9 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
             validator.set_context(self)
             validator.set_context(self)
             validator(data)
             validator(data)
 
 
+        # Enforce model validation
+        super(WritableDeviceSerializer, self).validate(data)
+
         return data
         return data
 
 
 
 
@@ -512,7 +518,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
         read_only_fields = ['connected_console']
         read_only_fields = ['connected_console']
 
 
 
 
-class WritableConsoleServerPortSerializer(serializers.ModelSerializer):
+class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = ConsoleServerPort
         model = ConsoleServerPort
@@ -532,7 +538,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
         fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
         fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
 
 
 
 
-class WritableConsolePortSerializer(serializers.ModelSerializer):
+class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = ConsolePort
         model = ConsolePort
@@ -552,7 +558,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
         read_only_fields = ['connected_port']
         read_only_fields = ['connected_port']
 
 
 
 
-class WritablePowerOutletSerializer(serializers.ModelSerializer):
+class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = PowerOutlet
         model = PowerOutlet
@@ -572,7 +578,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
         fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
         fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
 
 
 
 
-class WritablePowerPortSerializer(serializers.ModelSerializer):
+class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = PowerPort
         model = PowerPort
@@ -630,7 +636,7 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
         ]
         ]
 
 
 
 
-class WritableInterfaceSerializer(serializers.ModelSerializer):
+class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Interface
         model = Interface
@@ -652,7 +658,7 @@ class DeviceBaySerializer(serializers.ModelSerializer):
         fields = ['id', 'device', 'name', 'installed_device']
         fields = ['id', 'device', 'name', 'installed_device']
 
 
 
 
-class WritableDeviceBaySerializer(serializers.ModelSerializer):
+class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = DeviceBay
         model = DeviceBay
@@ -675,7 +681,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
         ]
         ]
 
 
 
 
-class WritableInventoryItemSerializer(serializers.ModelSerializer):
+class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = InventoryItem
         model = InventoryItem
@@ -707,7 +713,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'connection_status']
         fields = ['id', 'url', 'connection_status']
 
 
 
 
-class WritableInterfaceConnectionSerializer(serializers.ModelSerializer):
+class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = InterfaceConnection
         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)},
                 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):
     def create(self, validated_data):
 
 
         custom_fields = validated_data.pop('custom_fields', None)
         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,
     ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
 )
 )
 from users.api.serializers import NestedUserSerializer
 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
         return serializer(obj.parent, context={'request': self.context['request']}).data
 
 
 
 
-class WritableImageAttachmentSerializer(serializers.ModelSerializer):
+class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
     content_type = ContentTypeFieldSerializer()
     content_type = ContentTypeFieldSerializer()
 
 
     class Meta:
     class Meta:
@@ -121,6 +121,9 @@ class WritableImageAttachmentSerializer(serializers.ModelSerializer):
                 "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
                 "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
             )
             )
 
 
+        # Enforce model validation
+        super(WritableImageAttachmentSerializer, self).validate(data)
+
         return 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,
     PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
 )
 )
 from tenancy.api.serializers import NestedTenantSerializer
 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
 # Roles
 #
 #
 
 
-class RoleSerializer(serializers.ModelSerializer):
+class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Role
         model = Role
@@ -64,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
 # RIRs
 # RIRs
 #
 #
 
 
-class RIRSerializer(serializers.ModelSerializer):
+class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = RIR
         model = RIR
@@ -142,6 +142,9 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
                 validator.set_context(self)
                 validator.set_context(self)
                 validator(data)
                 validator(data)
 
 
+        # Enforce model validation
+        super(WritableVLANGroupSerializer, self).validate(data)
+
         return data
         return data
 
 
 
 
@@ -188,6 +191,9 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
                 validator.set_context(self)
                 validator.set_context(self)
                 validator(data)
                 validator(data)
 
 
+        # Enforce model validation
+        super(WritableVLANSerializer, self).validate(data)
+
         return data
         return data
 
 
 
 
@@ -297,6 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
         fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
         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 WritableServiceSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     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 dcim.api.serializers import NestedDeviceSerializer
 from secrets.models import Secret, SecretRole
 from secrets.models import Secret, SecretRole
+from utilities.api import ModelValidationMixin
 
 
 
 
 #
 #
 # SecretRoles
 # SecretRoles
 #
 #
 
 
-class SecretRoleSerializer(serializers.ModelSerializer):
+class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = SecretRole
         model = SecretRole
@@ -55,4 +56,7 @@ class WritableSecretSerializer(serializers.ModelSerializer):
             validator.set_context(self)
             validator.set_context(self)
             validator(data)
             validator(data)
 
 
+        # Enforce model validation
+        super(WritableSecretSerializer, self).validate(data)
+
         return 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 extras.api.customfields import CustomFieldModelSerializer
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
+from utilities.api import ModelValidationMixin
 
 
 
 
 #
 #
 # Tenant groups
 # Tenant groups
 #
 #
 
 
-class TenantGroupSerializer(serializers.ModelSerializer):
+class TenantGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = TenantGroup
         model = TenantGroup

+ 11 - 0
netbox/utilities/api.py

@@ -98,6 +98,17 @@ class ContentTypeFieldSerializer(Field):
             raise ValidationError("Invalid content type")
             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):
 class WritableSerializerMixin(object):
     """
     """
     Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT).
     Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT).