ctdb-scripts: Improve update and listing code
[samba4-gss.git] / python / samba / tests / samba_tool / domain_kds_root_key.py
blob3a6613a14c0fee83203adb94d4229465eff79818
1 # Unix SMB/CIFS implementation.
3 # Tests for samba-tool commands for Key Distribution Services
5 # Copyright © Catalyst.Net Ltd. 2024
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import json
21 import os
22 import re
23 from datetime import datetime, timezone
25 from .base import SambaToolCmdTest
26 from samba.dcerpc import misc
28 from samba.nt_time import (nt_now,
29 NT_TICKS_PER_SEC,
30 nt_time_from_string,
31 string_from_nt_time)
33 from ldb import SCOPE_SUBTREE, Dn
35 from samba.tests.gkdi import create_root_key
38 HOST = "ldap://{DC_SERVER}".format(**os.environ)
39 CREDS = "-U{DC_USERNAME}%{DC_PASSWORD}".format(**os.environ)
40 SMBCONF = os.environ['SERVERCONFFILE']
42 # alice%Secret007
43 NON_ADMIN_CREDS = "-U{DOMAIN_USER}%{DOMAIN_USER_PASSWORD}".format(**os.environ)
45 TIMESTAMP_RE = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}\+00:00'
47 NOWISH = 'about now'
50 class KdsRootKeyTestsBase(SambaToolCmdTest):
51 @classmethod
52 def setUpClass(cls):
53 cls.samdb = cls.getSamDB("-H", HOST, CREDS)
54 dn = cls.samdb.get_config_basedn()
55 dn.add_child("CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services")
56 cls.root_key_base_dn = dn
58 # we'll add one for all tests to rely on -- but most will add
59 # their own.
60 super().setUpClass()
62 @classmethod
63 def _create_root_key_timediff(cls, create_diff=0, use_diff=0):
64 now = nt_now()
65 nt_create = now + create_diff * NT_TICKS_PER_SEC
66 nt_use = now + use_diff * NT_TICKS_PER_SEC
67 guid, dn = create_root_key(cls.samdb,
68 cls.root_key_base_dn,
69 current_nt_time=nt_create,
70 use_start_time=nt_use)
72 return guid, dn, nt_create, nt_use
74 def _create_root_key_timediff_cleanup(self, create_diff=0, use_diff=0):
75 """create a root key that will disappear when the test ends."""
76 guid, dn, nt_create, nt_use = self._create_root_key_timediff(
77 create_diff,
78 use_diff)
79 self.addCleanup(self.samdb.delete, dn)
80 return guid, dn, nt_create, nt_use
82 def _check_timestamp(self, isotimestamp, expected, range=10000):
83 """Check that a timestamp string matches an nt-time.
85 By default we give a millisecond of leeway, because the ISO
86 timestamp has less resolution than NT time (at most 6 decimal
87 digits for seconds).
88 """
90 t = nt_time_from_string(isotimestamp)
92 if expected is None:
93 # we don't know what we want, but at least it's a time!
94 return
96 if expected is NOWISH:
97 expected = nt_now()
98 range = 2.0 * NT_TICKS_PER_SEC
100 self.assertGreaterEqual(t, expected - range)
101 self.assertLessEqual(t, expected + range)
103 def _test_list_output_snippet(self, output,
104 guid=r'\b[0-9a-fA-F-]{36}\b',
105 created=None,
106 used_from=None,
107 verbose=False):
108 # name 1146a853-b604-75ac-5acc-4ef4f0530584
109 # created 2024-02-15T22:55:47.865576+00:00 (about 4 days ago)
110 # usable from 2024-02-15T22:55:47.865576+00:00 (about 4 days ago)
111 self.assertRegex(output, f"(?m)^name {guid}$")
113 m = re.search(f' created +({TIMESTAMP_RE})', output)
114 self.assertIsNotNone(m, "create timestamp not found")
115 create_timestamp = m.group(1)
116 self._check_timestamp(create_timestamp, created)
118 m = re.search(f' usable from +({TIMESTAMP_RE})', output)
119 self.assertIsNotNone(m, "usable from timestamp not found")
120 used_from_timestamp = m.group(1)
121 self._check_timestamp(used_from_timestamp, used_from)
123 if verbose:
124 dn = f"CN={guid},{self.root_key_base_dn}"
125 self.assertRegex(output, f"(?m)^ +dn +{dn}$")
126 self.assertRegex(output, r"(?m)^ +whenCreated +\d{14}.0Z$")
127 self.assertRegex(output, r"(?m)^ +whenChanged +\d{14}.0Z$")
128 self.assertRegex(output, r"(?m)^ +objectGUID +[0-9a-fA-F-]{36}$")
129 self.assertRegex(output, r"(?m)^ +msKds-KDFAlgorithmID \w+$")
130 self.assertRegex(output, r"(?m)^ +msKds-KDFParam \w+$")
131 self.assertRegex(output, r"(?m)^ +msKds-SecretAgreementAlgorithmID \w+$")
132 self.assertRegex(output, r"(?m)^ +msKds-PublicKeyLength \d+$")
133 self.assertRegex(output, r"(?m)^ +msKds-PrivateKeyLength \d+$")
134 self.assertRegex(output, r"(?m)^ +msKds-Version 1$")
135 self.assertRegex(output, rf"(?m)^ +msKds-DomainID [\w=, ]+{self.samdb.domain_dn()}$",
136 re.MULTILINE)
137 self.assertRegex(output, f"(?m)^ +cn +{guid}$") # same guid as name
139 def _test_list_output_json_snippet(self, snippet,
140 guid=r'\b[0-9a-fA-F-]{36}\b',
141 created=None,
142 used_from=None,
143 verbose=False):
145 _guid = lambda x: re.fullmatch(str(guid), x)
146 _hexstr = lambda x: re.fullmatch('[0-9a-fA-F]+', x)
147 _str = lambda x: isinstance(x, str)
148 _int = lambda x: isinstance(x, int)
150 # these next 2 will raise an assertion error on failure
151 def _used_from(x):
152 self._check_timestamp(x, used_from)
153 return True
155 def _created(x):
156 self._check_timestamp(x, used_from)
157 return True
159 validators = {
160 "cn": _guid,
161 "dn": _str,
162 "msKds-CreateTime": _created,
163 "msKds-DomainID": _str,
164 "msKds-KDFAlgorithmID": _str,
165 "msKds-KDFParam": _hexstr,
166 "msKds-PrivateKeyLength": _int,
167 "msKds-PublicKeyLength": _int,
168 "msKds-SecretAgreementAlgorithmID": _str,
169 "msKds-UseStartTime": _used_from,
170 "msKds-Version": _int,
171 "name": _guid,
172 "objectGUID": _str,
173 "whenChanged": _str,
174 "whenCreated": _str,
176 if verbose:
177 keys = validators
178 else:
179 keys = ["name", "msKds-UseStartTime", "msKds-CreateTime", "dn"]
181 self.assertEqual(len(keys), len(snippet), f"keys: {keys}, json: {snippet}")
183 for k in keys:
184 f = validators.get(k)
185 v = snippet.get(k)
186 self.assertTrue(f(v), f"{k} value {v} is wrong or malformed")
188 def _get_root_key_guids(self):
189 """Get the current list of GUIDs."""
190 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "--json",
191 "-H", HOST, CREDS)
192 return [x['name'] for x in json.loads(out)]
194 def _delete_root_key(self, guid):
195 dn = Dn(self.samdb, str(self.root_key_base_dn))
196 dn.add_child(f"CN={guid}")
197 self.samdb.delete(dn)
199 class KdsRootKeyTests(KdsRootKeyTestsBase):
201 @classmethod
202 def setUpClass(cls):
203 super().setUpClass()
204 # we'll add one for all tests to rely on.
205 cls.common_guid, cls.common_dn, cls.common_time, _ = cls._create_root_key_timediff()
206 cls.addClassCleanup(cls.samdb.delete, cls.common_dn)
208 def test_list(self):
210 """Do we list root keys with the expected info?"""
211 # For this test we also need to create some root keys.
212 guid, dn, _created, _used = self._create_root_key_timediff_cleanup()
214 result, out, err = self.runcmd("domain", "kds", "root-key", "list",
215 "-H", HOST, CREDS)
216 self.assertCmdSuccess(result, out, err)
217 self.assertEqual(err, "", "not expecting error messages")
219 # the output looks something like
221 #------------------------------------------------------------------------
222 # 2 root keys found.
224 # name d58e85d7-ffc4-d118-9c43-46fac38dea05
225 # created 2024-02-27T09:09:21.065486+00:00 (about 1 seconds ago)
226 # usable from 2024-02-27T09:09:21.065486+00:00 (about 1 seconds ago)
228 # name 8f3e6557-3ec9-cb84-2ecd-9e258df68e79
229 # created 2024-02-27T09:09:10.853494+00:00 (about 12 seconds ago)
230 # usable from 2024-02-27T09:09:10.853494+00:00 (about 12 seconds ago)
231 #-------------------------------------------------------------------------
233 # we want to check the various bits.
235 parts = out.rstrip().split("\n\n")
237 self.assertEqual(parts[0], f"{len(parts) - 1} root keys found.")
239 self._test_list_output_snippet(parts[1], guid,
240 created=NOWISH,
241 used_from=NOWISH)
243 guid2, dn2, _created2, _used2 = self._create_root_key_timediff_cleanup()
245 result, out, err = self.runcmd("domain", "kds", "root-key", "list",
246 "-H", HOST, CREDS)
247 self.assertCmdSuccess(result, out, err)
248 self.assertEqual(err, "", "not expecting error messages")
250 parts2 = out.rstrip().split("\n\n")
251 self.assertEqual(parts2[0], f"{len(parts)} root keys found.")
252 self.assertEqual(len(parts2), len(parts) + 1)
254 # we want to check that both of them are still there, in the
255 # right order, which is newest first.
256 self._test_list_output_snippet(parts2[1], guid2,
257 created=_created2,
258 used_from=_used2)
259 self._test_list_output_snippet(parts2[2], guid,
260 created=_created,
261 used_from=_used)
263 def test_list_verbose(self):
264 """Do we list root keys with the expected info?"""
265 guid, dn, _created, _used = self._create_root_key_timediff_cleanup()
267 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "-v",
268 "-H", HOST, CREDS)
270 self.assertCmdSuccess(result, out, err)
271 self.assertEqual(err, "", "not expecting error messages")
273 self._test_list_output_snippet(out, guid, verbose=True)
275 guid2, dn2, _created2, _used2 = self._create_root_key_timediff_cleanup()
277 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "-v",
278 "-H", HOST, CREDS)
279 self.assertCmdSuccess(result, out, err)
280 self.assertEqual(err, "", "not expecting error messages")
282 self._test_list_output_snippet(out, guid2, verbose=True)
284 # in case there are other root keys, we will test each piece
285 # using the default '[0-9a-fA-F-]{36}' guid-ish assertion.
287 pieces = out.rstrip().split('\n\n')
288 self.assertRegex(pieces[0], f'{len(pieces) - 1} root keys found.')
290 for piece in pieces[1:]:
291 self._test_list_output_snippet(piece, verbose=True)
293 def test_list_json(self):
294 """The JSON should be a list of dicts, containing the right things"""
295 guid, dn, _created, _used = self._create_root_key_timediff_cleanup()
297 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "-v", "--json",
298 "-H", HOST, CREDS)
299 self.assertCmdSuccess(result, out, err)
300 self.assertEqual(err, "", "not expecting error messages")
301 data = json.loads(out)
302 for snippet in data:
303 self._test_list_output_json_snippet(snippet, verbose=True)
305 # non-verbose
306 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "--json",
307 "-H", HOST, CREDS)
308 self.assertCmdSuccess(result, out, err)
309 self.assertEqual(err, "", "not expecting error messages")
310 data = json.loads(out)
311 for snippet in data:
312 self._test_list_output_json_snippet(snippet)
314 def test_view_key_that_exists(self):
315 guid, dn, _created, _used = self._create_root_key_timediff_cleanup()
316 cmd = ["domain", "kds", "root-key", "view",
317 "-H", HOST, CREDS,
318 "--name", str(guid)]
320 result, out, err = self.runcmd(*cmd)
321 self.assertCmdSuccess(result, out, err)
322 self.assertEqual(err, "", "not expecting error messages")
324 self._test_list_output_snippet(out, guid,
325 created=NOWISH,
326 used_from=NOWISH,
327 verbose=True)
329 def test_view_key_that_exists_json(self):
330 guid, dn, _created, _used = self._create_root_key_timediff_cleanup()
332 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
333 "--json",
334 "--name", str(guid),
335 "-H", HOST, CREDS)
336 self.assertCmdSuccess(result, out, err)
337 self.assertEqual(err, "", "not expecting error messages")
338 data = json.loads(out)
339 self._test_list_output_json_snippet(data, guid,
340 created=_created,
341 used_from=_used,
342 verbose=True)
345 def test_view_key_latest_json(self):
346 guid, dn, _created, _used = self._create_root_key_timediff_cleanup()
348 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
349 "--json",
350 "--latest",
351 "-H", HOST, CREDS)
352 self.assertCmdSuccess(result, out, err)
353 self.assertEqual(err, "", "not expecting error messages")
354 data = json.loads(out)
355 self._test_list_output_json_snippet(data, guid,
356 created=_created,
357 used_from=_used,
358 verbose=True)
360 # if we make a new now-ish key, it will be shown with
361 # --latest, forgetting the old one.
362 guid2, dn2, _created2, _used2 = self._create_root_key_timediff_cleanup()
364 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
365 "--json",
366 "--latest",
367 "-H", HOST, CREDS)
368 self.assertCmdSuccess(result, out, err)
369 self.assertEqual(err, "", "not expecting error messages")
370 data = json.loads(out)
371 self._test_list_output_json_snippet(data, guid2,
372 created=_created2,
373 used_from=_used2,
374 verbose=True)
376 # if we make a new backdated key, it will not be shown as
377 # latest, even though it was the most recently created.
379 self._create_root_key_timediff_cleanup(use_diff=-600)
381 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
382 "--json",
383 "--latest",
384 "-H", HOST, CREDS)
385 self.assertCmdSuccess(result, out, err)
386 self.assertEqual(err, "", "not expecting error messages")
387 data = json.loads(out)
388 self._test_list_output_json_snippet(data, guid2,
389 created=_created2,
390 used_from=_used2,
391 verbose=True)
393 # if we make a future-dated key, it will be shown as
394 # latest, even though it doesn't work yet.
396 guid3, dn3, _created3, _used3 = self._create_root_key_timediff_cleanup()
398 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
399 "--json",
400 "--latest",
401 "-H", HOST, CREDS)
402 self.assertCmdSuccess(result, out, err)
403 self.assertEqual(err, "", "not expecting error messages")
404 data = json.loads(out)
405 self._test_list_output_json_snippet(data, guid3,
406 created=_created3,
407 used_from=_used3,
408 verbose=True)
410 def test_view_non_existent(self):
411 """Viewing a non-existent GUID should fail, regardless of what exists."""
412 guid = misc.GUID(b'a' * 16)
414 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
415 "-H", HOST, CREDS,
416 "--name", str(guid))
417 self.assertCmdFail(result)
419 self.assertIn("ERROR: no such root key: 61616161-6161-6161-6161-616161616161",
420 err)
422 def test_view_non_existent_json(self):
423 guid = misc.GUID(b'a' * 16)
425 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
426 "-H", HOST, CREDS,
427 "--name", str(guid),
428 "--json")
429 self.assertCmdFail(result)
430 data = json.loads(out)
431 self.assertEqual(
432 data,
434 "message": f"no such root key: {guid}",
435 "status": "error"
438 def test_delete_non_existent(self):
439 """Deletion of non-existent guid should fail"""
440 guid = 'eeeeeeee-1111-eeee-1111-000000000000'
441 result, out, err = self.runcmd("domain", "kds", "root-key", "delete",
442 "-H", HOST, CREDS,
443 "--name", guid)
444 self.assertCmdFail(result)
445 self.assertIn(f"ERROR: no such root key: {guid}", err)
447 def test_delete_non_existent_json(self):
448 """Deletion of non-existent guids should fail"""
449 for guid in ('eeeeeeee-1111-eeee-1111-000000000000',
450 'foo',
451 ''):
452 result, out, err = self.runcmd("domain", "kds", "root-key", "delete",
453 "-H", HOST, CREDS,
454 "--name", guid,
455 "--json")
456 self.assertCmdFail(result)
457 data = json.loads(out)
458 self.assertEqual(
459 data,
461 "message": f"no such root key: {guid}",
462 "status": "error"
465 def test_create(self):
466 """does create work?"""
467 pre_create = self._get_root_key_guids()
469 result, out, err = self.runcmd("domain", "kds", "root-key", "create",
470 "-H", HOST, CREDS)
471 self.assertCmdSuccess(result, out, err)
472 self.assertEqual(err, "", "not expecting error messages")
474 post_create = self._get_root_key_guids()
476 new_guids = list(set(post_create) - set(pre_create))
477 gone_guids = set(pre_create) - set(post_create)
478 self.assertEqual(len(gone_guids), 0)
479 self.assertEqual(len(new_guids), 1)
480 self.assertRegex(out,
481 f"created root key {new_guids[0]}, usable from {TIMESTAMP_RE}")
482 self._delete_root_key(new_guids[0])
484 def test_create_json(self):
485 """does create work?"""
486 pre_create = self._get_root_key_guids()
488 result, out, err = self.runcmd("domain", "kds", "root-key", "create",
489 "-H", HOST, CREDS, "--json")
490 self.assertCmdSuccess(result, out, err)
491 self.assertEqual(err, "", "not expecting error messages")
493 post_create = self._get_root_key_guids()
495 new_guids = list(set(post_create) - set(pre_create))
496 gone_guids = set(pre_create) - set(post_create)
497 self.assertEqual(len(gone_guids), 0)
498 self.assertEqual(len(new_guids), 1)
499 data = json.loads(out)
500 self.assertEqual(data['dn'], f"CN={new_guids[0]},{self.root_key_base_dn}")
501 self.assertEqual(data['status'], 'OK')
502 self.assertRegex(data['message'],
503 f"created root key {new_guids[0]}, usable from {TIMESTAMP_RE}")
504 self._delete_root_key(new_guids[0])
506 def test_create_json_non_admin(self):
507 """can you create a root-key without being admin?"""
508 pre_create = self._get_root_key_guids()
510 result, out, err = self.runcmd("domain", "kds", "root-key", "create",
511 "-H", HOST, NON_ADMIN_CREDS, "--json")
512 self.assertCmdFail(result)
514 post_create = self._get_root_key_guids()
516 self.assertEqual(set(pre_create), set(post_create))
517 data = json.loads(out)
518 self.assertEqual(data['status'], 'error')
519 self.assertEqual(data['message'], 'User has insufficient access rights')
520 self.assertEqual(err, "", "not expecting stderr messages")
522 def test_create_json_1997(self):
523 """does create work?"""
524 pre_create = self._get_root_key_guids()
526 result, out, err = self.runcmd("domain", "kds", "root-key", "create",
527 "-H", HOST, CREDS, "--json",
528 "--use-start-time",
529 "1997-11-11T23:18:00.259810+00:00")
530 self.assertCmdSuccess(result, out, err)
531 self.assertEqual(err, "", "not expecting error messages")
533 post_create = self._get_root_key_guids()
535 new_guids = list(set(post_create) - set(pre_create))
536 gone_guids = set(pre_create) - set(post_create)
537 self.assertEqual(len(gone_guids), 0)
538 self.assertEqual(len(new_guids), 1)
539 data = json.loads(out)
540 self.assertEqual(data['dn'], f"CN={new_guids[0]},{self.root_key_base_dn}")
541 self.assertEqual(data['status'], 'OK')
542 self.assertRegex(data['message'],
543 f"created root key {new_guids[0]}, usable from 1997-11-1")
544 self._delete_root_key(new_guids[0])
546 def test_create_json_2197(self):
547 """does create work?"""
548 pre_create = self._get_root_key_guids()
550 result, out, err = self.runcmd("domain", "kds", "root-key", "create",
551 "-H", HOST, CREDS, "--json",
552 "--use-start-time",
553 "2197-11-11T23:18:00")
554 self.assertCmdSuccess(result, out, err)
555 self.assertEqual(err, "", "not expecting error messages")
557 post_create = self._get_root_key_guids()
559 new_guids = list(set(post_create) - set(pre_create))
560 gone_guids = set(pre_create) - set(post_create)
561 self.assertEqual(len(gone_guids), 0)
562 self.assertEqual(len(new_guids), 1)
563 data = json.loads(out)
564 self.assertEqual(data['dn'], f"CN={new_guids[0]},{self.root_key_base_dn}")
565 self.assertEqual(data['status'], 'OK')
566 self.assertRegex(data['message'],
567 f"created root key {new_guids[0]}, usable from 2197-11-1")
568 self._delete_root_key(new_guids[0])
570 def test_create_future(self):
571 """does create work, with a use-start-time 500 seconds in the
572 future?"""
573 pre_create = self._get_root_key_guids()
574 now = nt_now()
575 later = now + 500 * NT_TICKS_PER_SEC
576 timestamp = string_from_nt_time(later)
578 result, out, err = self.runcmd("domain", "kds", "root-key", "create",
579 "-H", HOST, CREDS, "--json",
580 "--use-start-time", timestamp)
582 self.assertCmdSuccess(result, out, err)
583 self.assertEqual(err, "", "not expecting error messages")
585 post_create = self._get_root_key_guids()
587 new_guids = list(set(post_create) - set(pre_create))
588 gone_guids = set(pre_create) - set(post_create)
589 self.assertEqual(len(gone_guids), 0)
590 self.assertEqual(len(new_guids), 1)
591 data = json.loads(out)
592 self.assertEqual(data['dn'], f"CN={new_guids[0]},{self.root_key_base_dn}")
593 self.assertEqual(data['status'], 'OK')
594 self.assertRegex(data['message'],
595 f"created root key {new_guids[0]}, usable from {timestamp[:-10]}")
596 self._delete_root_key(new_guids[0])
598 def test_delete(self):
599 """does delete work?"""
600 # make one to delete, and get the list as JSON
601 _guid, dn, _created, _used = self._create_root_key_timediff()
602 guid = str(_guid)
604 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "--json",
605 "-H", HOST, CREDS)
606 pre_delete = json.loads(out)
608 result, out, err = self.runcmd("domain", "kds", "root-key", "delete",
609 "-H", HOST, CREDS,
610 "--name", guid)
611 self.assertCmdSuccess(result, out, err)
612 self.assertEqual(err, "", "not expecting error messages")
613 self.assertEqual(out, f"deleted root key {guid}\n")
615 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "--json",
616 "-H", HOST, CREDS)
617 post_delete = json.loads(out)
619 self.assertEqual(len(pre_delete), len(post_delete) + 1)
621 post_names = [x['name'] for x in post_delete]
622 pre_names = [x['name'] for x in pre_delete]
624 self.assertIn(guid, pre_names)
625 self.assertNotIn(guid, post_names)
627 def test_delete_json(self):
628 """does delete --json work?"""
629 _guid, dn, _created, _used = self._create_root_key_timediff()
630 guid = str(_guid)
632 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "--json",
633 "-H", HOST, CREDS)
634 pre_delete = json.loads(out)
636 result, out, err = self.runcmd("domain", "kds", "root-key", "delete",
637 "-H", HOST, CREDS, "--json",
638 "--name", guid)
640 self.assertCmdSuccess(result, out, err)
641 self.assertEqual(err, "", "not expecting error messages")
642 data = json.loads(out)
643 self.assertEqual(
644 data,
646 "message": f"deleted root key {guid}",
647 "status": "error"
650 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "--json",
651 "-H", HOST, CREDS)
652 post_delete = json.loads(out)
654 self.assertEqual(len(pre_delete), len(post_delete) + 1)
656 post_names = [x['name'] for x in post_delete]
657 pre_names = [x['name'] for x in pre_delete]
659 self.assertIn(guid, pre_names)
660 self.assertNotIn(guid, post_names)
662 def test_delete_non_admin(self):
663 """does delete as non-admin fail?"""
664 # make one to delete, and get the list as JSON
665 _guid, dn, _created, _used = self._create_root_key_timediff()
666 guid = str(_guid)
668 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "--json",
669 "-H", HOST, CREDS)
670 pre_delete = json.loads(out)
672 result, out, err = self.runcmd("domain", "kds", "root-key", "delete",
673 "-H", HOST, NON_ADMIN_CREDS,
674 "--name", guid)
675 self.assertCmdFail(result)
676 self.assertIn(f"ERROR: no such root key: {guid}", err)
678 # a bad guid should be just like a good guid
679 guid2 = 'eeeeeeee-1111-eeee-1111-000000000000'
680 result, out2, err2 = self.runcmd("domain", "kds", "root-key", "delete",
681 "-H", HOST, NON_ADMIN_CREDS,
682 "--name", guid2)
683 self.assertCmdFail(result)
684 self.assertIn(f"ERROR: no such root key: {guid2}", err2)
686 result, out, err = self.runcmd("domain", "kds", "root-key", "list", "--json",
687 "-H", HOST, CREDS)
688 post_delete = json.loads(out)
690 self.assertEqual(len(pre_delete), len(post_delete))
692 post_names = [x['name'] for x in post_delete]
693 pre_names = [x['name'] for x in pre_delete]
695 self.assertIn(guid, pre_names)
696 self.assertIn(guid, post_names)
698 def test_list_non_admin(self):
699 """There are root keys, but non-admins can't see them"""
700 result, out, err = self.runcmd("domain", "kds", "root-key", "list",
701 "-H", HOST, NON_ADMIN_CREDS)
702 self.assertCmdSuccess(result, out, err)
703 self.assertEqual(err, "", "not expecting error messages")
704 self.assertEqual(out, "no root keys found.\n")
706 def test_list_json_non_admin(self):
707 """Insufficient rights should look like an empty list."""
708 # this is a copy of the KdsNoRootKeyTests test below --
709 # non-admin should look exactly like an empty list.
710 for extra in ([], ["-v"]):
711 result, out, err = self.runcmd("domain", "kds", "root-key", "list",
712 "-H", HOST, NON_ADMIN_CREDS, "--json", *extra)
713 self.assertCmdSuccess(result, out, err)
714 self.assertEqual(err, "", "not expecting error messages")
715 data = json.loads(out)
716 self.assertEqual(data, [])
718 def test_view_key_non_admin(self):
719 """should not appear to non-admin"""
720 guid, dn, _created, _used = self._create_root_key_timediff_cleanup()
722 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
723 "--json",
724 "--name", str(guid),
725 "-H", HOST, NON_ADMIN_CREDS)
726 self.assertCmdFail(result)
727 self.assertEqual(err, "", "not expecting error messages")
728 data = json.loads(out)
729 data = json.loads(out)
730 self.assertEqual(
731 data,
733 "message": f"no such root key: {guid}",
734 "status": "error"
738 class KdsNoRootKeyTests(KdsRootKeyTestsBase):
739 """Here we test the case were there are no root keys, which we need to
740 ensure by deleting any that are there.
743 @classmethod
744 def setUpClass(cls):
745 super().setUpClass()
746 # We delete all the root keys, and add one back at the end,
747 # in case other tests want there to be one.
748 res = cls.samdb.search(cls.root_key_base_dn,
749 scope=SCOPE_SUBTREE,
750 expression="(objectClass = msKds-ProvRootKey)")
752 for msg in res:
753 cls.samdb.delete(msg.dn)
755 cls.addClassCleanup(cls.samdb.new_gkdi_root_key)
757 def test_list_empty(self):
758 """Check the message when there are no root keys"""
759 result, out, err = self.runcmd("domain", "kds", "root-key", "list",
760 "-H", HOST, CREDS)
761 self.assertCmdSuccess(result, out, err)
762 self.assertEqual(err, "", "not expecting error messages")
763 self.assertEqual(out, "no root keys found.\n")
765 def test_list_empty_json(self):
766 """The JSON should be an empty list when there are no root keys"""
767 # verbose flag makes no difference here.
768 for extra in ([], ["-v"]):
769 result, out, err = self.runcmd("domain", "kds", "root-key", "list",
770 "-H", HOST, CREDS, "--json", *extra)
771 self.assertCmdSuccess(result, out, err)
772 self.assertEqual(err, "", "not expecting error messages")
773 data = json.loads(out)
774 self.assertEqual(data, [])
776 def test_list_empty_json_non_admin(self):
777 """Insufficient rights should look like an empty list."""
778 # verbose flag makes no difference here.
779 for extra in ([], ["-v"]):
780 result, out, err = self.runcmd("domain", "kds", "root-key", "list",
781 "-H", HOST, NON_ADMIN_CREDS, "--json", *extra)
782 self.assertCmdSuccess(result, out, err)
783 self.assertEqual(err, "", "not expecting error messages")
784 data = json.loads(out)
785 self.assertEqual(data, [])
787 def test_view_latest_non_existent(self):
788 """With no root keys, --latest should return an error"""
790 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
791 "-H", HOST, CREDS,
792 "--latest")
794 self.assertEqual(err, "ERROR: no root keys found\n")
795 self.assertCmdFail(result)
797 def test_view_latest_non_existent_json(self):
798 """With no root keys, --latest should return an error"""
800 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
801 "-H", HOST, CREDS,
802 "--json", "--latest")
803 self.assertCmdFail(result)
804 data = json.loads(out)
805 self.assertEqual(
806 data,
808 "message": "no root keys found",
809 "status": "error"
812 def test_view_non_existent(self):
813 """Viewing a non-existent GUID should fail, regardless of what exists."""
814 guid = misc.GUID(b'b' * 16)
816 result, out, err = self.runcmd("domain", "kds", "root-key", "view",
817 "-H", HOST, CREDS,
818 "--name", str(guid))
819 self.assertCmdFail(result)
821 self.assertIn("ERROR: no such root key: 62626262-6262-6262-6262-626262626262",
822 err)