From 8ac53fd5411b637473753e91108405b1ea36db76 Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Wed, 4 Dec 2019 13:52:58 +0100
Subject: [PATCH] Allow users to edit host with no interface

JIRA INFRA-1604 #action In Progress
---
 app/models.py                   |  4 +--
 app/network/views.py            |  2 +-
 tests/functional/test_api.py    | 32 ++++++++++++++++++++++++
 tests/functional/test_models.py |  2 +-
 tests/functional/test_web.py    | 43 +++++++++++++++++++++++++++++++++
 5 files changed, 79 insertions(+), 4 deletions(-)

diff --git a/app/models.py b/app/models.py
index 87be9fc..dd0413e 100644
--- a/app/models.py
+++ b/app/models.py
@@ -264,9 +264,9 @@ class User(db.Model, UserMixin):
         - LOGIN_DISABLED can be set to True to turn off authentication check when testing.
           In this case, this function always returns True.
         """
-        if current_app.config.get("LOGIN_DISABLED") or self.is_admin:
+        if current_app.config.get("LOGIN_DISABLED") or self.is_admin or network is None:
             return True
-        if network is None or network.admin_only:
+        if network.admin_only:
             # True is already returned for admin users
             return False
         return str(network.scope) in self.csentry_network_scopes
diff --git a/app/network/views.py b/app/network/views.py
index e759400..a849720 100644
--- a/app/network/views.py
+++ b/app/network/views.py
@@ -285,7 +285,7 @@ def create_interface(hostname):
     form = InterfaceForm(
         request.form, host_id=host.id, interface_name=host.name, random_mac=random_mac
     )
-    if not current_user.is_admin:
+    if not current_user.is_admin and host.main_network is not None:
         # Restrict the networks to the same network scope as the main interface
         form.network_id.choices = [
             (str(network.id), network.vlan_name)
diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py
index c5988c9..316c1dc 100644
--- a/tests/functional/test_api.py
+++ b/tests/functional/test_api.py
@@ -135,6 +135,11 @@ def user_token(client):
     return get_token(client, "user_rw", "userrw")
 
 
+@pytest.fixture()
+def user_prod_token(client):
+    return get_token(client, "user_prod", "userprod")
+
+
 @pytest.fixture()
 def consultant_token(client):
     return get_token(client, "consultant", "consultantpwd")
@@ -1207,6 +1212,33 @@ def test_create_interface_ip_not_in_range_as_admin(
     assert response.status_code == 201
 
 
+def test_normal_user_can_create_interface_on_empty_host(
+    client, host, network_scope_factory, network_factory, user_prod_token
+):
+    scope = network_scope_factory(name="ProdNetworks")
+    network = network_factory(
+        address="192.168.1.0/24",
+        first_ip="192.168.1.10",
+        last_ip="192.168.1.250",
+        scope=scope,
+    )
+    data = {
+        "network": network.vlan_name,
+        "ip": "192.168.1.20",
+        "name": host.name,
+        "host": host.name,
+    }
+    response = post(
+        client, f"{API_URL}/network/interfaces", data=data, token=user_prod_token
+    )
+    assert response.status_code == 201
+    assert response.get_json()["network"] == network.vlan_name
+    assert response.get_json()["ip"] == "192.168.1.20"
+    assert response.get_json()["name"] == host.name
+    # This is the main interface
+    assert response.get_json()["is_main"]
+
+
 def test_delete_interface_invalid_credentials(client, interface_factory, user_token):
     interface1 = interface_factory()
     response = delete(
diff --git a/tests/functional/test_models.py b/tests/functional/test_models.py
index 377a1dc..0d7e45e 100644
--- a/tests/functional/test_models.py
+++ b/tests/functional/test_models.py
@@ -63,7 +63,7 @@ def test_user_has_access_to_network(
     assert user.has_access_to_network(network_prod)
     assert user.has_access_to_network(network_lab)
     assert not user.has_access_to_network(network_lab_admin)
-    assert not user.has_access_to_network(None)
+    assert user.has_access_to_network(None)
     user = user_factory(groups=["foo", "CSEntry Lab"])
     assert not user.has_access_to_network(network_prod)
     assert user.has_access_to_network(network_lab)
diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py
index c4bdda2..8827f2d 100644
--- a/tests/functional/test_web.py
+++ b/tests/functional/test_web.py
@@ -438,6 +438,49 @@ def test_create_interface(
     assert interface.host == host
 
 
+def test_add_interface_to_empty_host(
+    client, host_factory, network_scope_factory, network_factory,
+):
+    host = host_factory(name="myhost")
+    scope = network_scope_factory(name="ProdNetworks")
+    network = network_factory(
+        address="192.168.2.0/24",
+        first_ip="192.168.2.10",
+        last_ip="192.168.2.250",
+        scope=scope,
+    )
+    name = host.name
+    ip = "192.168.2.11"
+    mac = "02:42:42:46:3c:75"
+    form = {
+        "host_id": host.id,
+        "interface_name": name,
+        "network_id": network.id,
+        "random_mac": False,
+        "ip": ip,
+        "mac": mac,
+        "cnames_string": "",
+    }
+    # user_lab doesn't have permissions for the network domain: prod.example.org
+    # form validation will fail because the network_id won't be in the choices
+    login(client, "user_lab", "userlab")
+    response = client.post(f"/network/interfaces/create/{host.name}", data=form)
+    assert response.status_code == 200
+    # The host wasn't created
+    assert models.Interface.query.filter_by(name=name).first() is None
+    logout(client)
+    # Success with user_prod user
+    login(client, "user_prod", "userprod")
+    response = client.post(f"/network/interfaces/create/{host.name}", data=form)
+    assert response.status_code == 302
+    # The interface was created
+    interface = models.Interface.query.filter_by(name=name).first()
+    assert interface.ip == ip
+    assert interface.mac == mac
+    assert interface.name == name
+    assert interface.host == host
+
+
 def check_vm_creation_response(response, success=True):
     assert response.status_code == 200
     assert (b"View task" in response.data) is success
-- 
GitLab