1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/virtsecrets/tests/test_create_secret.py
catborise a95ec40f69 fix(virtsecrets): a secret in use is not deleted, and a password is stored as typed
- A secret is in use when a VM names it in a disk, a host device or its
  vTPM, or a pool in its source, by UUID or by its usage within the usage
  type, as they run and as saved. The vTPM, host devices and pools' saved
  definitions were not checked before.
- A Ceph key is taken in base64, as `ceph auth get-key` prints it; any other
  value is a password or passphrase stored as typed, spaces included.
- A new secret takes its value at once; a value refused leaves no secret.
- TLS and vTPM secrets can be created; the usage has no length limit.
- Every POST redirects; the list is read once and only with its users; a
  secret, VM or pool gone meanwhile is left out.
- domain_trees() is shared by the in-use checks of secrets, network filters,
  networks and pools; the storage page's Ceph secret picker reads the same
  listing.
2026-10-29 10:10:43 +03:00

100 lines
5.1 KiB
Python

"""A secret's usage goes into its XML as text, whatever it holds. Its value
is a Ceph key in base64, as `ceph auth get-key` prints it, or a password or
passphrase as typed; a new secret takes its value at once, or none, and a
value refused leaves no secret behind."""
from unittest.mock import patch
import libvirt
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.urls import reverse
from lxml import etree, html
from virtsecrets.forms import AddSecret
from vrtManager.util import OperationError
from vrtManager.virtsecrets import wvmSecrets
def connection():
with patch("vrtManager.connection.connection_manager.get_connection") as get:
conn = wvmSecrets("host", "login", "pw", 1)
return conn, get.return_value
class CreateSecretTests(SimpleTestCase):
def test_the_usage_is_text_of_its_element(self):
for usage_type, element, usage in (
("volume", "volume", "/var/lib/libvirt/images/vm-1.qcow2"),
("iscsi", "target", "iqn.2026-10.test.wvc:lun"),
("ceph", "name", "client.libvirt secret"),
("tls", "name", "migration"),
("vtpm", "name", "VM web-1's vTPM"),
("volume", "volume", "/a'b<c>&d\""),
):
with self.subTest(usage_type=usage_type, usage=usage):
conn, wvm = connection()
conn.create_secret(usage_type, usage, private=True)
secret = etree.fromstring(wvm.secretDefineXML.call_args.args[0])
self.assertEqual((secret.get("ephemeral"), secret.get("private")), ("no", "yes"))
self.assertEqual(secret.find("usage").get("type"), usage_type)
self.assertEqual(secret.findtext(f"usage/{element}"), usage)
wvm.secretDefineXML.return_value.setValue.assert_not_called()
def test_a_new_secret_takes_its_value_at_once(self):
for usage_type, value, stored in (("ceph", "cHcA/w==", b"pw\x00\xff"), ("iscsi", " pass word ", b" pass word ")):
with self.subTest(usage_type=usage_type):
conn, wvm = connection()
conn.create_secret(usage_type, "lun", value)
wvm.secretDefineXML.return_value.setValue.assert_called_once_with(stored)
def test_a_value_refused_leaves_no_secret(self):
conn, wvm = connection()
with self.assertRaisesRegex(OperationError, "base64"):
conn.create_secret("ceph", "client.libvirt", "not base64!")
wvm.secretDefineXML.assert_not_called()
wvm.secretDefineXML.return_value.setValue.side_effect = libvirt.libvirtError("refused")
with self.assertRaises(libvirt.libvirtError):
conn.create_secret("iscsi", "lun", "pw")
wvm.secretDefineXML.return_value.undefine.assert_called_once_with()
class SecretValueTests(SimpleTestCase):
def set_value(self, usage_type, value):
conn, wvm = connection()
secret = wvm.secretLookupByUUIDString.return_value
secret.XMLDesc.return_value = f"<secret><usage type='{usage_type}'><name>x</name></usage></secret>"
conn.set_secret_value("uuid", value)
return secret.setValue
def test_a_ceph_key_is_stored_as_its_bytes_a_password_as_typed(self):
self.set_value("ceph", "cHcA/w==").assert_called_once_with(b"pw\x00\xff")
# a password that happens to be base64 is not decoded
self.set_value("iscsi", "cHcA/w==").assert_called_once_with(b"cHcA/w==")
self.set_value("vtpm", " pässword ").assert_called_once_with(" pässword ".encode())
def test_a_ceph_key_that_is_not_base64_is_refused_without_the_value(self):
for value in ("b'cHc='", "not base64!", "kéy"):
with self.subTest(value=value), self.assertRaises(OperationError) as refused:
self.set_value("ceph", value)
self.assertNotIn(value, str(refused.exception))
class CreateSecretFormTests(SimpleTestCase):
def test_a_password_keeps_its_spaces_and_the_flags_are_checkboxes(self):
form = AddSecret({"usage_type": "iscsi", "usage": "iqn.2026-10.test.wvc:lun", "value": " pw ", "private": "on"})
self.assertTrue(form.is_valid(), form.errors)
self.assertEqual((form.cleaned_data["value"], form.cleaned_data["ephemeral"], form.cleaned_data["private"]),
(" pw ", False, True))
class CreateSecretDialogTests(TestCase):
def test_the_dialog_offers_the_usage_types_the_form_takes(self):
compute = Compute.objects.create(name="secret-usage", hostname="localhost", login="", password="", type=4)
self.client.force_login(get_user_model().objects.create_superuser("usage", "usage@example.com", "pw"))
with patch("virtsecrets.views.wvmSecrets") as conn:
conn.return_value.get_secrets_info.return_value = []
conn.return_value.get_users.return_value = {}
response = self.client.get(reverse("virtsecrets", args=[compute.id]))
offered = html.fromstring(response.content).xpath("//select[@id='secret_usage_type']/option/@value")
self.assertEqual(offered, [value for value, _ in AddSecret.base_fields["usage_type"].choices])