271
## Classes for commands.
273
# Abstract classes first
274
class Command(object):
275
"""Abstract class for commands"""
276
def run(self, mandos, clients):
277
"""Normal commands should implement run_on_one_client(), but
278
commands which want to operate on all clients at the same time
279
can override this run() method instead."""
281
for client in clients:
282
self.run_on_one_client(client)
284
class PrintCmd(Command):
285
"""Abstract class for commands printing client details"""
286
all_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
287
"Created", "Interval", "Host", "KeyID",
288
"Fingerprint", "CheckerRunning", "LastEnabled",
289
"ApprovalPending", "ApprovedByDefault",
290
"LastApprovalRequest", "ApprovalDelay",
291
"ApprovalDuration", "Checker", "ExtendedTimeout",
292
"Expires", "LastCheckerStatus")
293
def run(self, mandos, clients):
294
print(self.output(clients))
296
class PropertyCmd(Command):
297
"""Abstract class for Actions for setting one client property"""
298
def run_on_one_client(self, client):
299
"""Set the Client's D-Bus property"""
300
client.Set(client_interface, self.property, self.value_to_set,
301
dbus_interface=dbus.PROPERTIES_IFACE)
303
class ValueArgumentMixIn(object):
304
"""Mixin class for commands taking a value as argument"""
305
def __init__(self, value):
306
self.value_to_set = value
308
class MillisecondsValueArgumentMixIn(ValueArgumentMixIn):
309
"""Mixin class for commands taking a value argument as
312
def value_to_set(self):
315
def value_to_set(self, value):
316
"""When setting, convert value to a datetime.timedelta"""
317
self._vts = string_to_delta(value).total_seconds() * 1000
319
# Actual (non-abstract) command classes
321
class PrintTableCmd(PrintCmd):
322
def __init__(self, verbose=False):
323
self.verbose = verbose
325
def output(self, clients):
327
keywords = self.all_keywords
329
keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
330
return str(self.TableOfClients(clients.values(), keywords))
332
class TableOfClients(object):
335
"Enabled": "Enabled",
336
"Timeout": "Timeout",
337
"LastCheckedOK": "Last Successful Check",
338
"LastApprovalRequest": "Last Approval Request",
339
"Created": "Created",
340
"Interval": "Interval",
342
"Fingerprint": "Fingerprint",
344
"CheckerRunning": "Check Is Running",
345
"LastEnabled": "Last Enabled",
346
"ApprovalPending": "Approval Is Pending",
347
"ApprovedByDefault": "Approved By Default",
348
"ApprovalDelay": "Approval Delay",
349
"ApprovalDuration": "Approval Duration",
350
"Checker": "Checker",
351
"ExtendedTimeout": "Extended Timeout",
352
"Expires": "Expires",
353
"LastCheckerStatus": "Last Checker Status",
356
def __init__(self, clients, keywords, tableheaders=None):
357
self.clients = clients
358
self.keywords = keywords
359
if tableheaders is not None:
360
self.tableheaders = tableheaders
363
return "\n".join(self.rows())
365
if sys.version_info.major == 2:
366
__unicode__ = __str__
368
return str(self).encode(locale.getpreferredencoding())
371
format_string = self.row_formatting_string()
372
rows = [self.header_line(format_string)]
373
rows.extend(self.client_line(client, format_string)
374
for client in self.clients)
377
def row_formatting_string(self):
378
"Format string used to format table rows"
379
return " ".join("{{{key}:{width}}}".format(
380
width=max(len(self.tableheaders[key]),
381
*(len(self.string_from_client(client, key))
382
for client in self.clients)),
384
for key in self.keywords)
386
def string_from_client(self, client, key):
387
return self.valuetostring(client[key], key)
390
def valuetostring(value, keyword):
391
if isinstance(value, dbus.Boolean):
392
return "Yes" if value else "No"
393
if keyword in ("Timeout", "Interval", "ApprovalDelay",
394
"ApprovalDuration", "ExtendedTimeout"):
395
return milliseconds_to_string(value)
398
def header_line(self, format_string):
399
return format_string.format(**self.tableheaders)
401
def client_line(self, client, format_string):
402
return format_string.format(
403
**{key: self.string_from_client(client, key)
404
for key in self.keywords})
408
class DumpJSONCmd(PrintCmd):
409
def output(self, clients):
410
data = {client["Name"]:
411
{key: self.dbus_boolean_to_bool(client[key])
412
for key in self.all_keywords}
413
for client in clients.values()}
414
return json.dumps(data, indent=4, separators=(',', ': '))
416
def dbus_boolean_to_bool(value):
417
if isinstance(value, dbus.Boolean):
421
class IsEnabledCmd(Command):
422
def run_on_one_client(self, client):
423
if self.is_enabled(client):
426
def is_enabled(self, client):
427
return client.Get(client_interface, "Enabled",
428
dbus_interface=dbus.PROPERTIES_IFACE)
430
class RemoveCmd(Command):
431
def run_on_one_client(self, client):
432
self.mandos.RemoveClient(client.__dbus_object_path__)
434
class ApproveCmd(Command):
435
def run_on_one_client(self, client):
436
client.Approve(dbus.Boolean(True),
437
dbus_interface=client_interface)
439
class DenyCmd(Command):
440
def run_on_one_client(self, client):
441
client.Approve(dbus.Boolean(False),
442
dbus_interface=client_interface)
444
class EnableCmd(PropertyCmd):
446
value_to_set = dbus.Boolean(True)
448
class DisableCmd(PropertyCmd):
450
value_to_set = dbus.Boolean(False)
452
class BumpTimeoutCmd(PropertyCmd):
453
property = "LastCheckedOK"
456
class StartCheckerCmd(PropertyCmd):
457
property = "CheckerRunning"
458
value_to_set = dbus.Boolean(True)
460
class StopCheckerCmd(PropertyCmd):
461
property = "CheckerRunning"
462
value_to_set = dbus.Boolean(False)
464
class ApproveByDefaultCmd(PropertyCmd):
465
property = "ApprovedByDefault"
466
value_to_set = dbus.Boolean(True)
468
class DenyByDefaultCmd(PropertyCmd):
469
property = "ApprovedByDefault"
470
value_to_set = dbus.Boolean(False)
472
class SetCheckerCmd(PropertyCmd, ValueArgumentMixIn):
475
class SetHostCmd(PropertyCmd, ValueArgumentMixIn):
478
class SetSecretCmd(PropertyCmd, ValueArgumentMixIn):
481
class SetTimeoutCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
484
class SetExtendedTimeoutCmd(PropertyCmd,
485
MillisecondsValueArgumentMixIn):
486
property = "ExtendedTimeout"
488
class SetIntervalCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
489
property = "Interval"
491
class SetApprovalDelayCmd(PropertyCmd,
492
MillisecondsValueArgumentMixIn):
493
property = "ApprovalDelay"
495
class SetApprovalDurationCmd(PropertyCmd,
496
MillisecondsValueArgumentMixIn):
497
property = "ApprovalDuration"
236
def print_clients(clients, keywords):
237
def valuetostring(value, keyword):
238
if type(value) is dbus.Boolean:
239
return "Yes" if value else "No"
240
if keyword in ("Timeout", "Interval", "ApprovalDelay",
241
"ApprovalDuration", "ExtendedTimeout"):
242
return milliseconds_to_string(value)
245
# Create format string to print table rows
246
format_string = " ".join("{{{key}:{width}}}".format(
247
width=max(len(tablewords[key]),
248
max(len(valuetostring(client[key], key))
249
for client in clients)),
253
print(format_string.format(**tablewords))
254
for client in clients:
256
.format(**{key: valuetostring(client[key], key)
257
for key in keywords}))
499
260
def has_actions(options):
500
261
return any((options.enable,
699
374
.GetManagedObjects().items()
700
375
if client_interface in ifs_and_props}
702
# restore dbus logger
703
dbus_logger.removeFilter(dbus_filter)
378
os.dup2(stderrcopy, sys.stderr.fileno())
704
380
except dbus.exceptions.DBusException as e:
705
log.critical("Failed to access Mandos server through D-Bus:"
381
print("Access denied: "
382
"Accessing mandos server through D-Bus: {}".format(e),
709
386
# Compile dict of (clients: properties) to process
389
if options.all or not options.client:
713
390
clients = {bus.get_object(busname, path): properties
714
391
for path, properties in mandos_clients.items()}
716
for name in clientnames:
393
for name in options.client:
717
394
for path, client in mandos_clients.items():
718
395
if client["Name"] == name:
719
396
client_objc = bus.get_object(busname, path)
720
397
clients[client_objc] = client
723
log.critical("Client not found on server: %r", name)
400
print("Client not found on server: {!r}"
401
.format(name), file=sys.stderr)
726
# Run all commands on clients
727
for command in commands:
728
command.run(mandos_serv, clients)
731
class Test_milliseconds_to_string(unittest.TestCase):
733
self.assertEqual(milliseconds_to_string(93785000),
735
def test_no_days(self):
736
self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
737
def test_all_zero(self):
738
self.assertEqual(milliseconds_to_string(0), "00:00:00")
739
def test_no_fractional_seconds(self):
740
self.assertEqual(milliseconds_to_string(400), "00:00:00")
741
self.assertEqual(milliseconds_to_string(900), "00:00:00")
742
self.assertEqual(milliseconds_to_string(1900), "00:00:01")
744
class Test_string_to_delta(unittest.TestCase):
745
def test_handles_basic_rfc3339(self):
746
self.assertEqual(string_to_delta("PT2H"),
747
datetime.timedelta(0, 7200))
748
def test_falls_back_to_pre_1_6_1_with_warning(self):
749
# assertLogs only exists in Python 3.4
750
if hasattr(self, "assertLogs"):
751
with self.assertLogs(log, logging.WARNING):
752
value = string_to_delta("2h")
754
class WarningFilter(logging.Filter):
755
"""Don't show, but record the presence of, warnings"""
756
def filter(self, record):
757
is_warning = record.levelno >= logging.WARNING
758
self.found = is_warning or getattr(self, "found",
760
return not is_warning
761
warning_filter = WarningFilter()
762
log.addFilter(warning_filter)
764
value = string_to_delta("2h")
766
log.removeFilter(warning_filter)
767
self.assertTrue(getattr(warning_filter, "found", False))
768
self.assertEqual(value, datetime.timedelta(0, 7200))
771
class TestCmd(unittest.TestCase):
772
"""Abstract class for tests of command classes"""
775
class MockClient(object):
776
def __init__(self, name, **attributes):
777
self.__dbus_object_path__ = "objpath_{}".format(name)
778
self.attributes = attributes
779
self.attributes["Name"] = name
781
def Set(self, interface, property, value, dbus_interface):
782
testcase.assertEqual(interface, client_interface)
783
testcase.assertEqual(dbus_interface,
784
dbus.PROPERTIES_IFACE)
785
self.attributes[property] = value
786
self.calls.append(("Set", (interface, property, value,
788
def Get(self, interface, property, dbus_interface):
789
testcase.assertEqual(interface, client_interface)
790
testcase.assertEqual(dbus_interface,
791
dbus.PROPERTIES_IFACE)
792
self.calls.append(("Get", (interface, property,
794
return self.attributes[property]
795
def __getitem__(self, key):
796
return self.attributes[key]
797
def __setitem__(self, key, value):
798
self.attributes[key] = value
799
self.clients = collections.OrderedDict([
803
KeyID=("92ed150794387c03ce684574b1139a65"
804
"94a34f895daaaf09fd8ea90a27cddb12"),
806
Host="foo.example.org",
807
Enabled=dbus.Boolean(True),
809
LastCheckedOK="2019-02-03T00:00:00",
810
Created="2019-01-02T00:00:00",
812
Fingerprint=("778827225BA7DE539C5A"
813
"7CFA59CFF7CDBD9A5920"),
814
CheckerRunning=dbus.Boolean(False),
815
LastEnabled="2019-01-03T00:00:00",
816
ApprovalPending=dbus.Boolean(False),
817
ApprovedByDefault=dbus.Boolean(True),
818
LastApprovalRequest="",
820
ApprovalDuration=1000,
821
Checker="fping -q -- %(host)s",
822
ExtendedTimeout=900000,
823
Expires="2019-02-04T00:00:00",
824
LastCheckerStatus=0)),
828
KeyID=("0558568eedd67d622f5c83b35a115f79"
829
"6ab612cff5ad227247e46c2b020f441c"),
832
Enabled=dbus.Boolean(True),
834
LastCheckedOK="2019-02-04T00:00:00",
835
Created="2019-01-03T00:00:00",
837
Fingerprint=("3E393AEAEFB84C7E89E2"
838
"F547B3A107558FCA3A27"),
839
CheckerRunning=dbus.Boolean(True),
840
LastEnabled="2019-01-04T00:00:00",
841
ApprovalPending=dbus.Boolean(False),
842
ApprovedByDefault=dbus.Boolean(False),
843
LastApprovalRequest="2019-01-03T00:00:00",
845
ApprovalDuration=1000,
847
ExtendedTimeout=900000,
848
Expires="2019-02-05T00:00:00",
849
LastCheckerStatus=-2)),
852
class TestPrintTableCmd(TestCmd):
853
def test_normal(self):
854
output = PrintTableCmd().output(self.clients)
855
expected_output = """
856
Name Enabled Timeout Last Successful Check
857
foo Yes 00:05:00 2019-02-03T00:00:00
858
barbar Yes 00:05:00 2019-02-04T00:00:00
860
self.assertEqual(output, expected_output)
861
def test_verbose(self):
862
output = PrintTableCmd(verbose=True).output(self.clients)
863
expected_output = """
864
Name Enabled Timeout Last Successful Check Created Interval Host Key ID Fingerprint Check Is Running Last Enabled Approval Is Pending Approved By Default Last Approval Request Approval Delay Approval Duration Checker Extended Timeout Expires Last Checker Status
865
foo Yes 00:05:00 2019-02-03T00:00:00 2019-01-02T00:00:00 00:02:00 foo.example.org 92ed150794387c03ce684574b1139a6594a34f895daaaf09fd8ea90a27cddb12 778827225BA7DE539C5A7CFA59CFF7CDBD9A5920 No 2019-01-03T00:00:00 No Yes 00:00:00 00:00:01 fping -q -- %(host)s 00:15:00 2019-02-04T00:00:00 0
866
barbar Yes 00:05:00 2019-02-04T00:00:00 2019-01-03T00:00:00 00:02:00 192.0.2.3 0558568eedd67d622f5c83b35a115f796ab612cff5ad227247e46c2b020f441c 3E393AEAEFB84C7E89E2F547B3A107558FCA3A27 Yes 2019-01-04T00:00:00 No No 2019-01-03T00:00:00 00:00:30 00:00:01 : 00:15:00 2019-02-05T00:00:00 -2
868
self.assertEqual(output, expected_output)
869
def test_one_client(self):
870
output = PrintTableCmd().output({"foo": self.clients["foo"]})
871
expected_output = """
872
Name Enabled Timeout Last Successful Check
873
foo Yes 00:05:00 2019-02-03T00:00:00
875
self.assertEqual(output, expected_output)
877
class TestDumpJSONCmd(TestCmd):
879
self.expected_json = {
882
"KeyID": ("92ed150794387c03ce684574b1139a65"
883
"94a34f895daaaf09fd8ea90a27cddb12"),
884
"Host": "foo.example.org",
887
"LastCheckedOK": "2019-02-03T00:00:00",
888
"Created": "2019-01-02T00:00:00",
890
"Fingerprint": ("778827225BA7DE539C5A"
891
"7CFA59CFF7CDBD9A5920"),
892
"CheckerRunning": False,
893
"LastEnabled": "2019-01-03T00:00:00",
894
"ApprovalPending": False,
895
"ApprovedByDefault": True,
896
"LastApprovalRequest": "",
898
"ApprovalDuration": 1000,
899
"Checker": "fping -q -- %(host)s",
900
"ExtendedTimeout": 900000,
901
"Expires": "2019-02-04T00:00:00",
902
"LastCheckerStatus": 0,
906
"KeyID": ("0558568eedd67d622f5c83b35a115f79"
907
"6ab612cff5ad227247e46c2b020f441c"),
911
"LastCheckedOK": "2019-02-04T00:00:00",
912
"Created": "2019-01-03T00:00:00",
914
"Fingerprint": ("3E393AEAEFB84C7E89E2"
915
"F547B3A107558FCA3A27"),
916
"CheckerRunning": True,
917
"LastEnabled": "2019-01-04T00:00:00",
918
"ApprovalPending": False,
919
"ApprovedByDefault": False,
920
"LastApprovalRequest": "2019-01-03T00:00:00",
921
"ApprovalDelay": 30000,
922
"ApprovalDuration": 1000,
924
"ExtendedTimeout": 900000,
925
"Expires": "2019-02-05T00:00:00",
926
"LastCheckerStatus": -2,
929
return super(TestDumpJSONCmd, self).setUp()
930
def test_normal(self):
931
json_data = json.loads(DumpJSONCmd().output(self.clients))
932
self.assertDictEqual(json_data, self.expected_json)
933
def test_one_client(self):
934
clients = {"foo": self.clients["foo"]}
935
json_data = json.loads(DumpJSONCmd().output(clients))
936
expected_json = {"foo": self.expected_json["foo"]}
937
self.assertDictEqual(json_data, expected_json)
939
class TestIsEnabledCmd(TestCmd):
940
def test_is_enabled(self):
941
self.assertTrue(all(IsEnabledCmd().is_enabled(client)
942
for client in self.clients.values()))
943
def test_is_enabled_does_get_attribute(self):
944
client = self.clients["foo"]
945
self.assertTrue(IsEnabledCmd().is_enabled(client))
946
self.assertListEqual(client.calls,
948
("se.recompile.Mandos.Client",
950
"org.freedesktop.DBus.Properties"))])
951
def test_is_enabled_run_exits_successfully(self):
952
client = self.clients["foo"]
953
with self.assertRaises(SystemExit) as e:
954
IsEnabledCmd().run_on_one_client(client)
955
if e.exception.code is not None:
956
self.assertEqual(e.exception.code, 0)
958
self.assertIsNone(e.exception.code)
959
def test_is_enabled_run_exits_with_failure(self):
960
client = self.clients["foo"]
961
client["Enabled"] = dbus.Boolean(False)
962
with self.assertRaises(SystemExit) as e:
963
IsEnabledCmd().run_on_one_client(client)
964
if isinstance(e.exception.code, int):
965
self.assertNotEqual(e.exception.code, 0)
967
self.assertIsNotNone(e.exception.code)
971
def should_only_run_tests():
972
parser = argparse.ArgumentParser(add_help=False)
973
parser.add_argument("--check", action='store_true')
974
args, unknown_args = parser.parse_known_args()
975
run_tests = args.check
977
# Remove --check argument from sys.argv
978
sys.argv[1:] = unknown_args
981
# Add all tests from doctest strings
982
def load_tests(loader, tests, none):
984
tests.addTests(doctest.DocTestSuite())
404
if not has_actions(options) and clients:
405
if options.verbose or options.dump_json:
406
keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
407
"Created", "Interval", "Host", "Fingerprint",
408
"CheckerRunning", "LastEnabled",
409
"ApprovalPending", "ApprovedByDefault",
410
"LastApprovalRequest", "ApprovalDelay",
411
"ApprovalDuration", "Checker",
412
"ExtendedTimeout", "Expires",
415
keywords = defaultkeywords
417
if options.dump_json:
418
json.dump({client["Name"]: {key:
420
if isinstance(client[key],
424
for client in clients.values()},
425
fp=sys.stdout, indent=4,
426
separators=(',', ': '))
429
print_clients(clients.values(), keywords)
431
# Process each client in the list by all selected options
432
for client in clients:
434
def set_client_prop(prop, value):
435
"""Set a Client D-Bus property"""
436
client.Set(client_interface, prop, value,
437
dbus_interface=dbus.PROPERTIES_IFACE)
439
def set_client_prop_ms(prop, value):
440
"""Set a Client D-Bus property, converted
441
from a string to milliseconds."""
442
set_client_prop(prop,
443
string_to_delta(value).total_seconds()
447
mandos_serv.RemoveClient(client.__dbus_object_path__)
449
set_client_prop("Enabled", dbus.Boolean(True))
451
set_client_prop("Enabled", dbus.Boolean(False))
452
if options.bump_timeout:
453
set_client_prop("LastCheckedOK", "")
454
if options.start_checker:
455
set_client_prop("CheckerRunning", dbus.Boolean(True))
456
if options.stop_checker:
457
set_client_prop("CheckerRunning", dbus.Boolean(False))
458
if options.is_enabled:
459
if client.Get(client_interface, "Enabled",
460
dbus_interface=dbus.PROPERTIES_IFACE):
464
if options.checker is not None:
465
set_client_prop("Checker", options.checker)
466
if options.host is not None:
467
set_client_prop("Host", options.host)
468
if options.interval is not None:
469
set_client_prop_ms("Interval", options.interval)
470
if options.approval_delay is not None:
471
set_client_prop_ms("ApprovalDelay",
472
options.approval_delay)
473
if options.approval_duration is not None:
474
set_client_prop_ms("ApprovalDuration",
475
options.approval_duration)
476
if options.timeout is not None:
477
set_client_prop_ms("Timeout", options.timeout)
478
if options.extended_timeout is not None:
479
set_client_prop_ms("ExtendedTimeout",
480
options.extended_timeout)
481
if options.secret is not None:
482
set_client_prop("Secret",
483
dbus.ByteArray(options.secret.read()))
484
if options.approved_by_default is not None:
485
set_client_prop("ApprovedByDefault",
487
.approved_by_default))
489
client.Approve(dbus.Boolean(True),
490
dbus_interface=client_interface)
492
client.Approve(dbus.Boolean(False),
493
dbus_interface=client_interface)
987
496
if __name__ == "__main__":
988
if should_only_run_tests():
989
# Call using ./tdd-python-script --check [--verbose]