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}))
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"
260
499
def has_actions(options):
261
500
return any((options.enable,
360
688
mandos_serv_object_manager = dbus.Interface(
361
689
mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
363
# block stderr since dbus library prints to stderr
364
null = os.open(os.path.devnull, os.O_RDWR)
365
stderrcopy = os.dup(sys.stderr.fileno())
366
os.dup2(null, sys.stderr.fileno())
691
# Filter out log message from dbus module
692
dbus_logger = logging.getLogger("dbus.proxies")
693
class NullFilter(logging.Filter):
694
def filter(self, record):
696
dbus_filter = NullFilter()
370
mandos_clients = {path: ifs_and_props[client_interface]
371
for path, ifs_and_props in
372
mandos_serv_object_manager
373
.GetManagedObjects().items()
374
if client_interface in ifs_and_props}
377
os.dup2(stderrcopy, sys.stderr.fileno())
698
dbus_logger.addFilter(dbus_filter)
699
mandos_clients = {path: ifs_and_props[client_interface]
700
for path, ifs_and_props in
701
mandos_serv_object_manager
702
.GetManagedObjects().items()
703
if client_interface in ifs_and_props}
379
704
except dbus.exceptions.DBusException as e:
380
print("Access denied: "
381
"Accessing mandos server through D-Bus: {}".format(e),
705
log.critical("Failed to access Mandos server through D-Bus:"
709
# restore dbus logger
710
dbus_logger.removeFilter(dbus_filter)
385
712
# Compile dict of (clients: properties) to process
388
if options.all or not options.client:
389
716
clients = {bus.get_object(busname, path): properties
390
717
for path, properties in mandos_clients.items()}
392
for name in options.client:
719
for name in clientnames:
393
720
for path, client in mandos_clients.items():
394
721
if client["Name"] == name:
395
722
client_objc = bus.get_object(busname, path)
396
723
clients[client_objc] = client
399
print("Client not found on server: {!r}"
400
.format(name), file=sys.stderr)
726
log.critical("Client not found on server: %r", name)
403
if not has_actions(options) and clients:
404
if options.verbose or options.dump_json:
405
keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
406
"Created", "Interval", "Host", "Fingerprint",
407
"CheckerRunning", "LastEnabled",
408
"ApprovalPending", "ApprovedByDefault",
409
"LastApprovalRequest", "ApprovalDelay",
410
"ApprovalDuration", "Checker",
411
"ExtendedTimeout", "Expires",
414
keywords = defaultkeywords
416
if options.dump_json:
417
json.dump({client["Name"]: {key:
419
if isinstance(client[key],
423
for client in clients.values()},
424
fp=sys.stdout, indent=4,
425
separators=(',', ': '))
428
print_clients(clients.values(), keywords)
430
# Process each client in the list by all selected options
431
for client in clients:
433
def set_client_prop(prop, value):
434
"""Set a Client D-Bus property"""
435
client.Set(client_interface, prop, value,
436
dbus_interface=dbus.PROPERTIES_IFACE)
438
def set_client_prop_ms(prop, value):
439
"""Set a Client D-Bus property, converted
440
from a string to milliseconds."""
441
set_client_prop(prop,
442
string_to_delta(value).total_seconds()
446
mandos_serv.RemoveClient(client.__dbus_object_path__)
448
set_client_prop("Enabled", dbus.Boolean(True))
450
set_client_prop("Enabled", dbus.Boolean(False))
451
if options.bump_timeout:
452
set_client_prop("LastCheckedOK", "")
453
if options.start_checker:
454
set_client_prop("CheckerRunning", dbus.Boolean(True))
455
if options.stop_checker:
456
set_client_prop("CheckerRunning", dbus.Boolean(False))
457
if options.is_enabled:
458
if client.Get(client_interface, "Enabled",
459
dbus_interface=dbus.PROPERTIES_IFACE):
463
if options.checker is not None:
464
set_client_prop("Checker", options.checker)
465
if options.host is not None:
466
set_client_prop("Host", options.host)
467
if options.interval is not None:
468
set_client_prop_ms("Interval", options.interval)
469
if options.approval_delay is not None:
470
set_client_prop_ms("ApprovalDelay",
471
options.approval_delay)
472
if options.approval_duration is not None:
473
set_client_prop_ms("ApprovalDuration",
474
options.approval_duration)
475
if options.timeout is not None:
476
set_client_prop_ms("Timeout", options.timeout)
477
if options.extended_timeout is not None:
478
set_client_prop_ms("ExtendedTimeout",
479
options.extended_timeout)
480
if options.secret is not None:
481
set_client_prop("Secret",
482
dbus.ByteArray(options.secret.read()))
483
if options.approved_by_default is not None:
484
set_client_prop("ApprovedByDefault",
486
.approved_by_default))
488
client.Approve(dbus.Boolean(True),
489
dbus_interface=client_interface)
491
client.Approve(dbus.Boolean(False),
492
dbus_interface=client_interface)
729
# Run all commands on clients
730
for command in commands:
731
command.run(mandos_serv, clients)
734
class Test_milliseconds_to_string(unittest.TestCase):
736
self.assertEqual(milliseconds_to_string(93785000),
738
def test_no_days(self):
739
self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
740
def test_all_zero(self):
741
self.assertEqual(milliseconds_to_string(0), "00:00:00")
742
def test_no_fractional_seconds(self):
743
self.assertEqual(milliseconds_to_string(400), "00:00:00")
744
self.assertEqual(milliseconds_to_string(900), "00:00:00")
745
self.assertEqual(milliseconds_to_string(1900), "00:00:01")
747
class Test_string_to_delta(unittest.TestCase):
748
def test_handles_basic_rfc3339(self):
749
self.assertEqual(string_to_delta("PT2H"),
750
datetime.timedelta(0, 7200))
751
def test_falls_back_to_pre_1_6_1_with_warning(self):
752
# assertLogs only exists in Python 3.4
753
if hasattr(self, "assertLogs"):
754
with self.assertLogs(log, logging.WARNING):
755
value = string_to_delta("2h")
757
class WarningFilter(logging.Filter):
758
"""Don't show, but record the presence of, warnings"""
759
def filter(self, record):
760
is_warning = record.levelno >= logging.WARNING
761
self.found = is_warning or getattr(self, "found",
763
return not is_warning
764
warning_filter = WarningFilter()
765
log.addFilter(warning_filter)
767
value = string_to_delta("2h")
769
log.removeFilter(warning_filter)
770
self.assertTrue(getattr(warning_filter, "found", False))
771
self.assertEqual(value, datetime.timedelta(0, 7200))
774
class TestCmd(unittest.TestCase):
775
"""Abstract class for tests of command classes"""
778
class MockClient(object):
779
def __init__(self, name, **attributes):
780
self.__dbus_object_path__ = "objpath_{}".format(name)
781
self.attributes = attributes
782
self.attributes["Name"] = name
784
def Set(self, interface, property, value, dbus_interface):
785
testcase.assertEqual(interface, client_interface)
786
testcase.assertEqual(dbus_interface,
787
dbus.PROPERTIES_IFACE)
788
self.attributes[property] = value
789
self.calls.append(("Set", (interface, property, value,
791
def Get(self, interface, property, dbus_interface):
792
testcase.assertEqual(interface, client_interface)
793
testcase.assertEqual(dbus_interface,
794
dbus.PROPERTIES_IFACE)
795
self.calls.append(("Get", (interface, property,
797
return self.attributes[property]
798
def Approve(self, approve, dbus_interface):
799
testcase.assertEqual(dbus_interface, client_interface)
800
self.calls.append(("Approve", (approve,
802
def __getitem__(self, key):
803
return self.attributes[key]
804
def __setitem__(self, key, value):
805
self.attributes[key] = value
806
self.clients = collections.OrderedDict([
810
KeyID=("92ed150794387c03ce684574b1139a65"
811
"94a34f895daaaf09fd8ea90a27cddb12"),
813
Host="foo.example.org",
814
Enabled=dbus.Boolean(True),
816
LastCheckedOK="2019-02-03T00:00:00",
817
Created="2019-01-02T00:00:00",
819
Fingerprint=("778827225BA7DE539C5A"
820
"7CFA59CFF7CDBD9A5920"),
821
CheckerRunning=dbus.Boolean(False),
822
LastEnabled="2019-01-03T00:00:00",
823
ApprovalPending=dbus.Boolean(False),
824
ApprovedByDefault=dbus.Boolean(True),
825
LastApprovalRequest="",
827
ApprovalDuration=1000,
828
Checker="fping -q -- %(host)s",
829
ExtendedTimeout=900000,
830
Expires="2019-02-04T00:00:00",
831
LastCheckerStatus=0)),
835
KeyID=("0558568eedd67d622f5c83b35a115f79"
836
"6ab612cff5ad227247e46c2b020f441c"),
839
Enabled=dbus.Boolean(True),
841
LastCheckedOK="2019-02-04T00:00:00",
842
Created="2019-01-03T00:00:00",
844
Fingerprint=("3E393AEAEFB84C7E89E2"
845
"F547B3A107558FCA3A27"),
846
CheckerRunning=dbus.Boolean(True),
847
LastEnabled="2019-01-04T00:00:00",
848
ApprovalPending=dbus.Boolean(False),
849
ApprovedByDefault=dbus.Boolean(False),
850
LastApprovalRequest="2019-01-03T00:00:00",
852
ApprovalDuration=1000,
854
ExtendedTimeout=900000,
855
Expires="2019-02-05T00:00:00",
856
LastCheckerStatus=-2)),
858
self.client = self.clients["foo"]
860
class TestPrintTableCmd(TestCmd):
861
def test_normal(self):
862
output = PrintTableCmd().output(self.clients)
863
expected_output = """
864
Name Enabled Timeout Last Successful Check
865
foo Yes 00:05:00 2019-02-03T00:00:00
866
barbar Yes 00:05:00 2019-02-04T00:00:00
868
self.assertEqual(output, expected_output)
869
def test_verbose(self):
870
output = PrintTableCmd(verbose=True).output(self.clients)
871
expected_output = """
872
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
873
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
874
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
876
self.assertEqual(output, expected_output)
877
def test_one_client(self):
878
output = PrintTableCmd().output({"foo": self.client})
879
expected_output = """
880
Name Enabled Timeout Last Successful Check
881
foo Yes 00:05:00 2019-02-03T00:00:00
883
self.assertEqual(output, expected_output)
885
class TestDumpJSONCmd(TestCmd):
887
self.expected_json = {
890
"KeyID": ("92ed150794387c03ce684574b1139a65"
891
"94a34f895daaaf09fd8ea90a27cddb12"),
892
"Host": "foo.example.org",
895
"LastCheckedOK": "2019-02-03T00:00:00",
896
"Created": "2019-01-02T00:00:00",
898
"Fingerprint": ("778827225BA7DE539C5A"
899
"7CFA59CFF7CDBD9A5920"),
900
"CheckerRunning": False,
901
"LastEnabled": "2019-01-03T00:00:00",
902
"ApprovalPending": False,
903
"ApprovedByDefault": True,
904
"LastApprovalRequest": "",
906
"ApprovalDuration": 1000,
907
"Checker": "fping -q -- %(host)s",
908
"ExtendedTimeout": 900000,
909
"Expires": "2019-02-04T00:00:00",
910
"LastCheckerStatus": 0,
914
"KeyID": ("0558568eedd67d622f5c83b35a115f79"
915
"6ab612cff5ad227247e46c2b020f441c"),
919
"LastCheckedOK": "2019-02-04T00:00:00",
920
"Created": "2019-01-03T00:00:00",
922
"Fingerprint": ("3E393AEAEFB84C7E89E2"
923
"F547B3A107558FCA3A27"),
924
"CheckerRunning": True,
925
"LastEnabled": "2019-01-04T00:00:00",
926
"ApprovalPending": False,
927
"ApprovedByDefault": False,
928
"LastApprovalRequest": "2019-01-03T00:00:00",
929
"ApprovalDelay": 30000,
930
"ApprovalDuration": 1000,
932
"ExtendedTimeout": 900000,
933
"Expires": "2019-02-05T00:00:00",
934
"LastCheckerStatus": -2,
937
return super(TestDumpJSONCmd, self).setUp()
938
def test_normal(self):
939
json_data = json.loads(DumpJSONCmd().output(self.clients))
940
self.assertDictEqual(json_data, self.expected_json)
941
def test_one_client(self):
942
clients = {"foo": self.client}
943
json_data = json.loads(DumpJSONCmd().output(clients))
944
expected_json = {"foo": self.expected_json["foo"]}
945
self.assertDictEqual(json_data, expected_json)
947
class TestIsEnabledCmd(TestCmd):
948
def test_is_enabled(self):
949
self.assertTrue(all(IsEnabledCmd().is_enabled(client)
950
for client in self.clients.values()))
951
def test_is_enabled_does_get_attribute(self):
952
self.assertTrue(IsEnabledCmd().is_enabled(self.client))
953
self.assertListEqual(self.client.calls,
955
("se.recompile.Mandos.Client",
957
"org.freedesktop.DBus.Properties"))])
958
def test_is_enabled_run_exits_successfully(self):
959
with self.assertRaises(SystemExit) as e:
960
IsEnabledCmd().run(None, [self.client])
961
if e.exception.code is not None:
962
self.assertEqual(e.exception.code, 0)
964
self.assertIsNone(e.exception.code)
965
def test_is_enabled_run_exits_with_failure(self):
966
self.client["Enabled"] = dbus.Boolean(False)
967
with self.assertRaises(SystemExit) as e:
968
IsEnabledCmd().run(None, [self.client])
969
if isinstance(e.exception.code, int):
970
self.assertNotEqual(e.exception.code, 0)
972
self.assertIsNotNone(e.exception.code)
974
class TestRemoveCmd(TestCmd):
975
def test_remove(self):
976
class MockMandos(object):
979
def RemoveClient(self, dbus_path):
980
self.calls.append(("RemoveClient", (dbus_path,)))
981
mandos = MockMandos()
982
RemoveCmd().run(mandos, [self.client])
983
self.assertEqual(len(mandos.calls), 1)
984
self.assertListEqual(mandos.calls,
986
(self.client.__dbus_object_path__,))])
988
class TestApproveCmd(TestCmd):
989
def test_approve(self):
990
ApproveCmd().run(None, [self.client])
991
self.assertListEqual(self.client.calls,
992
[("Approve", (True, client_interface))])
993
class TestDenyCmd(TestCmd):
994
def test_approve(self):
995
DenyCmd().run(None, [self.client])
996
self.assertListEqual(self.client.calls,
997
[("Approve", (False, client_interface))])
1001
def should_only_run_tests():
1002
parser = argparse.ArgumentParser(add_help=False)
1003
parser.add_argument("--check", action='store_true')
1004
args, unknown_args = parser.parse_known_args()
1005
run_tests = args.check
1007
# Remove --check argument from sys.argv
1008
sys.argv[1:] = unknown_args
1011
# Add all tests from doctest strings
1012
def load_tests(loader, tests, none):
1014
tests.addTests(doctest.DocTestSuite())
495
1017
if __name__ == "__main__":
1018
if should_only_run_tests():
1019
# Call using ./tdd-python-script --check [--verbose]