From 782709a38ffd9b4ed15542c4c7cc2fadb84f5156 Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Mon, 13 Nov 2017 14:13:07 +0100
Subject: [PATCH] Add validators for first and last IP in Network

The database constraints will catch the same error
(and raise an IntegrityError).

Having some validation at the model level allows to display nicer error
messages.
We use CSEntryError exception and not ValidationError so that it can be
used both by the API and views.
---
 app/models.py                | 35 ++++++++++++++++++++++++++++++-----
 tests/functional/test_api.py |  6 +++---
 2 files changed, 33 insertions(+), 8 deletions(-)

diff --git a/app/models.py b/app/models.py
index 40d439a..a643ab0 100644
--- a/app/models.py
+++ b/app/models.py
@@ -298,15 +298,40 @@ class Network(db.Model):
     def __str__(self):
         return str(self.prefix)
 
+    @staticmethod
+    def ip_in_network(ip, prefix):
+        """Ensure the IP is in the network
+
+        :param str user_id: unicode ID of a user
+        :returns: a tuple with the IP and network as (IPv4Address, IPv4Network)
+        :raises: CSEntryError if the IP is not in the network
+        """
+        addr = ipaddress.ip_address(ip)
+        net = ipaddress.ip_network(prefix)
+        if addr not in net:
+            raise utils.CSEntryError(f'IP address {ip} is not in network {prefix}', status_code=422)
+        return (addr, net)
+
+    @validates('first')
+    def validate_first(self, key, ip):
+        """Ensure the first IP is in the network"""
+        self.ip_in_network(ip, self.prefix)
+        return ip
+
+    @validates('last')
+    def validate_last(self, key, ip):
+        """Ensure the last IP is in the network"""
+        addr, net = self.ip_in_network(ip, self.prefix)
+        if addr < ipaddress.ip_address(self.first):
+            raise utils.CSEntryError(f'Last IP address {ip} is less than the first address {self.first}', status_code=422)
+        return ip
+
     @validates('hosts')
     def validate_hosts(self, key, host):
         """Ensure the host IP is in the network range"""
-        addr = ipaddress.ip_address(host.ip)
-        net = ipaddress.ip_network(self.prefix)
-        if addr not in net:
-            raise utils.CSEntryError(f'IP address shall be in network {self.prefix}', status_code=422)
+        addr, net = self.ip_in_network(host.ip, self.prefix)
         if addr < ipaddress.ip_address(self.first) or addr > ipaddress.ip_address(self.last):
-            raise utils.CSEntryError(f'IP address shall be in range {self.first} - {self.last}', status_code=422)
+            raise utils.CSEntryError(f'IP address {host.ip} is not in range {self.first} - {self.last}', status_code=422)
         return host
 
     def to_dict(self):
diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py
index 4b0b737..c17c6b9 100644
--- a/tests/functional/test_api.py
+++ b/tests/functional/test_api.py
@@ -516,16 +516,16 @@ def test_create_network_constraint_fail(client, session, admin_token):
             'first': '172.16.2.10',
             'last': '172.16.1.250'}
     response = post(client, '/api/networks', data=data, token=admin_token)
-    check_response_message(response, 'IntegrityError', 409)
+    check_response_message(response, 'IP address 172.16.2.10 is not in network 172.16.1.0/24', 422)
     # last not in prefix
     data = {'prefix': '172.16.1.0/24',
             'first': '172.16.1.10',
             'last': '172.16.5.250'}
     response = post(client, '/api/networks', data=data, token=admin_token)
-    check_response_message(response, 'IntegrityError', 409)
+    check_response_message(response, 'IP address 172.16.5.250 is not in network 172.16.1.0/24', 422)
     # first > last
     data = {'prefix': '172.16.1.0/24',
             'first': '172.16.1.10',
             'last': '172.16.1.9'}
     response = post(client, '/api/networks', data=data, token=admin_token)
-    check_response_message(response, 'IntegrityError', 409)
+    check_response_message(response, 'Last IP address 172.16.1.9 is less than the first address 172.16.1.10', 422)
-- 
GitLab