diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index f2e6d0d00b6..cdab3427a37 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -6,6 +6,7 @@ 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 Meta: # Circuit types # -class CircuitTypeSerializer(serializers.ModelSerializer): +class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = CircuitType @@ -110,7 +111,7 @@ class Meta: ] -class WritableCircuitTerminationSerializer(serializers.ModelSerializer): +class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = CircuitTermination diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 7d48913080c..d0a8d4a4366 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -13,7 +13,7 @@ ) 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 Meta: fields = ['id', 'name', 'slug', 'parent'] -class WritableRegionSerializer(serializers.ModelSerializer): +class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = Region @@ -98,7 +98,7 @@ class Meta: fields = ['id', 'url', 'name', 'slug'] -class WritableRackGroupSerializer(serializers.ModelSerializer): +class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = RackGroup @@ -109,7 +109,7 @@ class Meta: # Rack roles # -class RackRoleSerializer(serializers.ModelSerializer): +class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = RackRole @@ -174,6 +174,9 @@ def validate(self, data): validator.set_context(self) validator(data) + # Enforce model validation + super(WritableRackSerializer, self).validate(data) + return data @@ -211,7 +214,7 @@ class Meta: 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 Meta: # Manufacturers # -class ManufacturerSerializer(serializers.ModelSerializer): +class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = Manufacturer @@ -287,7 +290,7 @@ class Meta: fields = ['id', 'device_type', 'name'] -class WritableConsolePortTemplateSerializer(serializers.ModelSerializer): +class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = ConsolePortTemplate @@ -306,7 +309,7 @@ class Meta: fields = ['id', 'device_type', 'name'] -class WritableConsoleServerPortTemplateSerializer(serializers.ModelSerializer): +class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = ConsoleServerPortTemplate @@ -325,7 +328,7 @@ class Meta: fields = ['id', 'device_type', 'name'] -class WritablePowerPortTemplateSerializer(serializers.ModelSerializer): +class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = PowerPortTemplate @@ -344,7 +347,7 @@ class Meta: fields = ['id', 'device_type', 'name'] -class WritablePowerOutletTemplateSerializer(serializers.ModelSerializer): +class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = PowerOutletTemplate @@ -364,7 +367,7 @@ class Meta: 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 Meta: fields = ['id', 'device_type', 'name'] -class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer): +class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = DeviceBayTemplate @@ -394,7 +397,7 @@ class Meta: # Device roles # -class DeviceRoleSerializer(serializers.ModelSerializer): +class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = DeviceRole @@ -413,7 +416,7 @@ class Meta: # Platforms # -class PlatformSerializer(serializers.ModelSerializer): +class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = Platform @@ -496,6 +499,9 @@ def validate(self, data): validator.set_context(self) validator(data) + # Enforce model validation + super(WritableDeviceSerializer, self).validate(data) + return data @@ -512,7 +518,7 @@ class Meta: read_only_fields = ['connected_console'] -class WritableConsoleServerPortSerializer(serializers.ModelSerializer): +class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = ConsoleServerPort @@ -532,7 +538,7 @@ class Meta: 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 Meta: read_only_fields = ['connected_port'] -class WritablePowerOutletSerializer(serializers.ModelSerializer): +class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = PowerOutlet @@ -572,7 +578,7 @@ class Meta: 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 Meta: ] -class WritableInterfaceSerializer(serializers.ModelSerializer): +class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = Interface @@ -652,7 +658,7 @@ class Meta: fields = ['id', 'device', 'name', 'installed_device'] -class WritableDeviceBaySerializer(serializers.ModelSerializer): +class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = DeviceBay @@ -675,7 +681,7 @@ class Meta: ] -class WritableInventoryItemSerializer(serializers.ModelSerializer): +class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = InventoryItem @@ -707,7 +713,7 @@ class Meta: fields = ['id', 'url', 'connection_status'] -class WritableInterfaceConnectionSerializer(serializers.ModelSerializer): +class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = InterfaceConnection diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 5a1878b7781..52f127a7d6b 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -111,6 +111,16 @@ def _save_custom_fields(self, instance, custom_fields): 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) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index c8b3ff6f762..39ce63524c0 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -10,7 +10,7 @@ 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 @@ def get_parent(self, obj): 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 @@ def validate(self, data): "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) ) + # Enforce model validation + super(WritableImageAttachmentSerializer, self).validate(data) + return data diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 5a7d9635277..1374d355275 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -11,7 +11,7 @@ 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 Meta: # Roles # -class RoleSerializer(serializers.ModelSerializer): +class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = Role @@ -64,7 +64,7 @@ class Meta: # RIRs # -class RIRSerializer(serializers.ModelSerializer): +class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer): class Meta: model = RIR @@ -142,6 +142,9 @@ def validate(self, data): validator.set_context(self) validator(data) + # Enforce model validation + super(WritableVLANGroupSerializer, self).validate(data) + return data @@ -188,6 +191,9 @@ def validate(self, data): validator.set_context(self) validator(data) + # Enforce model validation + super(WritableVLANSerializer, self).validate(data) + return data @@ -297,6 +303,7 @@ class Meta: 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: diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index 3c7132d3765..ff2eb1dfa55 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -5,13 +5,14 @@ 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 @@ def validate(self, data): validator.set_context(self) validator(data) + # Enforce model validation + super(WritableSecretSerializer, self).validate(data) + return data diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 712d524c58c..ef5b15a169c 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -4,13 +4,14 @@ 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 diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 6fcfc694946..5774584a69f 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -98,6 +98,17 @@ def to_internal_value(self, data): 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).