1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

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
This commit is contained in:
catborise 2026-10-26 11:25:36 +03:00
parent 6c2704f97d
commit b619cf3cd4
4 changed files with 81 additions and 8 deletions

View file

@ -60,9 +60,9 @@
<tr>
<td>{{ forloop.counter }}</td>
<td><a href="{% url 'instances:instance' inst.instance.id %}">{{ inst.instance.name }}</a></td>
<td>{{ inst.is_vnc }}</td>
<td>{{ inst.is_change }}</td>
<td>{{ inst.is_delete }}</td>
<td>{% if inst.is_vnc %}{% bs_icon 'check' %}{% endif %}</td>
<td>{% if inst.is_change %}{% bs_icon 'check' %}{% endif %}</td>
<td>{% if inst.is_delete %}{% bs_icon 'check' %}{% endif %}</td>
<td style="width:5px;">
<a href="{% url 'accounts:user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
{% bs_icon 'pencil' %}
@ -74,6 +74,8 @@
</a>
</td>
</tr>
{% empty %}
<tr><td colspan="7" class="text-body-secondary">{% trans "No VMs." %}</td></tr>
{% endfor %}
</tbody>
</table>
@ -86,6 +88,7 @@
<tr>
<th scope="col">{% trans "Key name" %}</th>
<th scope="col">{% trans "Public key" %}</th>
<th scope="col"><span class="visually-hidden">{% trans "Action" %}</span></th>
</tr>
</thead>
<tbody>
@ -93,6 +96,11 @@
<tr>
<td>{{ publickey.keyname }}</td>
<td title="{{ publickey.keypublic }}">{{ publickey.keypublic|truncatechars:64 }}</td>
<td style="width:5px;">
<a class="btn btn-sm btn-secondary" href="{% url 'accounts:account_ssh_key_delete' user.id publickey.id %}" title="{% trans "Delete" %}">
{% bs_icon 'trash' %}
</a>
</td>
</tr>
{% endfor %}
</tbody>

View file

@ -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, "<td>True</td>", html=True)
self.assertNotContains(response, "<td>False</td>", 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())

View file

@ -14,6 +14,7 @@ urlpatterns = [
path("profile/<int:user_id>/", views.account, name="account"),
path("profile/<int:user_id>/api_token/<str:token_key>/delete/", views.api_token_delete,
name="account_api_token_delete"),
path("profile/<int:user_id>/ssh_key/<int:pk>/delete/", views.ssh_key_delete, name="account_ssh_key_delete"),
path("change_password/", views.change_password, name="change_password"),
path(
"user_instance/create/<int:user_id>/",

View file

@ -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."),
},