437
430
parser.error("--dump-json can only be used alone.")
438
431
if options.all and not has_actions(options):
439
432
parser.error("--all requires an action.")
440
if options.is_enabled and len(options.client) > 1:
441
parser.error("--is-enabled requires exactly one client")
443
options.remove = False
444
if has_actions(options) and not options.deny:
445
parser.error("--remove can only be combined with --deny")
446
options.remove = True
449
def commands_from_options(options):
453
if options.is_enabled:
454
commands.append(IsEnabledCmd())
457
commands.append(ApproveCmd())
460
commands.append(DenyCmd())
463
commands.append(RemoveCmd())
465
if options.dump_json:
466
commands.append(DumpJSONCmd())
469
commands.append(EnableCmd())
472
commands.append(DisableCmd())
474
if options.bump_timeout:
475
commands.append(BumpTimeoutCmd())
477
if options.start_checker:
478
commands.append(StartCheckerCmd())
480
if options.stop_checker:
481
commands.append(StopCheckerCmd())
483
if options.approved_by_default is not None:
484
if options.approved_by_default:
485
commands.append(ApproveByDefaultCmd())
487
commands.append(DenyByDefaultCmd())
489
if options.checker is not None:
490
commands.append(SetCheckerCmd(options.checker))
492
if options.host is not None:
493
commands.append(SetHostCmd(options.host))
495
if options.secret is not None:
496
commands.append(SetSecretCmd(options.secret))
498
if options.timeout is not None:
499
commands.append(SetTimeoutCmd(options.timeout))
501
if options.extended_timeout:
503
SetExtendedTimeoutCmd(options.extended_timeout))
505
if options.interval is not None:
506
commands.append(SetIntervalCmd(options.interval))
508
if options.approval_delay is not None:
509
commands.append(SetApprovalDelayCmd(options.approval_delay))
511
if options.approval_duration is not None:
513
SetApprovalDurationCmd(options.approval_duration))
515
# If no command option has been given, show table of clients,
516
# optionally verbosely
518
commands.append(PrintTableCmd(verbose=options.verbose))
523
class Command(object):
524
"""Abstract class for commands"""
525
def run(self, clients, bus=None, mandos=None):
526
"""Normal commands should implement run_on_one_client(), but
527
commands which want to operate on all clients at the same time
528
can override this run() method instead."""
530
for clientpath, properties in clients.items():
531
log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
532
dbus_busname, str(clientpath))
533
client = bus.get_object(dbus_busname, clientpath)
534
self.run_on_one_client(client, properties)
537
class IsEnabledCmd(Command):
538
def run(self, clients, bus=None, mandos=None):
539
client, properties = next(iter(clients.items()))
540
if self.is_enabled(client, properties):
543
def is_enabled(self, client, properties):
544
return properties["Enabled"]
547
class ApproveCmd(Command):
548
def run_on_one_client(self, client, properties):
549
log.debug("D-Bus: %s:%s:%s.Approve(True)", dbus_busname,
550
client.__dbus_object_path__, client_dbus_interface)
551
client.Approve(dbus.Boolean(True),
552
dbus_interface=client_dbus_interface)
555
class DenyCmd(Command):
556
def run_on_one_client(self, client, properties):
557
log.debug("D-Bus: %s:%s:%s.Approve(False)", dbus_busname,
558
client.__dbus_object_path__, client_dbus_interface)
559
client.Approve(dbus.Boolean(False),
560
dbus_interface=client_dbus_interface)
563
class RemoveCmd(Command):
564
def run_on_one_client(self, client, properties):
565
log.debug("D-Bus: %s:%s:%s.RemoveClient(%r)", dbus_busname,
566
server_dbus_path, server_dbus_interface,
567
str(client.__dbus_object_path__))
568
self.mandos.RemoveClient(client.__dbus_object_path__)
571
class OutputCmd(Command):
572
"""Abstract class for commands outputting client details"""
573
all_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
574
"Created", "Interval", "Host", "KeyID",
575
"Fingerprint", "CheckerRunning", "LastEnabled",
576
"ApprovalPending", "ApprovedByDefault",
577
"LastApprovalRequest", "ApprovalDelay",
578
"ApprovalDuration", "Checker", "ExtendedTimeout",
579
"Expires", "LastCheckerStatus")
581
def run(self, clients, bus=None, mandos=None):
582
print(self.output(clients.values()))
584
def output(self, clients):
585
raise NotImplementedError()
588
class DumpJSONCmd(OutputCmd):
589
def output(self, clients):
590
data = {client["Name"]:
591
{key: self.dbus_boolean_to_bool(client[key])
592
for key in self.all_keywords}
593
for client in clients}
594
return json.dumps(data, indent=4, separators=(',', ': '))
597
def dbus_boolean_to_bool(value):
598
if isinstance(value, dbus.Boolean):
603
class PrintTableCmd(OutputCmd):
604
def __init__(self, verbose=False):
605
self.verbose = verbose
607
def output(self, clients):
608
default_keywords = ("Name", "Enabled", "Timeout",
610
keywords = default_keywords
612
keywords = self.all_keywords
613
return str(self.TableOfClients(clients, keywords))
615
class TableOfClients(object):
618
"Enabled": "Enabled",
619
"Timeout": "Timeout",
620
"LastCheckedOK": "Last Successful Check",
621
"LastApprovalRequest": "Last Approval Request",
622
"Created": "Created",
623
"Interval": "Interval",
625
"Fingerprint": "Fingerprint",
627
"CheckerRunning": "Check Is Running",
628
"LastEnabled": "Last Enabled",
629
"ApprovalPending": "Approval Is Pending",
630
"ApprovedByDefault": "Approved By Default",
631
"ApprovalDelay": "Approval Delay",
632
"ApprovalDuration": "Approval Duration",
633
"Checker": "Checker",
634
"ExtendedTimeout": "Extended Timeout",
635
"Expires": "Expires",
636
"LastCheckerStatus": "Last Checker Status",
639
def __init__(self, clients, keywords, tableheaders=None):
640
self.clients = clients
641
self.keywords = keywords
642
if tableheaders is not None:
643
self.tableheaders = tableheaders
646
return "\n".join(self.rows())
648
if sys.version_info.major == 2:
649
__unicode__ = __str__
651
return str(self).encode(locale.getpreferredencoding())
654
format_string = self.row_formatting_string()
655
rows = [self.header_line(format_string)]
656
rows.extend(self.client_line(client, format_string)
657
for client in self.clients)
660
def row_formatting_string(self):
661
"Format string used to format table rows"
662
return " ".join("{{{key}:{width}}}".format(
663
width=max(len(self.tableheaders[key]),
664
*(len(self.string_from_client(client, key))
665
for client in self.clients)),
667
for key in self.keywords)
669
def string_from_client(self, client, key):
670
return self.valuetostring(client[key], key)
673
def valuetostring(cls, value, keyword):
674
if isinstance(value, dbus.Boolean):
675
return "Yes" if value else "No"
676
if keyword in ("Timeout", "Interval", "ApprovalDelay",
677
"ApprovalDuration", "ExtendedTimeout"):
678
return cls.milliseconds_to_string(value)
681
def header_line(self, format_string):
682
return format_string.format(**self.tableheaders)
684
def client_line(self, client, format_string):
685
return format_string.format(
686
**{key: self.string_from_client(client, key)
687
for key in self.keywords})
690
def milliseconds_to_string(ms):
691
td = datetime.timedelta(0, 0, 0, ms)
692
return ("{days}{hours:02}:{minutes:02}:{seconds:02}"
693
.format(days="{}T".format(td.days)
695
hours=td.seconds // 3600,
696
minutes=(td.seconds % 3600) // 60,
697
seconds=td.seconds % 60))
700
class PropertyCmd(Command):
701
"""Abstract class for Actions for setting one client property"""
703
def run_on_one_client(self, client, properties):
704
"""Set the Client's D-Bus property"""
705
log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", dbus_busname,
706
client.__dbus_object_path__,
707
dbus.PROPERTIES_IFACE, client_dbus_interface,
708
self.propname, self.value_to_set
709
if not isinstance(self.value_to_set, dbus.Boolean)
710
else bool(self.value_to_set))
711
client.Set(client_dbus_interface, self.propname,
713
dbus_interface=dbus.PROPERTIES_IFACE)
717
raise NotImplementedError()
720
class EnableCmd(PropertyCmd):
722
value_to_set = dbus.Boolean(True)
725
class DisableCmd(PropertyCmd):
727
value_to_set = dbus.Boolean(False)
730
class BumpTimeoutCmd(PropertyCmd):
731
propname = "LastCheckedOK"
735
class StartCheckerCmd(PropertyCmd):
736
propname = "CheckerRunning"
737
value_to_set = dbus.Boolean(True)
740
class StopCheckerCmd(PropertyCmd):
741
propname = "CheckerRunning"
742
value_to_set = dbus.Boolean(False)
745
class ApproveByDefaultCmd(PropertyCmd):
746
propname = "ApprovedByDefault"
747
value_to_set = dbus.Boolean(True)
750
class DenyByDefaultCmd(PropertyCmd):
751
propname = "ApprovedByDefault"
752
value_to_set = dbus.Boolean(False)
755
class PropertyValueCmd(PropertyCmd):
756
"""Abstract class for PropertyCmd recieving a value as argument"""
757
def __init__(self, value):
758
self.value_to_set = value
761
class SetCheckerCmd(PropertyValueCmd):
765
class SetHostCmd(PropertyValueCmd):
769
class SetSecretCmd(PropertyValueCmd):
773
def value_to_set(self):
777
def value_to_set(self, value):
778
"""When setting, read data from supplied file object"""
779
self._vts = value.read()
783
class MillisecondsPropertyValueArgumentCmd(PropertyValueCmd):
784
"""Abstract class for PropertyValueCmd taking a value argument as
785
a datetime.timedelta() but should store it as milliseconds."""
788
def value_to_set(self):
792
def value_to_set(self, value):
793
"""When setting, convert value from a datetime.timedelta"""
794
self._vts = int(round(value.total_seconds() * 1000))
797
class SetTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
801
class SetExtendedTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
802
propname = "ExtendedTimeout"
805
class SetIntervalCmd(MillisecondsPropertyValueArgumentCmd):
806
propname = "Interval"
809
class SetApprovalDelayCmd(MillisecondsPropertyValueArgumentCmd):
810
propname = "ApprovalDelay"
813
class SetApprovalDurationCmd(MillisecondsPropertyValueArgumentCmd):
814
propname = "ApprovalDuration"
435
bus = dbus.SystemBus()
436
mandos_dbus_objc = bus.get_object(busname, server_path)
437
except dbus.exceptions.DBusException:
438
log.critical("Could not connect to Mandos server")
441
mandos_serv = dbus.Interface(mandos_dbus_objc,
442
dbus_interface=server_interface)
443
mandos_serv_object_manager = dbus.Interface(
444
mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
446
# block stderr since dbus library prints to stderr
447
null = os.open(os.path.devnull, os.O_RDWR)
448
stderrcopy = os.dup(sys.stderr.fileno())
449
os.dup2(null, sys.stderr.fileno())
453
mandos_clients = {path: ifs_and_props[client_interface]
454
for path, ifs_and_props in
455
mandos_serv_object_manager
456
.GetManagedObjects().items()
457
if client_interface in ifs_and_props}
460
os.dup2(stderrcopy, sys.stderr.fileno())
462
except dbus.exceptions.DBusException as e:
463
log.critical("Failed to access Mandos server through D-Bus:"
467
# Compile dict of (clients: properties) to process
470
if options.all or not options.client:
471
clients = {bus.get_object(busname, path): properties
472
for path, properties in mandos_clients.items()}
474
for name in options.client:
475
for path, client in mandos_clients.items():
476
if client["Name"] == name:
477
client_objc = bus.get_object(busname, path)
478
clients[client_objc] = client
481
log.critical("Client not found on server: %r", name)
484
if not has_actions(options) and clients:
485
if options.verbose or options.dump_json:
486
keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
487
"Created", "Interval", "Host", "KeyID",
488
"Fingerprint", "CheckerRunning",
489
"LastEnabled", "ApprovalPending",
490
"ApprovedByDefault", "LastApprovalRequest",
491
"ApprovalDelay", "ApprovalDuration",
492
"Checker", "ExtendedTimeout", "Expires",
495
keywords = defaultkeywords
497
if options.dump_json:
498
json.dump({client["Name"]: {key:
500
if isinstance(client[key],
504
for client in clients.values()},
505
fp=sys.stdout, indent=4,
506
separators=(',', ': '))
509
print(TableOfClients(clients.values(), keywords))
511
# Process each client in the list by all selected options
512
for client in clients:
514
def set_client_prop(prop, value):
515
"""Set a Client D-Bus property"""
516
client.Set(client_interface, prop, value,
517
dbus_interface=dbus.PROPERTIES_IFACE)
519
def set_client_prop_ms(prop, value):
520
"""Set a Client D-Bus property, converted
521
from a string to milliseconds."""
522
set_client_prop(prop,
523
string_to_delta(value).total_seconds()
527
mandos_serv.RemoveClient(client.__dbus_object_path__)
529
set_client_prop("Enabled", dbus.Boolean(True))
531
set_client_prop("Enabled", dbus.Boolean(False))
532
if options.bump_timeout:
533
set_client_prop("LastCheckedOK", "")
534
if options.start_checker:
535
set_client_prop("CheckerRunning", dbus.Boolean(True))
536
if options.stop_checker:
537
set_client_prop("CheckerRunning", dbus.Boolean(False))
538
if options.is_enabled:
539
if client.Get(client_interface, "Enabled",
540
dbus_interface=dbus.PROPERTIES_IFACE):
544
if options.checker is not None:
545
set_client_prop("Checker", options.checker)
546
if options.host is not None:
547
set_client_prop("Host", options.host)
548
if options.interval is not None:
549
set_client_prop_ms("Interval", options.interval)
550
if options.approval_delay is not None:
551
set_client_prop_ms("ApprovalDelay",
552
options.approval_delay)
553
if options.approval_duration is not None:
554
set_client_prop_ms("ApprovalDuration",
555
options.approval_duration)
556
if options.timeout is not None:
557
set_client_prop_ms("Timeout", options.timeout)
558
if options.extended_timeout is not None:
559
set_client_prop_ms("ExtendedTimeout",
560
options.extended_timeout)
561
if options.secret is not None:
562
set_client_prop("Secret",
563
dbus.ByteArray(options.secret.read()))
564
if options.approved_by_default is not None:
565
set_client_prop("ApprovedByDefault",
567
.approved_by_default))
569
client.Approve(dbus.Boolean(True),
570
dbus_interface=client_interface)
572
client.Approve(dbus.Boolean(False),
573
dbus_interface=client_interface)
576
class Test_milliseconds_to_string(unittest.TestCase):
578
self.assertEqual(milliseconds_to_string(93785000),
580
def test_no_days(self):
581
self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
582
def test_all_zero(self):
583
self.assertEqual(milliseconds_to_string(0), "00:00:00")
584
def test_no_fractional_seconds(self):
585
self.assertEqual(milliseconds_to_string(400), "00:00:00")
586
self.assertEqual(milliseconds_to_string(900), "00:00:00")
587
self.assertEqual(milliseconds_to_string(1900), "00:00:01")
818
589
class Test_string_to_delta(unittest.TestCase):
819
590
def test_handles_basic_rfc3339(self):
820
self.assertEqual(string_to_delta("PT0S"),
821
datetime.timedelta())
822
self.assertEqual(string_to_delta("P0D"),
823
datetime.timedelta())
824
self.assertEqual(string_to_delta("PT1S"),
825
datetime.timedelta(0, 1))
826
591
self.assertEqual(string_to_delta("PT2H"),
827
592
datetime.timedelta(0, 7200))
829
593
def test_falls_back_to_pre_1_6_1_with_warning(self):
830
594
# assertLogs only exists in Python 3.4
831
595
if hasattr(self, "assertLogs"):
832
596
with self.assertLogs(log, logging.WARNING):
833
597
value = string_to_delta("2h")
835
class WarningFilter(logging.Filter):
836
"""Don't show, but record the presence of, warnings"""
837
def filter(self, record):
838
is_warning = record.levelno >= logging.WARNING
839
self.found = is_warning or getattr(self, "found",
841
return not is_warning
842
warning_filter = WarningFilter()
843
log.addFilter(warning_filter)
845
value = string_to_delta("2h")
847
log.removeFilter(warning_filter)
848
self.assertTrue(getattr(warning_filter, "found", False))
599
value = string_to_delta("2h")
849
600
self.assertEqual(value, datetime.timedelta(0, 7200))
852
class Test_check_option_syntax(unittest.TestCase):
854
self.parser = argparse.ArgumentParser()
855
add_command_line_options(self.parser)
857
def test_actions_requires_client_or_all(self):
858
for action, value in self.actions.items():
859
options = self.parser.parse_args()
860
setattr(options, action, value)
861
with self.assertParseError():
862
self.check_option_syntax(options)
864
# This mostly corresponds to the definition from has_actions() in
865
# check_option_syntax()
867
# The actual values set here are not that important, but we do
868
# at least stick to the correct types, even though they are
872
"bump_timeout": True,
873
"start_checker": True,
874
"stop_checker": True,
878
"timeout": datetime.timedelta(),
879
"extended_timeout": datetime.timedelta(),
880
"interval": datetime.timedelta(),
881
"approved_by_default": True,
882
"approval_delay": datetime.timedelta(),
883
"approval_duration": datetime.timedelta(),
885
"secret": io.BytesIO(b"x"),
890
@contextlib.contextmanager
891
def assertParseError(self):
892
with self.assertRaises(SystemExit) as e:
893
with self.temporarily_suppress_stderr():
895
# Exit code from argparse is guaranteed to be "2". Reference:
896
# https://docs.python.org/3/library
897
# /argparse.html#exiting-methods
898
self.assertEqual(e.exception.code, 2)
901
@contextlib.contextmanager
902
def temporarily_suppress_stderr():
903
null = os.open(os.path.devnull, os.O_RDWR)
904
stderrcopy = os.dup(sys.stderr.fileno())
905
os.dup2(null, sys.stderr.fileno())
911
os.dup2(stderrcopy, sys.stderr.fileno())
914
def check_option_syntax(self, options):
915
check_option_syntax(self.parser, options)
917
def test_actions_conflicts_with_verbose(self):
918
for action, value in self.actions.items():
919
options = self.parser.parse_args()
920
setattr(options, action, value)
921
options.verbose = True
922
with self.assertParseError():
923
self.check_option_syntax(options)
925
def test_dump_json_conflicts_with_verbose(self):
926
options = self.parser.parse_args()
927
options.dump_json = True
928
options.verbose = True
929
with self.assertParseError():
930
self.check_option_syntax(options)
932
def test_dump_json_conflicts_with_action(self):
933
for action, value in self.actions.items():
934
options = self.parser.parse_args()
935
setattr(options, action, value)
936
options.dump_json = True
937
with self.assertParseError():
938
self.check_option_syntax(options)
940
def test_all_can_not_be_alone(self):
941
options = self.parser.parse_args()
943
with self.assertParseError():
944
self.check_option_syntax(options)
946
def test_all_is_ok_with_any_action(self):
947
for action, value in self.actions.items():
948
options = self.parser.parse_args()
949
setattr(options, action, value)
951
self.check_option_syntax(options)
953
def test_is_enabled_fails_without_client(self):
954
options = self.parser.parse_args()
955
options.is_enabled = True
956
with self.assertParseError():
957
self.check_option_syntax(options)
959
def test_is_enabled_works_with_one_client(self):
960
options = self.parser.parse_args()
961
options.is_enabled = True
962
options.client = ["foo"]
963
self.check_option_syntax(options)
965
def test_is_enabled_fails_with_two_clients(self):
966
options = self.parser.parse_args()
967
options.is_enabled = True
968
options.client = ["foo", "barbar"]
969
with self.assertParseError():
970
self.check_option_syntax(options)
972
def test_remove_can_only_be_combined_with_action_deny(self):
973
for action, value in self.actions.items():
974
if action in {"remove", "deny"}:
976
options = self.parser.parse_args()
977
setattr(options, action, value)
979
options.remove = True
980
with self.assertParseError():
981
self.check_option_syntax(options)
984
class Test_commands_from_options(unittest.TestCase):
986
self.parser = argparse.ArgumentParser()
987
add_command_line_options(self.parser)
989
def test_is_enabled(self):
990
self.assert_command_from_args(["--is-enabled", "foo"],
993
def assert_command_from_args(self, args, command_cls,
995
"""Assert that parsing ARGS should result in an instance of
996
COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
997
options = self.parser.parse_args(args)
998
check_option_syntax(self.parser, options)
999
commands = commands_from_options(options)
1000
self.assertEqual(len(commands), 1)
1001
command = commands[0]
1002
self.assertIsInstance(command, command_cls)
1003
for key, value in cmd_attrs.items():
1004
self.assertEqual(getattr(command, key), value)
1006
def test_is_enabled_short(self):
1007
self.assert_command_from_args(["-V", "foo"], IsEnabledCmd)
1009
def test_approve(self):
1010
self.assert_command_from_args(["--approve", "foo"],
1013
def test_approve_short(self):
1014
self.assert_command_from_args(["-A", "foo"], ApproveCmd)
1016
def test_deny(self):
1017
self.assert_command_from_args(["--deny", "foo"], DenyCmd)
1019
def test_deny_short(self):
1020
self.assert_command_from_args(["-D", "foo"], DenyCmd)
1022
def test_remove(self):
1023
self.assert_command_from_args(["--remove", "foo"],
1026
def test_deny_before_remove(self):
1027
options = self.parser.parse_args(["--deny", "--remove",
1029
check_option_syntax(self.parser, options)
1030
commands = commands_from_options(options)
1031
self.assertEqual(len(commands), 2)
1032
self.assertIsInstance(commands[0], DenyCmd)
1033
self.assertIsInstance(commands[1], RemoveCmd)
1035
def test_deny_before_remove_reversed(self):
1036
options = self.parser.parse_args(["--remove", "--deny",
1038
check_option_syntax(self.parser, options)
1039
commands = commands_from_options(options)
1040
self.assertEqual(len(commands), 2)
1041
self.assertIsInstance(commands[0], DenyCmd)
1042
self.assertIsInstance(commands[1], RemoveCmd)
1044
def test_remove_short(self):
1045
self.assert_command_from_args(["-r", "foo"], RemoveCmd)
1047
def test_dump_json(self):
1048
self.assert_command_from_args(["--dump-json"], DumpJSONCmd)
1050
def test_enable(self):
1051
self.assert_command_from_args(["--enable", "foo"], EnableCmd)
1053
def test_enable_short(self):
1054
self.assert_command_from_args(["-e", "foo"], EnableCmd)
1056
def test_disable(self):
1057
self.assert_command_from_args(["--disable", "foo"],
1060
def test_disable_short(self):
1061
self.assert_command_from_args(["-d", "foo"], DisableCmd)
1063
def test_bump_timeout(self):
1064
self.assert_command_from_args(["--bump-timeout", "foo"],
1067
def test_bump_timeout_short(self):
1068
self.assert_command_from_args(["-b", "foo"], BumpTimeoutCmd)
1070
def test_start_checker(self):
1071
self.assert_command_from_args(["--start-checker", "foo"],
1074
def test_stop_checker(self):
1075
self.assert_command_from_args(["--stop-checker", "foo"],
1078
def test_approve_by_default(self):
1079
self.assert_command_from_args(["--approve-by-default", "foo"],
1080
ApproveByDefaultCmd)
1082
def test_deny_by_default(self):
1083
self.assert_command_from_args(["--deny-by-default", "foo"],
1086
def test_checker(self):
1087
self.assert_command_from_args(["--checker", ":", "foo"],
1088
SetCheckerCmd, value_to_set=":")
1090
def test_checker_empty(self):
1091
self.assert_command_from_args(["--checker", "", "foo"],
1092
SetCheckerCmd, value_to_set="")
1094
def test_checker_short(self):
1095
self.assert_command_from_args(["-c", ":", "foo"],
1096
SetCheckerCmd, value_to_set=":")
1098
def test_host(self):
1099
self.assert_command_from_args(["--host", "foo.example.org",
1101
value_to_set="foo.example.org")
1103
def test_host_short(self):
1104
self.assert_command_from_args(["-H", "foo.example.org",
1106
value_to_set="foo.example.org")
1108
def test_secret_devnull(self):
1109
self.assert_command_from_args(["--secret", os.path.devnull,
1110
"foo"], SetSecretCmd,
1113
def test_secret_tempfile(self):
1114
with tempfile.NamedTemporaryFile(mode="r+b") as f:
1115
value = b"secret\0xyzzy\nbar"
1118
self.assert_command_from_args(["--secret", f.name,
1119
"foo"], SetSecretCmd,
1122
def test_secret_devnull_short(self):
1123
self.assert_command_from_args(["-s", os.path.devnull, "foo"],
1124
SetSecretCmd, value_to_set=b"")
1126
def test_secret_tempfile_short(self):
1127
with tempfile.NamedTemporaryFile(mode="r+b") as f:
1128
value = b"secret\0xyzzy\nbar"
1131
self.assert_command_from_args(["-s", f.name, "foo"],
1135
def test_timeout(self):
1136
self.assert_command_from_args(["--timeout", "PT5M", "foo"],
1138
value_to_set=300000)
1140
def test_timeout_short(self):
1141
self.assert_command_from_args(["-t", "PT5M", "foo"],
1143
value_to_set=300000)
1145
def test_extended_timeout(self):
1146
self.assert_command_from_args(["--extended-timeout", "PT15M",
1148
SetExtendedTimeoutCmd,
1149
value_to_set=900000)
1151
def test_interval(self):
1152
self.assert_command_from_args(["--interval", "PT2M", "foo"],
1154
value_to_set=120000)
1156
def test_interval_short(self):
1157
self.assert_command_from_args(["-i", "PT2M", "foo"],
1159
value_to_set=120000)
1161
def test_approval_delay(self):
1162
self.assert_command_from_args(["--approval-delay", "PT30S",
1163
"foo"], SetApprovalDelayCmd,
1166
def test_approval_duration(self):
1167
self.assert_command_from_args(["--approval-duration", "PT1S",
1168
"foo"], SetApprovalDurationCmd,
1171
def test_print_table(self):
1172
self.assert_command_from_args([], PrintTableCmd,
1175
def test_print_table_verbose(self):
1176
self.assert_command_from_args(["--verbose"], PrintTableCmd,
1179
def test_print_table_verbose_short(self):
1180
self.assert_command_from_args(["-v"], PrintTableCmd,
1184
class TestCmd(unittest.TestCase):
1185
"""Abstract class for tests of command classes"""
1189
class MockClient(object):
1190
def __init__(self, name, **attributes):
1191
self.__dbus_object_path__ = "/clients/{}".format(name)
1192
self.attributes = attributes
1193
self.attributes["Name"] = name
1195
def Set(self, interface, propname, value, dbus_interface):
1196
testcase.assertEqual(interface, client_dbus_interface)
1197
testcase.assertEqual(dbus_interface,
1198
dbus.PROPERTIES_IFACE)
1199
self.attributes[propname] = value
1200
def Get(self, interface, propname, dbus_interface):
1201
testcase.assertEqual(interface, client_dbus_interface)
1202
testcase.assertEqual(dbus_interface,
1203
dbus.PROPERTIES_IFACE)
1204
return self.attributes[propname]
1205
def Approve(self, approve, dbus_interface):
1206
testcase.assertEqual(dbus_interface,
1207
client_dbus_interface)
1208
self.calls.append(("Approve", (approve,
1210
self.client = MockClient(
1212
KeyID=("92ed150794387c03ce684574b1139a65"
1213
"94a34f895daaaf09fd8ea90a27cddb12"),
1215
Host="foo.example.org",
1216
Enabled=dbus.Boolean(True),
1218
LastCheckedOK="2019-02-03T00:00:00",
1219
Created="2019-01-02T00:00:00",
1221
Fingerprint=("778827225BA7DE539C5A"
1222
"7CFA59CFF7CDBD9A5920"),
1223
CheckerRunning=dbus.Boolean(False),
1224
LastEnabled="2019-01-03T00:00:00",
1225
ApprovalPending=dbus.Boolean(False),
1226
ApprovedByDefault=dbus.Boolean(True),
1227
LastApprovalRequest="",
1229
ApprovalDuration=1000,
1230
Checker="fping -q -- %(host)s",
1231
ExtendedTimeout=900000,
1232
Expires="2019-02-04T00:00:00",
1233
LastCheckerStatus=0)
1234
self.other_client = MockClient(
1236
KeyID=("0558568eedd67d622f5c83b35a115f79"
1237
"6ab612cff5ad227247e46c2b020f441c"),
1238
Secret=b"secretbar",
1240
Enabled=dbus.Boolean(True),
1242
LastCheckedOK="2019-02-04T00:00:00",
1243
Created="2019-01-03T00:00:00",
1245
Fingerprint=("3E393AEAEFB84C7E89E2"
1246
"F547B3A107558FCA3A27"),
1247
CheckerRunning=dbus.Boolean(True),
1248
LastEnabled="2019-01-04T00:00:00",
1249
ApprovalPending=dbus.Boolean(False),
1250
ApprovedByDefault=dbus.Boolean(False),
1251
LastApprovalRequest="2019-01-03T00:00:00",
1252
ApprovalDelay=30000,
1253
ApprovalDuration=93785000,
1255
ExtendedTimeout=900000,
1256
Expires="2019-02-05T00:00:00",
1257
LastCheckerStatus=-2)
1258
self.clients = collections.OrderedDict(
1260
("/clients/foo", self.client.attributes),
1261
("/clients/barbar", self.other_client.attributes),
1263
self.one_client = {"/clients/foo": self.client.attributes}
1269
def get_object(client_bus_name, path):
1270
self.assertEqual(client_bus_name, dbus_busname)
1272
# Note: "self" here is the TestCmd instance, not
1273
# the Bus instance, since this is a static method!
1274
"/clients/foo": self.client,
1275
"/clients/barbar": self.other_client,
1280
class TestIsEnabledCmd(TestCmd):
1281
def test_is_enabled(self):
1282
self.assertTrue(all(IsEnabledCmd().is_enabled(client,
1284
for client, properties
1285
in self.clients.items()))
1287
def test_is_enabled_run_exits_successfully(self):
1288
with self.assertRaises(SystemExit) as e:
1289
IsEnabledCmd().run(self.one_client)
1290
if e.exception.code is not None:
1291
self.assertEqual(e.exception.code, 0)
1293
self.assertIsNone(e.exception.code)
1295
def test_is_enabled_run_exits_with_failure(self):
1296
self.client.attributes["Enabled"] = dbus.Boolean(False)
1297
with self.assertRaises(SystemExit) as e:
1298
IsEnabledCmd().run(self.one_client)
1299
if isinstance(e.exception.code, int):
1300
self.assertNotEqual(e.exception.code, 0)
1302
self.assertIsNotNone(e.exception.code)
1305
class TestApproveCmd(TestCmd):
1306
def test_approve(self):
1307
ApproveCmd().run(self.clients, self.bus)
1308
for clientpath in self.clients:
1309
client = self.bus.get_object(dbus_busname, clientpath)
1310
self.assertIn(("Approve", (True, client_dbus_interface)),
1314
class TestDenyCmd(TestCmd):
1315
def test_deny(self):
1316
DenyCmd().run(self.clients, self.bus)
1317
for clientpath in self.clients:
1318
client = self.bus.get_object(dbus_busname, clientpath)
1319
self.assertIn(("Approve", (False, client_dbus_interface)),
1323
class TestRemoveCmd(TestCmd):
1324
def test_remove(self):
1325
class MockMandos(object):
1328
def RemoveClient(self, dbus_path):
1329
self.calls.append(("RemoveClient", (dbus_path,)))
1330
mandos = MockMandos()
1331
super(TestRemoveCmd, self).setUp()
1332
RemoveCmd().run(self.clients, self.bus, mandos)
1333
self.assertEqual(len(mandos.calls), 2)
1334
for clientpath in self.clients:
1335
self.assertIn(("RemoveClient", (clientpath,)),
1339
class TestDumpJSONCmd(TestCmd):
1341
self.expected_json = {
1344
"KeyID": ("92ed150794387c03ce684574b1139a65"
1345
"94a34f895daaaf09fd8ea90a27cddb12"),
1346
"Host": "foo.example.org",
1349
"LastCheckedOK": "2019-02-03T00:00:00",
1350
"Created": "2019-01-02T00:00:00",
1352
"Fingerprint": ("778827225BA7DE539C5A"
1353
"7CFA59CFF7CDBD9A5920"),
1354
"CheckerRunning": False,
1355
"LastEnabled": "2019-01-03T00:00:00",
1356
"ApprovalPending": False,
1357
"ApprovedByDefault": True,
1358
"LastApprovalRequest": "",
1360
"ApprovalDuration": 1000,
1361
"Checker": "fping -q -- %(host)s",
1362
"ExtendedTimeout": 900000,
1363
"Expires": "2019-02-04T00:00:00",
1364
"LastCheckerStatus": 0,
1368
"KeyID": ("0558568eedd67d622f5c83b35a115f79"
1369
"6ab612cff5ad227247e46c2b020f441c"),
1370
"Host": "192.0.2.3",
1373
"LastCheckedOK": "2019-02-04T00:00:00",
1374
"Created": "2019-01-03T00:00:00",
1376
"Fingerprint": ("3E393AEAEFB84C7E89E2"
1377
"F547B3A107558FCA3A27"),
1378
"CheckerRunning": True,
1379
"LastEnabled": "2019-01-04T00:00:00",
1380
"ApprovalPending": False,
1381
"ApprovedByDefault": False,
1382
"LastApprovalRequest": "2019-01-03T00:00:00",
1383
"ApprovalDelay": 30000,
1384
"ApprovalDuration": 93785000,
1386
"ExtendedTimeout": 900000,
1387
"Expires": "2019-02-05T00:00:00",
1388
"LastCheckerStatus": -2,
602
class Test_TableOfClients(unittest.TestCase):
608
"Bool": "A D-BUS Boolean",
609
"NonDbusBoolean": "A Non-D-BUS Boolean",
610
"Integer": "An Integer",
611
"Timeout": "Timedelta 1",
612
"Interval": "Timedelta 2",
613
"ApprovalDelay": "Timedelta 3",
614
"ApprovalDuration": "Timedelta 4",
615
"ExtendedTimeout": "Timedelta 5",
616
"String": "A String",
1391
return super(TestDumpJSONCmd, self).setUp()
1393
def test_normal(self):
1394
output = DumpJSONCmd().output(self.clients.values())
1395
json_data = json.loads(output)
1396
self.assertDictEqual(json_data, self.expected_json)
1398
def test_one_client(self):
1399
output = DumpJSONCmd().output(self.one_client.values())
1400
json_data = json.loads(output)
1401
expected_json = {"foo": self.expected_json["foo"]}
1402
self.assertDictEqual(json_data, expected_json)
1405
class TestPrintTableCmd(TestCmd):
1406
def test_normal(self):
1407
output = PrintTableCmd().output(self.clients.values())
1408
expected_output = "\n".join((
1409
"Name Enabled Timeout Last Successful Check",
1410
"foo Yes 00:05:00 2019-02-03T00:00:00 ",
1411
"barbar Yes 00:05:00 2019-02-04T00:00:00 ",
1413
self.assertEqual(output, expected_output)
1415
def test_verbose(self):
1416
output = PrintTableCmd(verbose=True).output(
1417
self.clients.values())
1432
"Last Successful Check ",
1433
"2019-02-03T00:00:00 ",
1434
"2019-02-04T00:00:00 ",
1437
"2019-01-02T00:00:00 ",
1438
"2019-01-03T00:00:00 ",
1450
("92ed150794387c03ce684574b1139a6594a34f895daaaf09fd8"
1452
("0558568eedd67d622f5c83b35a115f796ab612cff5ad227247e"
1456
"778827225BA7DE539C5A7CFA59CFF7CDBD9A5920 ",
1457
"3E393AEAEFB84C7E89E2F547B3A107558FCA3A27 ",
1459
"Check Is Running ",
1464
"2019-01-03T00:00:00 ",
1465
"2019-01-04T00:00:00 ",
1467
"Approval Is Pending ",
1471
"Approved By Default ",
1475
"Last Approval Request ",
1477
"2019-01-03T00:00:00 ",
1483
"Approval Duration ",
1488
"fping -q -- %(host)s ",
1491
"Extended Timeout ",
1496
"2019-02-04T00:00:00 ",
1497
"2019-02-05T00:00:00 ",
1499
"Last Checker Status",
1504
num_lines = max(len(rows) for rows in columns)
1505
expected_output = "\n".join("".join(rows[line]
1506
for rows in columns)
1507
for line in range(num_lines))
1508
self.assertEqual(output, expected_output)
1510
def test_one_client(self):
1511
output = PrintTableCmd().output(self.one_client.values())
1512
expected_output = "\n".join((
1513
"Name Enabled Timeout Last Successful Check",
1514
"foo Yes 00:05:00 2019-02-03T00:00:00 ",
1516
self.assertEqual(output, expected_output)
1519
class TestPropertyCmd(TestCmd):
1520
"""Abstract class for tests of PropertyCmd classes"""
1522
if not hasattr(self, "command"):
1524
values_to_get = getattr(self, "values_to_get",
1526
for value_to_set, value_to_get in zip(self.values_to_set,
1528
for clientpath in self.clients:
1529
client = self.bus.get_object(dbus_busname, clientpath)
1530
old_value = client.attributes[self.propname]
1531
self.assertNotIsInstance(old_value, self.Unique)
1532
client.attributes[self.propname] = self.Unique()
1533
self.run_command(value_to_set, self.clients)
1534
for clientpath in self.clients:
1535
client = self.bus.get_object(dbus_busname, clientpath)
1536
value = client.attributes[self.propname]
1537
self.assertNotIsInstance(value, self.Unique)
1538
self.assertEqual(value, value_to_get)
1540
class Unique(object):
1541
"""Class for objects which exist only to be unique objects,
1542
since unittest.mock.sentinel only exists in Python 3.3"""
1544
def run_command(self, value, clients):
1545
self.command().run(clients, self.bus)
1548
class TestEnableCmd(TestPropertyCmd):
1550
propname = "Enabled"
1551
values_to_set = [dbus.Boolean(True)]
1554
class TestDisableCmd(TestPropertyCmd):
1555
command = DisableCmd
1556
propname = "Enabled"
1557
values_to_set = [dbus.Boolean(False)]
1560
class TestBumpTimeoutCmd(TestPropertyCmd):
1561
command = BumpTimeoutCmd
1562
propname = "LastCheckedOK"
1563
values_to_set = [""]
1566
class TestStartCheckerCmd(TestPropertyCmd):
1567
command = StartCheckerCmd
1568
propname = "CheckerRunning"
1569
values_to_set = [dbus.Boolean(True)]
1572
class TestStopCheckerCmd(TestPropertyCmd):
1573
command = StopCheckerCmd
1574
propname = "CheckerRunning"
1575
values_to_set = [dbus.Boolean(False)]
1578
class TestApproveByDefaultCmd(TestPropertyCmd):
1579
command = ApproveByDefaultCmd
1580
propname = "ApprovedByDefault"
1581
values_to_set = [dbus.Boolean(True)]
1584
class TestDenyByDefaultCmd(TestPropertyCmd):
1585
command = DenyByDefaultCmd
1586
propname = "ApprovedByDefault"
1587
values_to_set = [dbus.Boolean(False)]
1590
class TestPropertyValueCmd(TestPropertyCmd):
1591
"""Abstract class for tests of PropertyValueCmd classes"""
1594
if type(self) is TestPropertyValueCmd:
1596
return super(TestPropertyValueCmd, self).runTest()
1598
def run_command(self, value, clients):
1599
self.command(value).run(clients, self.bus)
1602
class TestSetCheckerCmd(TestPropertyValueCmd):
1603
command = SetCheckerCmd
1604
propname = "Checker"
1605
values_to_set = ["", ":", "fping -q -- %s"]
1608
class TestSetHostCmd(TestPropertyValueCmd):
1609
command = SetHostCmd
1611
values_to_set = ["192.0.2.3", "foo.example.org"]
1614
class TestSetSecretCmd(TestPropertyValueCmd):
1615
command = SetSecretCmd
1617
values_to_set = [io.BytesIO(b""),
1618
io.BytesIO(b"secret\0xyzzy\nbar")]
1619
values_to_get = [b"", b"secret\0xyzzy\nbar"]
1622
class TestSetTimeoutCmd(TestPropertyValueCmd):
1623
command = SetTimeoutCmd
1624
propname = "Timeout"
1625
values_to_set = [datetime.timedelta(),
1626
datetime.timedelta(minutes=5),
1627
datetime.timedelta(seconds=1),
1628
datetime.timedelta(weeks=1),
1629
datetime.timedelta(weeks=52)]
1630
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1633
class TestSetExtendedTimeoutCmd(TestPropertyValueCmd):
1634
command = SetExtendedTimeoutCmd
1635
propname = "ExtendedTimeout"
1636
values_to_set = [datetime.timedelta(),
1637
datetime.timedelta(minutes=5),
1638
datetime.timedelta(seconds=1),
1639
datetime.timedelta(weeks=1),
1640
datetime.timedelta(weeks=52)]
1641
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1644
class TestSetIntervalCmd(TestPropertyValueCmd):
1645
command = SetIntervalCmd
1646
propname = "Interval"
1647
values_to_set = [datetime.timedelta(),
1648
datetime.timedelta(minutes=5),
1649
datetime.timedelta(seconds=1),
1650
datetime.timedelta(weeks=1),
1651
datetime.timedelta(weeks=52)]
1652
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1655
class TestSetApprovalDelayCmd(TestPropertyValueCmd):
1656
command = SetApprovalDelayCmd
1657
propname = "ApprovalDelay"
1658
values_to_set = [datetime.timedelta(),
1659
datetime.timedelta(minutes=5),
1660
datetime.timedelta(seconds=1),
1661
datetime.timedelta(weeks=1),
1662
datetime.timedelta(weeks=52)]
1663
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1666
class TestSetApprovalDurationCmd(TestPropertyValueCmd):
1667
command = SetApprovalDurationCmd
1668
propname = "ApprovalDuration"
1669
values_to_set = [datetime.timedelta(),
1670
datetime.timedelta(minutes=5),
1671
datetime.timedelta(seconds=1),
1672
datetime.timedelta(weeks=1),
1673
datetime.timedelta(weeks=52)]
1674
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
618
self.keywords = ["Attr1", "AttrTwo"]
624
"Bool": dbus.Boolean(False),
625
"NonDbusBoolean": False,
629
"ApprovalDelay": 2000,
630
"ApprovalDuration": 3000,
631
"ExtendedTimeout": 4000,
638
"Bool": dbus.Boolean(True),
639
"NonDbusBoolean": True,
642
"Interval": 93786000,
643
"ApprovalDelay": 93787000,
644
"ApprovalDuration": 93788000,
645
"ExtendedTimeout": 93789000,
646
"String": "A huge string which will not fit," * 10,
649
def test_short_header(self):
650
text = str(TableOfClients(self.clients, self.keywords,
657
self.assertEqual(text, expected_text)
658
def test_booleans(self):
659
keywords = ["Bool", "NonDbusBoolean"]
660
text = str(TableOfClients(self.clients, keywords,
663
A D-BUS Boolean A Non-D-BUS Boolean
667
self.assertEqual(text, expected_text)
668
def test_milliseconds_detection(self):
669
keywords = ["Integer", "Timeout", "Interval", "ApprovalDelay",
670
"ApprovalDuration", "ExtendedTimeout"]
671
text = str(TableOfClients(self.clients, keywords,
674
An Integer Timedelta 1 Timedelta 2 Timedelta 3 Timedelta 4 Timedelta 5
675
0 00:00:00 00:00:01 00:00:02 00:00:03 00:00:04
676
1 1T02:03:05 1T02:03:06 1T02:03:07 1T02:03:08 1T02:03:09
678
self.assertEqual(text, expected_text)
679
def test_empty_and_long_string_values(self):
680
keywords = ["String"]
681
text = str(TableOfClients(self.clients, keywords,
686
A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,
688
self.assertEqual(text, expected_text)