From 16268c465c8d814b6449167468a3469b88d1e546 Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Thu, 19 Dec 2019 15:53:36 +0100
Subject: [PATCH] Prevent supernet overlapping

JIRA INFRA-1627
---
 app/models.py                   | 12 ++++++++++++
 app/network/views.py            | 22 ++++++++++++++--------
 tests/functional/test_models.py | 12 ++++++++++++
 tests/functional/test_web.py    | 16 ++++++++++++++++
 4 files changed, 54 insertions(+), 8 deletions(-)

diff --git a/app/models.py b/app/models.py
index f2b78d9..f5a2601 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1615,6 +1615,18 @@ class NetworkScope(CreatedMixin, db.Model):
     def __str__(self):
         return str(self.name)
 
+    @validates("supernet")
+    def validate_supernet(self, key, supernet):
+        """Ensure the supernet doesn't overlap existing supernets"""
+        supernet_address = ipaddress.ip_network(supernet)
+        existing_scopes = NetworkScope.query.all()
+        for existing_scope in existing_scopes:
+            if supernet_address.overlaps(existing_scope.supernet_ip):
+                raise ValidationError(
+                    f"{supernet} overlaps {existing_scope} ({existing_scope.supernet_ip})"
+                )
+        return supernet
+
     @validates("networks")
     def validate_networks(self, key, network):
         """Ensure the network is included in the supernet and doesn't overlap
diff --git a/app/network/views.py b/app/network/views.py
index f16ecf7..d8cf18c 100644
--- a/app/network/views.py
+++ b/app/network/views.py
@@ -588,14 +588,20 @@ def view_scope(name):
 def create_scope():
     form = NetworkScopeForm()
     if form.validate_on_submit():
-        scope = models.NetworkScope(
-            name=form.name.data,
-            description=form.description.data or None,
-            first_vlan=form.first_vlan.data,
-            last_vlan=form.last_vlan.data,
-            supernet=form.supernet.data,
-            domain=models.Domain.query.get(form.domain_id.data),
-        )
+        try:
+            scope = models.NetworkScope(
+                name=form.name.data,
+                description=form.description.data or None,
+                first_vlan=form.first_vlan.data,
+                last_vlan=form.last_vlan.data,
+                supernet=form.supernet.data,
+                domain=models.Domain.query.get(form.domain_id.data),
+            )
+        except ValidationError as e:
+            # Check for error raised by model validation (not implemented in form vaildation)
+            current_app.logger.warning(f"{e}")
+            flash(f"{e}", "error")
+            return render_template("network/create_scope.html", form=form)
         current_app.logger.debug(f"Trying to create: {scope!r}")
         db.session.add(scope)
         try:
diff --git a/tests/functional/test_models.py b/tests/functional/test_models.py
index fe5daff..31bf839 100644
--- a/tests/functional/test_models.py
+++ b/tests/functional/test_models.py
@@ -919,3 +919,15 @@ def test_scope_available_subnets(network_scope_factory, network_factory):
     network_factory(vlan_id=3802, address="172.30.238.64/26", scope=scope)
     expected3 = [subnet for subnet in expected2 if subnet != "172.30.238.0/24"]
     assert scope.available_subnets(24) == expected3
+
+
+@pytest.mark.parametrize("address", ("172.30.0.0/22", "172.30.244.0/22"))
+def test_network_scope_overlapping(address, network_scope_factory):
+    scope = network_scope_factory(
+        first_vlan=3800, last_vlan=4000, supernet="172.30.0.0/16"
+    )
+    with pytest.raises(ValidationError) as excinfo:
+        network_scope_factory(first_vlan=2000, last_vlan=2200, supernet=address)
+    assert f"{address} overlaps {scope.name} ({scope.supernet_ip})" in str(
+        excinfo.value
+    )
diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py
index 5966191..0b08160 100644
--- a/tests/functional/test_web.py
+++ b/tests/functional/test_web.py
@@ -788,3 +788,19 @@ def test_create_network_overlapping(
     response = logged_admin_client.post("/network/networks/create", data=form)
     assert response.status_code == 200
     assert b"172.30.1.0/24 overlaps network1 (172.30.0.0/23)" in response.data
+
+
+def test_create_network_scope_overlapping(network_scope_factory, logged_admin_client):
+    scope1 = network_scope_factory(
+        name="scope1", first_vlan=3800, last_vlan=4000, supernet="172.30.0.0/16"
+    )
+    form = {
+        "name": "scope2",
+        "first_vlan": 200,
+        "last_vlan": 500,
+        "supernet": "172.30.200.0/22",
+        "domain_id": scope1.domain_id,
+    }
+    response = logged_admin_client.post("/network/scopes/create", data=form)
+    assert response.status_code == 200
+    assert b"172.30.200.0/22 overlaps scope1 (172.30.0.0/16)" in response.data
-- 
GitLab