From a6811ba962d6c7d79bb283385610847a3783973c Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand <benjamin.bertrand@esss.se> Date: Thu, 9 May 2019 22:28:20 +0200 Subject: [PATCH] Allow users to delete their host - admin can delete any hosts - normal users can delete hosts they created Note that still only admin users can delete hosts via the API (more dangerous operation as a script could delete many hosts). JIRA INFRA-1018 #action In Progress --- app/models.py | 12 ++++++++ app/network/views.py | 10 ++++++- app/templates/network/view_host.html | 2 -- docs/network.rst | 2 +- tests/functional/test_web.py | 45 ++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/app/models.py b/app/models.py index 166074e..a9e75fd 100644 --- a/app/models.py +++ b/app/models.py @@ -324,6 +324,18 @@ class User(db.Model, UserMixin): in current_app.config["ALLOWED_SET_BOOT_PROFILE_DOMAINS"] ) + def can_delete_host(self, host): + """Return True if the user can delete the host + + - admin users can delete any host + - normal users must be creator of the host + - 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: + return True + return self.id == host.user.id + def favorite_attributes(self): """Return all user's favorite attributes""" favorites_list = [ diff --git a/app/network/views.py b/app/network/views.py index 6b469f3..60860d4 100644 --- a/app/network/views.py +++ b/app/network/views.py @@ -127,15 +127,23 @@ def create_host(): @bp.route("/hosts/delete", methods=["POST"]) -@login_groups_accepted("admin") +@login_required def delete_host(): host = models.Host.query.get_or_404(request.form["host_id"]) + if not current_user.can_delete_host(host): + abort(403) # Deleting the host will also delete all # associated interfaces due to the cascade delete option # defined on the model db.session.delete(host) db.session.commit() flash(f"Host {host.name} has been deleted", "success") + if host.device_type.name == "VirtualMachine": + flash( + "Note that the host was only removed from CSEntry. " + "To delete a running VM, please contact an administrator.", + "info", + ) return redirect(url_for("network.list_hosts")) diff --git a/app/templates/network/view_host.html b/app/templates/network/view_host.html index 97397c3..c46036c 100644 --- a/app/templates/network/view_host.html +++ b/app/templates/network/view_host.html @@ -27,7 +27,6 @@ <div class="col-sm-10"> {{ host.fqdn }} </div> - {% if current_user.is_admin %} <div class="col-sm-2 text-right"> <form method="POST" action="/network/hosts/delete"> <input id="host_id" name="host_id" type="hidden" value="{{ host.id }}"> @@ -35,7 +34,6 @@ "Are you sure you want to delete the host %s?" | format(host.name)) }} </form> </div> - {% endif %} </div> </dd> <dt class="col-sm-3">Device Type</dt> diff --git a/docs/network.rst b/docs/network.rst index c3a39ef..cb1255a 100644 --- a/docs/network.rst +++ b/docs/network.rst @@ -105,7 +105,7 @@ If you need more than one interface, you should go to the *Add interface* pane f Delete a host ------------- -Note that this is restricted to admin users. +Normal users can only delete hosts they created (admin can delete any hosts). To delete a host, just click on the **trash** icon next to the hostname on the *View host* page. .. image:: _static/network/view_host_delete.png diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py index a5030fc..bf7f787 100644 --- a/tests/functional/test_web.py +++ b/tests/functional/test_web.py @@ -37,6 +37,13 @@ def logged_rw_client(client): logout(client) +@pytest.fixture +def logged_admin_client(client): + login(client, "admin", "adminpasswd") + yield client + logout(client) + + @pytest.fixture def no_login_check_client(request, app): app.config["LOGIN_DISABLED"] = True @@ -497,3 +504,41 @@ def test_create_vm( f"/network/hosts/view/{vioc_lab.name}", data=form, follow_redirects=True ) check_vm_creation_response(response, success=False) + + +def test_delete_host_as_admin(logged_admin_client, host_factory, user_factory): + # admin can delete any host + admin = models.User.query.filter_by(username="admin").first() + user1 = user_factory(username="user1") + host1 = host_factory(name="host1", user=admin) + host2 = host_factory(name="host2", user=user1) + assert len(models.Host.query.all()) == 2 + response = logged_admin_client.post( + "/network/hosts/delete", data={"host_id": host1.id} + ) + assert response.status_code == 302 + response = logged_admin_client.post( + "/network/hosts/delete", data={"host_id": host2.id} + ) + assert response.status_code == 302 + assert len(models.Host.query.all()) == 0 + + +def test_delete_host_as_normal_user(logged_rw_client, host_factory, user_factory): + # a normal user can only delete its own hosts + user_rw = models.User.query.filter_by(username="user_rw").first() + user1 = user_factory(username="user1") + host1 = host_factory(name="host1", user=user_rw) + host2 = host_factory(name="host2", user=user1) + assert len(models.Host.query.all()) == 2 + # user_rw can delete its host + response = logged_rw_client.post( + "/network/hosts/delete", data={"host_id": host1.id} + ) + assert response.status_code == 302 + # user_rw can't delete host owned by user1 + response = logged_rw_client.post( + "/network/hosts/delete", data={"host_id": host2.id} + ) + assert response.status_code == 403 + assert len(models.Host.query.all()) == 1 -- GitLab