From b619cf3cd4e636c5810acf464bffe4449983f86e Mon Sep 17 00:00:00 2001 From: catborise Date: Mon, 26 Oct 2026 11:25:36 +0300 Subject: [PATCH] fix(accounts): a user's page shows permissions as marks, says when there are no VMs, deletes keys On a superuser's page of a user the VM permissions read True/False; they are now check marks as on the Users list. A user without VMs gets "No VMs." instead of an empty table. A superuser can delete one of the user's SSH keys, as with API tokens: the same view takes the user, the row names whose key it was. Closes #218 --- accounts/templates/account.html | 14 ++++++-- accounts/tests/test_account_page.py | 53 +++++++++++++++++++++++++++++ accounts/urls.py | 1 + accounts/views.py | 21 +++++++++--- 4 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 accounts/tests/test_account_page.py diff --git a/accounts/templates/account.html b/accounts/templates/account.html index 083fe494..2becbb6d 100644 --- a/accounts/templates/account.html +++ b/accounts/templates/account.html @@ -60,9 +60,9 @@ {{ forloop.counter }} {{ inst.instance.name }} - {{ inst.is_vnc }} - {{ inst.is_change }} - {{ inst.is_delete }} + {% if inst.is_vnc %}{% bs_icon 'check' %}{% endif %} + {% if inst.is_change %}{% bs_icon 'check' %}{% endif %} + {% if inst.is_delete %}{% bs_icon 'check' %}{% endif %} {% bs_icon 'pencil' %} @@ -74,6 +74,8 @@ + {% empty %} + {% trans "No VMs." %} {% endfor %} @@ -86,6 +88,7 @@ {% trans "Key name" %} {% trans "Public key" %} + {% trans "Action" %} @@ -93,6 +96,11 @@ {{ publickey.keyname }} {{ publickey.keypublic|truncatechars:64 }} + + + {% bs_icon 'trash' %} + + {% endfor %} diff --git a/accounts/tests/test_account_page.py b/accounts/tests/test_account_page.py new file mode 100644 index 00000000..039179c0 --- /dev/null +++ b/accounts/tests/test_account_page.py @@ -0,0 +1,53 @@ +"""A superuser's page of a user: the VMs and their permissions, the SSH keys.""" + +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse + +from accounts.models import UserInstance, UserSSHKey +from computes.models import Compute +from instances.models import Instance +from logs.models import Logs + +KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGbXMJlKTOlUOvwzdjDs3ac3ivHQoOxAUZEwi3WsJHfs alice@example" + + +class AccountPageTests(TestCase): + def setUp(self): + self.admin = User.objects.create_superuser("ap-admin", "ap@example.com", "pw") + self.alice = User.objects.create_user("alice", password="pw") + self.key = UserSSHKey.objects.create(user=self.alice, keyname="laptop", keypublic=KEY) + self.client.force_login(self.admin) + + def page(self): + return self.client.get(reverse("accounts:account", args=[self.alice.pk])) + + def test_the_permissions_are_marks_not_true_or_false(self): + compute = Compute.objects.create(name="ap-host", hostname="localhost", login="", password="", details="", type=4) + vm = Instance.objects.create(compute=compute, name="ap-vm", uuid="00000000-0000-0000-0000-000000000001") + UserInstance.objects.create(user=self.alice, instance=vm, is_vnc=True, is_change=False, is_delete=False) + response = self.page() + self.assertContains(response, "ap-vm") + self.assertNotContains(response, "True", html=True) + self.assertNotContains(response, "False", html=True) + + def test_a_user_without_vms_is_told_so(self): + self.assertContains(self.page(), "No VMs.") + + def test_a_superuser_deletes_a_users_key(self): + url = reverse("accounts:account_ssh_key_delete", args=[self.alice.pk, self.key.pk]) + self.assertContains(self.page(), f'href="{url}"') + self.assertContains(self.client.get(url), "laptop") + response = self.client.post(url) + self.assertRedirects(response, reverse("accounts:account", args=[self.alice.pk]) + "#public-keys", + fetch_redirect_response=False) + self.assertFalse(UserSSHKey.objects.exists()) + self.assertTrue(Logs.objects.filter(message="SSH key laptop of alice: delete").exists()) + + def test_only_a_superuser_and_only_that_users_key(self): + bob = User.objects.create_user("bob", password="pw") + self.assertEqual(self.client.post(reverse("accounts:account_ssh_key_delete", args=[bob.pk, self.key.pk])).status_code, 404) + self.client.force_login(bob) + url = reverse("accounts:account_ssh_key_delete", args=[self.alice.pk, self.key.pk]) + self.assertEqual(self.client.post(url).status_code, 403) + self.assertTrue(UserSSHKey.objects.exists()) diff --git a/accounts/urls.py b/accounts/urls.py index 7c009f62..a794af7c 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -14,6 +14,7 @@ urlpatterns = [ path("profile//", views.account, name="account"), path("profile//api_token//delete/", views.api_token_delete, name="account_api_token_delete"), + path("profile//ssh_key//delete/", views.ssh_key_delete, name="account_ssh_key_delete"), path("change_password/", views.change_password, name="change_password"), path( "user_instance/create//", diff --git a/accounts/views.py b/accounts/views.py index 52a87cb5..de0b6c50 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -124,19 +124,30 @@ def ssh_key_create(request): ) -def ssh_key_delete(request, pk): - ssh_key = get_object_or_404(UserSSHKey, pk=pk, user=request.user) +def ssh_key_delete(request, pk, user_id=None): + """Delete one of the user's own keys or, given user_id, a superuser + deletes one of that user's.""" + if user_id is None: + ssh_key = get_object_or_404(UserSSHKey, pk=pk, user=request.user) + subject = f"SSH key {ssh_key.keyname}" + back = reverse("accounts:profile") + elif request.user.is_superuser: + ssh_key = get_object_or_404(UserSSHKey, pk=pk, user_id=user_id) + subject = f"SSH key {ssh_key.keyname} of {ssh_key.user.username}" + back = reverse("accounts:account", args=[user_id]) + "#public-keys" + else: + raise PermissionDenied if request.method == "POST": - with audit(request, None, f"SSH key {ssh_key.keyname}", "delete"): + with audit(request, None, subject, "delete"): ssh_key.delete() messages.success(request, _("SSH key deleted")) - return redirect("accounts:profile") + return redirect(back) return render( request, "common/confirm_delete.html", { - "object": ssh_key, + "object": subject, "title": _("Delete SSH key"), "consequence": _("VMs that already have it keep it; VMs created from now on do not get it."), },