Repository: netbox-community/netbox

Issue:
Improve error handling on available-prefixes API endpoint
### Environment
* Python version:  3.5.2
* NetBox version:  2.5.2

### Steps to Reproduce
1. Create a new prefix and note its primary key
2. Send an API request to automatically provision a new prefix within that prefix, specifying the `prefix_length` as a string. For example:


```
curl -X POST \
-H "Authorization: Token <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
https://netbox/api/ipam/prefixes/<pk>/available-prefixes/ \
--data '{"prefix_length": "28", "is_pool": true}'
```

### Expected Behavior
NetBox should return a validation error, indicating that `prefix_length` must be an integer.

### Observed Behavior
A `TypeError` exception is raised, resulting in a server error.

The ideal fix for this is to adopt an actual API serializer to handle the requested prefixes, instead of processing it as a raw data dict like we do now.


Discussion:
Is this something I could work on or is there already work underway to fix it?
@hellerve I _think_ the approach needed here is to write a generic (non-model) serializer to represent the prefix being requested. This would provide native validation of fields like `prefix_length` and `vrf`. Feel free to poke around!
I suppose this should also be done for `available-ips`, then?
I added a POC that passes all the tests and is a little better, but I don’t think all the fields are in there as of yet. I’ll make a PR tomorrow once I figure out what else needs to go in there.

Resolving patch:
diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py
--- a/netbox/ipam/api/serializers.py
+++ b/netbox/ipam/api/serializers.py
@@ -154,6 +154,33 @@ class Meta:
         read_only_fields = ['family']
 
 
+class PrefixLengthSerializer(serializers.Serializer):
+
+    prefix_length = serializers.IntegerField()
+
+    def to_internal_value(self, data):
+        requested_prefix = data.get('prefix_length')
+        if requested_prefix is None:
+            raise serializers.ValidationError({
+                'prefix_length': 'this field can not be missing'
+            })
+        if not isinstance(requested_prefix, int):
+            raise serializers.ValidationError({
+                'prefix_length': 'this field must be int type'
+            })
+
+        prefix = self.context.get('prefix')
+        if prefix.family == 4 and requested_prefix > 32:
+            raise serializers.ValidationError({
+                'prefix_length': 'Invalid prefix length ({}) for IPv4'.format((requested_prefix))
+            })
+        elif prefix.family == 6 and requested_prefix > 128:
+            raise serializers.ValidationError({
+                'prefix_length': 'Invalid prefix length ({}) for IPv6'.format((requested_prefix))
+            })
+        return data
+
+
 class AvailablePrefixSerializer(serializers.Serializer):
     """
     Representation of a prefix which does not exist in the database.
diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py
--- a/netbox/ipam/api/views.py
+++ b/netbox/ipam/api/views.py
@@ -105,45 +105,25 @@ def available_prefixes(self, request, pk=None):
             if not request.user.has_perm('ipam.add_prefix'):
                 raise PermissionDenied()
 
-            # Normalize to a list of objects
-            requested_prefixes = request.data if isinstance(request.data, list) else [request.data]
+            # Validate Requested Prefixes' length
+            serializer = serializers.PrefixLengthSerializer(
+                data=request.data if isinstance(request.data, list) else [request.data],
+                many=True,
+                context={
+                    'request': request,
+                    'prefix': prefix,
+                }
+            )
+            if not serializer.is_valid():
+                return Response(
+                    serializer.errors,
+                    status=status.HTTP_400_BAD_REQUEST
+                )
 
+            requested_prefixes = serializer.validated_data
             # Allocate prefixes to the requested objects based on availability within the parent
             for i, requested_prefix in enumerate(requested_prefixes):
 
-                # Validate requested prefix size
-                prefix_length = requested_prefix.get('prefix_length')
-                if prefix_length is None:
-                    return Response(
-                        {
-                            "detail": "Item {}: prefix_length field missing".format(i)
-                        },
-                        status=status.HTTP_400_BAD_REQUEST
-                    )
-                try:
-                    prefix_length = int(prefix_length)
-                except ValueError:
-                    return Response(
-                        {
-                            "detail": "Item {}: Invalid prefix length ({})".format(i, prefix_length),
-                        },
-                        status=status.HTTP_400_BAD_REQUEST
-                    )
-                if prefix.family == 4 and prefix_length > 32:
-                    return Response(
-                        {
-                            "detail": "Item {}: Invalid prefix length ({}) for IPv4".format(i, prefix_length),
-                        },
-                        status=status.HTTP_400_BAD_REQUEST
-                    )
-                elif prefix.family == 6 and prefix_length > 128:
-                    return Response(
-                        {
-                            "detail": "Item {}: Invalid prefix length ({}) for IPv6".format(i, prefix_length),
-                        },
-                        status=status.HTTP_400_BAD_REQUEST
-                    )
-
                 # Find the first available prefix equal to or larger than the requested size
                 for available_prefix in available_prefixes.iter_cidrs():
                     if requested_prefix['prefix_length'] >= available_prefix.prefixlen:


Test patch:
diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py
--- a/netbox/ipam/tests/test_api.py
+++ b/netbox/ipam/tests/test_api.py
@@ -611,10 +611,15 @@ def test_create_single_available_prefix(self):
             self.assertEqual(response.data['description'], data['description'])
 
         # Try to create one more prefix
-        response = self.client.post(url, {'prefix_length': 30}, **self.header)
+        response = self.client.post(url, {'prefix_length': 30}, format='json', **self.header)
         self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
         self.assertIn('detail', response.data)
 
+        # Try to create invalid prefix type
+        response = self.client.post(url, {'prefix_length': '30'}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+        self.assertIn('prefix_length', response.data[0])
+
     def test_create_multiple_available_prefixes(self):
 
         prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), is_pool=True)