mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 12:46:49 +00:00
feat(operations): admit claims atomically; redact secrets
This commit is contained in:
parent
99b9aa2a3c
commit
fca4a5d91c
4 changed files with 227 additions and 0 deletions
67
operations/tests/test_claims.py
Normal file
67
operations/tests/test_claims.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""Admission writes claims only when nothing active conflicts, all or
|
||||
nothing; exclusive conflicts with any claim, shared only with exclusive."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from operations import claims
|
||||
from operations.models import Claim, Operation
|
||||
|
||||
S, X = Claim.SHARED, Claim.EXCLUSIVE
|
||||
|
||||
|
||||
def job(user, key="k"):
|
||||
return Operation(kind="test.noop", user=user, username=user.username, submission_key=key,
|
||||
resource="instance:u1", resource_label="c/vm")
|
||||
|
||||
|
||||
class AdmitTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create_user("claims_user", "c@example.com", "pw")
|
||||
|
||||
def test_shared_claims_coexist_and_exclusive_ones_do_not(self):
|
||||
claims.admit({"compute:1": S}, holder="a")
|
||||
claims.admit({"compute:1": S}, holder="b")
|
||||
with self.assertRaises(claims.Busy) as busy:
|
||||
claims.admit({"compute:1": X}, holder="c")
|
||||
self.assertEqual((busy.exception.resource, busy.exception.blocker), ("compute:1", None))
|
||||
|
||||
def test_a_refusal_writes_nothing(self):
|
||||
claims.admit({"instance:u1": X}, holder="a")
|
||||
with self.assertRaises(claims.Busy):
|
||||
claims.admit({"compute:1": S, "instance:u1": X}, holder="b")
|
||||
self.assertFalse(Claim.objects.filter(resource="compute:1").exists())
|
||||
|
||||
def test_a_job_and_its_claims_are_written_together(self):
|
||||
op = claims.admit_job(job(self.user), {"compute:1": S, "instance:u1": X}, existing=lambda: None)
|
||||
self.assertEqual(Operation.objects.get().pk, op.pk)
|
||||
self.assertEqual(set(Claim.objects.values_list("resource", "operation")),
|
||||
{("compute:1", op.pk), ("instance:u1", op.pk)})
|
||||
with self.assertRaises(claims.Busy) as busy:
|
||||
claims.admit({"instance:u1": S}, holder="b")
|
||||
self.assertEqual(busy.exception.blocker.pk, op.pk)
|
||||
|
||||
def test_an_existing_job_is_returned_before_conflicts_are_checked(self):
|
||||
first = claims.admit_job(job(self.user), {"instance:u1": X}, existing=lambda: None)
|
||||
again = claims.admit_job(job(self.user, key="k2"), {"instance:u1": X}, existing=lambda: first)
|
||||
self.assertEqual(again.pk, first.pk)
|
||||
self.assertEqual(Operation.objects.count(), 1)
|
||||
|
||||
def test_a_refused_job_is_not_saved(self):
|
||||
claims.admit({"instance:u1": X}, holder="a")
|
||||
with self.assertRaises(claims.Busy):
|
||||
claims.admit_job(job(self.user), {"instance:u1": X}, existing=lambda: None)
|
||||
self.assertFalse(Operation.objects.exists())
|
||||
|
||||
def test_an_expired_transient_claim_does_not_block(self):
|
||||
Claim.objects.create(resource="instance:u1", mode=X, holder="dead:1:1",
|
||||
expires_at=timezone.now() - timedelta(seconds=1))
|
||||
claims.admit({"instance:u1": X}, holder="b")
|
||||
self.assertEqual(list(Claim.objects.values_list("holder", flat=True)), ["b"])
|
||||
|
||||
def test_keys_are_canonical(self):
|
||||
self.assertEqual(claims.compute_key(3), "compute:3")
|
||||
self.assertEqual(claims.instance_key("u-1"), "instance:u-1")
|
||||
36
operations/tests/test_redact.py
Normal file
36
operations/tests/test_redact.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from django.test import SimpleTestCase
|
||||
|
||||
from operations import utils
|
||||
|
||||
|
||||
class RedactTestCase(SimpleTestCase):
|
||||
def test_credentials_in_text_are_hidden(self):
|
||||
text = ("Cannot connect to qemu+tcp://admin:s3cret@192.0.2.5/system: "
|
||||
"<graphics passwd='vncpw'/> password=hunter2 auth_password: abc console_pass=cp1 "
|
||||
'password="quoted1" token: \'quoted2\' <passphrase>pp</passphrase>')
|
||||
safe = utils.safe_text(text)
|
||||
for secret in ("s3cret", "vncpw", "hunter2", "abc", "quoted1", "quoted2", "pp<", "cp1"):
|
||||
self.assertNotIn(secret, safe)
|
||||
self.assertIn("192.0.2.5", safe)
|
||||
|
||||
def test_structured_and_multiline_secrets_are_hidden(self):
|
||||
safe = utils.safe_text('{"password": "hunter2", \'token\': \'t0k\'} password="line1\nline2"')
|
||||
for secret in ("hunter2", "t0k", "line1", "line2"):
|
||||
self.assertNotIn(secret, safe)
|
||||
|
||||
def test_escaped_quotes_and_nested_markup_are_hidden(self):
|
||||
safe = utils.safe_text('{"password": "abc\\"def"} <passphrase><![CDATA[x</literal>y]]></passphrase>')
|
||||
for secret in ("abc", "def", "x<", "y]]"):
|
||||
self.assertNotIn(secret, safe)
|
||||
|
||||
def test_unterminated_and_pathological_quotes_are_handled(self):
|
||||
self.assertNotIn("hunter2", utils.safe_text('failed: password="hunter2'))
|
||||
utils.safe_text('password="' + "\\" * 5000)
|
||||
|
||||
def test_secret_looking_params_are_refused(self):
|
||||
for key in ("password", "root_pass", "console_passwd", "secret", "private_key"):
|
||||
with self.subTest(key), self.assertRaises(ValueError):
|
||||
utils.check_params({"name": "x", key: "v"})
|
||||
with self.assertRaises(ValueError):
|
||||
utils.check_params({"config": [{"password": "v"}]})
|
||||
utils.check_params({"instance": 1, "name": "snap"})
|
||||
Loading…
Add table
Add a link
Reference in a new issue