/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos-ctl

  • Committer: Teddy Hogeborn
  • Date: 2019-03-12 18:11:55 UTC
  • Revision ID: teddy@recompile.se-20190312181155-q8q3moo97u7ta2za
mandos-ctl: White space and other non-semantic changes only

* mandos-ctl: Break long lines.

Show diffs side-by-side

added added

removed removed

Lines of Context:
44
44
import logging
45
45
import io
46
46
import tempfile
 
47
import contextlib
47
48
 
48
49
import dbus
49
50
 
63
64
 
64
65
locale.setlocale(locale.LC_ALL, "")
65
66
 
66
 
domain = "se.recompile"
67
 
busname = domain + ".Mandos"
68
 
server_path = "/"
69
 
server_interface = domain + ".Mandos"
70
 
client_interface = domain + ".Mandos.Client"
 
67
dbus_busname_domain = "se.recompile"
 
68
dbus_busname = dbus_busname_domain + ".Mandos"
 
69
server_dbus_path = "/"
 
70
server_dbus_interface = dbus_busname_domain + ".Mandos"
 
71
client_dbus_interface = dbus_busname_domain + ".Mandos.Client"
 
72
del dbus_busname_domain
71
73
version = "1.8.3"
72
74
 
73
75
 
275
277
# Abstract classes first
276
278
class Command(object):
277
279
    """Abstract class for commands"""
278
 
    def run(self, mandos, clients):
 
280
    def run(self, clients, bus=None, mandos=None):
279
281
        """Normal commands should implement run_on_one_client(), but
280
282
        commands which want to operate on all clients at the same time
281
283
        can override this run() method instead."""
282
284
        self.mandos = mandos
283
 
        for client, properties in clients.items():
 
285
        for clientpath, properties in clients.items():
 
286
            log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
287
                      dbus_busname, str(clientpath))
 
288
            client = bus.get_object(dbus_busname, clientpath)
284
289
            self.run_on_one_client(client, properties)
285
290
 
286
291
class PrintCmd(Command):
292
297
                    "LastApprovalRequest", "ApprovalDelay",
293
298
                    "ApprovalDuration", "Checker", "ExtendedTimeout",
294
299
                    "Expires", "LastCheckerStatus")
295
 
    def run(self, mandos, clients):
296
 
        print(self.output(clients))
 
300
    def run(self, clients, bus=None, mandos=None):
 
301
        print(self.output(clients.values()))
 
302
    def output(self, clients):
 
303
        raise NotImplementedError()
297
304
 
298
305
class PropertyCmd(Command):
299
306
    """Abstract class for Actions for setting one client property"""
300
307
    def run_on_one_client(self, client, properties):
301
308
        """Set the Client's D-Bus property"""
302
 
        client.Set(client_interface, self.property, self.value_to_set,
 
309
        log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", dbus_busname,
 
310
                  client.__dbus_object_path__,
 
311
                  dbus.PROPERTIES_IFACE, client_dbus_interface,
 
312
                  self.propname, self.value_to_set
 
313
                  if not isinstance(self.value_to_set, dbus.Boolean)
 
314
                  else bool(self.value_to_set))
 
315
        client.Set(client_dbus_interface, self.propname,
 
316
                   self.value_to_set,
303
317
                   dbus_interface=dbus.PROPERTIES_IFACE)
 
318
    @property
 
319
    def propname(self):
 
320
        raise NotImplementedError()
304
321
 
305
 
class ValueArgumentMixIn(object):
306
 
    """Mixin class for commands taking a value as argument"""
 
322
class PropertyValueCmd(PropertyCmd):
 
323
    """Abstract class for PropertyCmd recieving a value as argument"""
307
324
    def __init__(self, value):
308
325
        self.value_to_set = value
309
326
 
310
 
class MillisecondsValueArgumentMixIn(ValueArgumentMixIn):
311
 
    """Mixin class for commands taking a value argument as
312
 
    milliseconds."""
 
327
class MillisecondsPropertyValueArgumentCmd(PropertyValueCmd):
 
328
    """Abstract class for PropertyValueCmd taking a value argument as
 
329
a datetime.timedelta() but should store it as milliseconds."""
313
330
    @property
314
331
    def value_to_set(self):
315
332
        return self._vts
316
333
    @value_to_set.setter
317
334
    def value_to_set(self, value):
318
 
        """When setting, convert value to a datetime.timedelta"""
 
335
        """When setting, convert value from a datetime.timedelta"""
319
336
        self._vts = int(round(value.total_seconds() * 1000))
320
337
 
321
338
# Actual (non-abstract) command classes
325
342
        self.verbose = verbose
326
343
 
327
344
    def output(self, clients):
328
 
        default_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
 
345
        default_keywords = ("Name", "Enabled", "Timeout",
 
346
                            "LastCheckedOK")
329
347
        keywords = default_keywords
330
348
        if self.verbose:
331
349
            keywords = self.all_keywords
332
 
        return str(self.TableOfClients(clients.values(), keywords))
 
350
        return str(self.TableOfClients(clients, keywords))
333
351
 
334
352
    class TableOfClients(object):
335
353
        tableheaders = {
421
439
        return value
422
440
 
423
441
class IsEnabledCmd(Command):
424
 
    def run_on_one_client(self, client, properties):
 
442
    def run(self, clients, bus=None, mandos=None):
 
443
        client, properties = next(iter(clients.items()))
425
444
        if self.is_enabled(client, properties):
426
445
            sys.exit(0)
427
446
        sys.exit(1)
428
447
    def is_enabled(self, client, properties):
429
 
        return bool(properties["Enabled"])
 
448
        return properties["Enabled"]
430
449
 
431
450
class RemoveCmd(Command):
432
451
    def run_on_one_client(self, client, properties):
 
452
        log.debug("D-Bus: %s:%s:%s.RemoveClient(%r)", dbus_busname,
 
453
                  server_dbus_path, server_dbus_interface,
 
454
                  str(client.__dbus_object_path__))
433
455
        self.mandos.RemoveClient(client.__dbus_object_path__)
434
456
 
435
457
class ApproveCmd(Command):
436
458
    def run_on_one_client(self, client, properties):
 
459
        log.debug("D-Bus: %s:%s:%s.Approve(True)", dbus_busname,
 
460
                  client.__dbus_object_path__, client_dbus_interface)
437
461
        client.Approve(dbus.Boolean(True),
438
 
                       dbus_interface=client_interface)
 
462
                       dbus_interface=client_dbus_interface)
439
463
 
440
464
class DenyCmd(Command):
441
465
    def run_on_one_client(self, client, properties):
 
466
        log.debug("D-Bus: %s:%s:%s.Approve(False)", dbus_busname,
 
467
                  client.__dbus_object_path__, client_dbus_interface)
442
468
        client.Approve(dbus.Boolean(False),
443
 
                       dbus_interface=client_interface)
 
469
                       dbus_interface=client_dbus_interface)
444
470
 
445
471
class EnableCmd(PropertyCmd):
446
 
    property = "Enabled"
 
472
    propname = "Enabled"
447
473
    value_to_set = dbus.Boolean(True)
448
474
 
449
475
class DisableCmd(PropertyCmd):
450
 
    property = "Enabled"
 
476
    propname = "Enabled"
451
477
    value_to_set = dbus.Boolean(False)
452
478
 
453
479
class BumpTimeoutCmd(PropertyCmd):
454
 
    property = "LastCheckedOK"
 
480
    propname = "LastCheckedOK"
455
481
    value_to_set = ""
456
482
 
457
483
class StartCheckerCmd(PropertyCmd):
458
 
    property = "CheckerRunning"
 
484
    propname = "CheckerRunning"
459
485
    value_to_set = dbus.Boolean(True)
460
486
 
461
487
class StopCheckerCmd(PropertyCmd):
462
 
    property = "CheckerRunning"
 
488
    propname = "CheckerRunning"
463
489
    value_to_set = dbus.Boolean(False)
464
490
 
465
491
class ApproveByDefaultCmd(PropertyCmd):
466
 
    property = "ApprovedByDefault"
 
492
    propname = "ApprovedByDefault"
467
493
    value_to_set = dbus.Boolean(True)
468
494
 
469
495
class DenyByDefaultCmd(PropertyCmd):
470
 
    property = "ApprovedByDefault"
 
496
    propname = "ApprovedByDefault"
471
497
    value_to_set = dbus.Boolean(False)
472
498
 
473
 
class SetCheckerCmd(PropertyCmd, ValueArgumentMixIn):
474
 
    property = "Checker"
475
 
 
476
 
class SetHostCmd(PropertyCmd, ValueArgumentMixIn):
477
 
    property = "Host"
478
 
 
479
 
class SetSecretCmd(PropertyCmd, ValueArgumentMixIn):
 
499
class SetCheckerCmd(PropertyValueCmd):
 
500
    propname = "Checker"
 
501
 
 
502
class SetHostCmd(PropertyValueCmd):
 
503
    propname = "Host"
 
504
 
 
505
class SetSecretCmd(PropertyValueCmd):
 
506
    propname = "Secret"
480
507
    @property
481
508
    def value_to_set(self):
482
509
        return self._vts
485
512
        """When setting, read data from supplied file object"""
486
513
        self._vts = value.read()
487
514
        value.close()
488
 
    property = "Secret"
489
 
 
490
 
class SetTimeoutCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
491
 
    property = "Timeout"
492
 
 
493
 
class SetExtendedTimeoutCmd(PropertyCmd,
494
 
                            MillisecondsValueArgumentMixIn):
495
 
    property = "ExtendedTimeout"
496
 
 
497
 
class SetIntervalCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
498
 
    property = "Interval"
499
 
 
500
 
class SetApprovalDelayCmd(PropertyCmd,
501
 
                          MillisecondsValueArgumentMixIn):
502
 
    property = "ApprovalDelay"
503
 
 
504
 
class SetApprovalDurationCmd(PropertyCmd,
505
 
                             MillisecondsValueArgumentMixIn):
506
 
    property = "ApprovalDuration"
 
515
 
 
516
class SetTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
 
517
    propname = "Timeout"
 
518
 
 
519
class SetExtendedTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
 
520
    propname = "ExtendedTimeout"
 
521
 
 
522
class SetIntervalCmd(MillisecondsPropertyValueArgumentCmd):
 
523
    propname = "Interval"
 
524
 
 
525
class SetApprovalDelayCmd(MillisecondsPropertyValueArgumentCmd):
 
526
    propname = "ApprovalDelay"
 
527
 
 
528
class SetApprovalDurationCmd(MillisecondsPropertyValueArgumentCmd):
 
529
    propname = "ApprovalDuration"
507
530
 
508
531
def add_command_line_options(parser):
509
532
    parser.add_argument("--version", action="version",
565
588
        help="Approve any current client request")
566
589
    approve_deny.add_argument("-D", "--deny", action="store_true",
567
590
                              help="Deny any current client request")
 
591
    parser.add_argument("--debug", action="store_true",
 
592
                        help="Debug mode (show D-Bus commands)")
568
593
    parser.add_argument("--check", action="store_true",
569
594
                        help="Run self-test")
570
595
    parser.add_argument("client", nargs="*", help="Client name")
595
620
    if options.is_enabled:
596
621
        commands.append(IsEnabledCmd())
597
622
 
598
 
    if options.remove:
599
 
        commands.append(RemoveCmd())
600
 
 
601
623
    if options.checker is not None:
602
624
        commands.append(SetCheckerCmd(options.checker))
603
625
 
636
658
    if options.deny:
637
659
        commands.append(DenyCmd())
638
660
 
 
661
    if options.remove:
 
662
        commands.append(RemoveCmd())
 
663
 
639
664
    # If no command option has been given, show table of clients,
640
665
    # optionally verbosely
641
666
    if not commands:
644
669
    return commands
645
670
 
646
671
 
647
 
def main():
648
 
    parser = argparse.ArgumentParser()
649
 
 
650
 
    add_command_line_options(parser)
651
 
 
652
 
    options = parser.parse_args()
 
672
def check_option_syntax(parser, options):
 
673
    """Apply additional restrictions on options, not expressible in
 
674
argparse"""
653
675
 
654
676
    def has_actions(options):
655
677
        return any((options.enable,
682
704
        parser.error("--all requires an action.")
683
705
    if options.is_enabled and len(options.client) > 1:
684
706
        parser.error("--is-enabled requires exactly one client")
 
707
    if options.remove:
 
708
        options.remove = False
 
709
        if has_actions(options) and not options.deny:
 
710
            parser.error("--remove can only be combined with --deny")
 
711
        options.remove = True
 
712
 
 
713
 
 
714
def main():
 
715
    parser = argparse.ArgumentParser()
 
716
 
 
717
    add_command_line_options(parser)
 
718
 
 
719
    options = parser.parse_args()
 
720
 
 
721
    check_option_syntax(parser, options)
685
722
 
686
723
    clientnames = options.client
687
724
 
 
725
    if options.debug:
 
726
        log.setLevel(logging.DEBUG)
 
727
 
688
728
    try:
689
729
        bus = dbus.SystemBus()
690
 
        mandos_dbus_objc = bus.get_object(busname, server_path)
 
730
        log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
731
                  dbus_busname, server_dbus_path)
 
732
        mandos_dbus_objc = bus.get_object(dbus_busname,
 
733
                                          server_dbus_path)
691
734
    except dbus.exceptions.DBusException:
692
735
        log.critical("Could not connect to Mandos server")
693
736
        sys.exit(1)
694
737
 
695
738
    mandos_serv = dbus.Interface(mandos_dbus_objc,
696
 
                                 dbus_interface=server_interface)
 
739
                                 dbus_interface=server_dbus_interface)
697
740
    mandos_serv_object_manager = dbus.Interface(
698
741
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
699
742
 
705
748
    dbus_filter = NullFilter()
706
749
    try:
707
750
        dbus_logger.addFilter(dbus_filter)
708
 
        mandos_clients = {path: ifs_and_props[client_interface]
 
751
        log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", dbus_busname,
 
752
                  server_dbus_path, dbus.OBJECT_MANAGER_IFACE)
 
753
        mandos_clients = {path: ifs_and_props[client_dbus_interface]
709
754
                          for path, ifs_and_props in
710
755
                          mandos_serv_object_manager
711
756
                          .GetManagedObjects().items()
712
 
                          if client_interface in ifs_and_props}
 
757
                          if client_dbus_interface in ifs_and_props}
713
758
    except dbus.exceptions.DBusException as e:
714
759
        log.critical("Failed to access Mandos server through D-Bus:"
715
760
                     "\n%s", e)
722
767
    clients = {}
723
768
 
724
769
    if not clientnames:
725
 
        clients = {bus.get_object(busname, path): properties
726
 
                   for path, properties in mandos_clients.items()}
 
770
        clients = {objpath: properties
 
771
                   for objpath, properties in mandos_clients.items()}
727
772
    else:
728
773
        for name in clientnames:
729
 
            for path, client in mandos_clients.items():
730
 
                if client["Name"] == name:
731
 
                    client_objc = bus.get_object(busname, path)
732
 
                    clients[client_objc] = client
 
774
            for objpath, properties in mandos_clients.items():
 
775
                if properties["Name"] == name:
 
776
                    clients[objpath] = properties
733
777
                    break
734
778
            else:
735
779
                log.critical("Client not found on server: %r", name)
738
782
    # Run all commands on clients
739
783
    commands = commands_from_options(options)
740
784
    for command in commands:
741
 
        command.run(mandos_serv, clients)
 
785
        command.run(clients, bus, mandos_serv)
742
786
 
743
787
 
744
788
class Test_milliseconds_to_string(unittest.TestCase):
793
837
        testcase = self
794
838
        class MockClient(object):
795
839
            def __init__(self, name, **attributes):
796
 
                self.__dbus_object_path__ = "objpath_{}".format(name)
 
840
                self.__dbus_object_path__ = "/clients/{}".format(name)
797
841
                self.attributes = attributes
798
842
                self.attributes["Name"] = name
799
843
                self.calls = []
800
 
            def Set(self, interface, property, value, dbus_interface):
801
 
                testcase.assertEqual(interface, client_interface)
802
 
                testcase.assertEqual(dbus_interface,
803
 
                                     dbus.PROPERTIES_IFACE)
804
 
                self.attributes[property] = value
805
 
            def Get(self, interface, property, dbus_interface):
806
 
                testcase.assertEqual(interface, client_interface)
807
 
                testcase.assertEqual(dbus_interface,
808
 
                                     dbus.PROPERTIES_IFACE)
809
 
                return self.attributes[property]
 
844
            def Set(self, interface, propname, value, dbus_interface):
 
845
                testcase.assertEqual(interface, client_dbus_interface)
 
846
                testcase.assertEqual(dbus_interface,
 
847
                                     dbus.PROPERTIES_IFACE)
 
848
                self.attributes[propname] = value
 
849
            def Get(self, interface, propname, dbus_interface):
 
850
                testcase.assertEqual(interface, client_dbus_interface)
 
851
                testcase.assertEqual(dbus_interface,
 
852
                                     dbus.PROPERTIES_IFACE)
 
853
                return self.attributes[propname]
810
854
            def Approve(self, approve, dbus_interface):
811
 
                testcase.assertEqual(dbus_interface, client_interface)
 
855
                testcase.assertEqual(dbus_interface,
 
856
                                     client_dbus_interface)
812
857
                self.calls.append(("Approve", (approve,
813
858
                                               dbus_interface)))
814
859
        self.client = MockClient(
861
906
            LastCheckerStatus=-2)
862
907
        self.clients =  collections.OrderedDict(
863
908
            [
864
 
                (self.client, self.client.attributes),
865
 
                (self.other_client, self.other_client.attributes),
 
909
                ("/clients/foo", self.client.attributes),
 
910
                ("/clients/barbar", self.other_client.attributes),
866
911
            ])
867
 
        self.one_client = {self.client: self.client.attributes}
 
912
        self.one_client = {"/clients/foo": self.client.attributes}
 
913
    @property
 
914
    def bus(self):
 
915
        class Bus(object):
 
916
            @staticmethod
 
917
            def get_object(client_bus_name, path):
 
918
                self.assertEqual(client_bus_name, dbus_busname)
 
919
                return {
 
920
                    "/clients/foo": self.client,
 
921
                    "/clients/barbar": self.other_client,
 
922
                }[path]
 
923
        return Bus()
868
924
 
869
925
class TestPrintTableCmd(TestCmd):
870
926
    def test_normal(self):
871
 
        output = PrintTableCmd().output(self.clients)
872
 
        expected_output = """
873
 
Name   Enabled Timeout  Last Successful Check
874
 
foo    Yes     00:05:00 2019-02-03T00:00:00  
875
 
barbar Yes     00:05:00 2019-02-04T00:00:00  
876
 
"""[1:-1]
 
927
        output = PrintTableCmd().output(self.clients.values())
 
928
        expected_output = "\n".join((
 
929
            "Name   Enabled Timeout  Last Successful Check",
 
930
            "foo    Yes     00:05:00 2019-02-03T00:00:00  ",
 
931
            "barbar Yes     00:05:00 2019-02-04T00:00:00  ",
 
932
        ))
877
933
        self.assertEqual(output, expected_output)
878
934
    def test_verbose(self):
879
 
        output = PrintTableCmd(verbose=True).output(self.clients)
880
 
        expected_output = """
881
 
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
882
 
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                  
883
 
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                 
884
 
"""[1:-1]
 
935
        output = PrintTableCmd(verbose=True).output(
 
936
            self.clients.values())
 
937
        expected_output = "\n".join((
 
938
            # First line (headers)
 
939
            "Name   Enabled Timeout  Last Successful Check Created   "
 
940
            "          Interval Host            Key ID               "
 
941
            "                                            Fingerprint "
 
942
            "                             Check Is Running Last Enabl"
 
943
            "ed        Approval Is Pending Approved By Default Last A"
 
944
            "pproval Request Approval Delay Approval Duration Checker"
 
945
            "              Extended Timeout Expires             Last "
 
946
            "Checker Status",
 
947
            # Second line (client "foo")
 
948
            "foo    Yes     00:05:00 2019-02-03T00:00:00   2019-01-02"
 
949
            "T00:00:00 00:02:00 foo.example.org 92ed150794387c03ce684"
 
950
            "574b1139a6594a34f895daaaf09fd8ea90a27cddb12 778827225BA7"
 
951
            "DE539C5A7CFA59CFF7CDBD9A5920 No               2019-01-03"
 
952
            "T00:00:00 No                  Yes                       "
 
953
            "                00:00:00       00:00:01          fping -"
 
954
            "q -- %(host)s 00:15:00         2019-02-04T00:00:00 0    "
 
955
            "              ",
 
956
            # Third line (client "barbar")
 
957
            "barbar Yes     00:05:00 2019-02-04T00:00:00   2019-01-03"
 
958
            "T00:00:00 00:02:00 192.0.2.3       0558568eedd67d622f5c8"
 
959
            "3b35a115f796ab612cff5ad227247e46c2b020f441c 3E393AEAEFB8"
 
960
            "4C7E89E2F547B3A107558FCA3A27 Yes              2019-01-04"
 
961
            "T00:00:00 No                  No                  2019-0"
 
962
            "1-03T00:00:00   00:00:30       00:00:01          :      "
 
963
            "              00:15:00         2019-02-05T00:00:00 -2   "
 
964
            "              ",
 
965
        ))
885
966
        self.assertEqual(output, expected_output)
886
967
    def test_one_client(self):
887
 
        output = PrintTableCmd().output(self.one_client)
 
968
        output = PrintTableCmd().output(self.one_client.values())
888
969
        expected_output = """
889
970
Name Enabled Timeout  Last Successful Check
890
971
foo  Yes     00:05:00 2019-02-03T00:00:00  
955
1036
 
956
1037
class TestIsEnabledCmd(TestCmd):
957
1038
    def test_is_enabled(self):
958
 
        self.assertTrue(all(IsEnabledCmd().is_enabled(client, properties)
959
 
                            for client, properties in self.clients.items()))
 
1039
        self.assertTrue(all(IsEnabledCmd().is_enabled(client,
 
1040
                                                      properties)
 
1041
                            for client, properties
 
1042
                            in self.clients.items()))
960
1043
    def test_is_enabled_run_exits_successfully(self):
961
1044
        with self.assertRaises(SystemExit) as e:
962
 
            IsEnabledCmd().run(None, self.one_client)
 
1045
            IsEnabledCmd().run(self.one_client)
963
1046
        if e.exception.code is not None:
964
1047
            self.assertEqual(e.exception.code, 0)
965
1048
        else:
967
1050
    def test_is_enabled_run_exits_with_failure(self):
968
1051
        self.client.attributes["Enabled"] = dbus.Boolean(False)
969
1052
        with self.assertRaises(SystemExit) as e:
970
 
            IsEnabledCmd().run(None, self.one_client)
 
1053
            IsEnabledCmd().run(self.one_client)
971
1054
        if isinstance(e.exception.code, int):
972
1055
            self.assertNotEqual(e.exception.code, 0)
973
1056
        else:
982
1065
                self.calls.append(("RemoveClient", (dbus_path,)))
983
1066
        mandos = MockMandos()
984
1067
        super(TestRemoveCmd, self).setUp()
985
 
        RemoveCmd().run(mandos, self.clients)
 
1068
        RemoveCmd().run(self.clients, self.bus, mandos)
986
1069
        self.assertEqual(len(mandos.calls), 2)
987
 
        for client in self.clients:
988
 
            self.assertIn(("RemoveClient",
989
 
                           (client.__dbus_object_path__,)),
 
1070
        for clientpath in self.clients:
 
1071
            self.assertIn(("RemoveClient", (clientpath,)),
990
1072
                          mandos.calls)
991
1073
 
992
1074
class TestApproveCmd(TestCmd):
993
1075
    def test_approve(self):
994
 
        ApproveCmd().run(None, self.clients)
995
 
        for client in self.clients:
996
 
            self.assertIn(("Approve", (True, client_interface)),
 
1076
        ApproveCmd().run(self.clients, self.bus)
 
1077
        for clientpath in self.clients:
 
1078
            client = self.bus.get_object(dbus_busname, clientpath)
 
1079
            self.assertIn(("Approve", (True, client_dbus_interface)),
997
1080
                          client.calls)
998
1081
 
999
1082
class TestDenyCmd(TestCmd):
1000
1083
    def test_deny(self):
1001
 
        DenyCmd().run(None, self.clients)
1002
 
        for client in self.clients:
1003
 
            self.assertIn(("Approve", (False, client_interface)),
 
1084
        DenyCmd().run(self.clients, self.bus)
 
1085
        for clientpath in self.clients:
 
1086
            client = self.bus.get_object(dbus_busname, clientpath)
 
1087
            self.assertIn(("Approve", (False, client_dbus_interface)),
1004
1088
                          client.calls)
1005
1089
 
1006
1090
class TestEnableCmd(TestCmd):
1007
1091
    def test_enable(self):
1008
 
        for client in self.clients:
 
1092
        for clientpath in self.clients:
 
1093
            client = self.bus.get_object(dbus_busname, clientpath)
1009
1094
            client.attributes["Enabled"] = False
1010
1095
 
1011
 
        EnableCmd().run(None, self.clients)
 
1096
        EnableCmd().run(self.clients, self.bus)
1012
1097
 
1013
 
        for client in self.clients:
 
1098
        for clientpath in self.clients:
 
1099
            client = self.bus.get_object(dbus_busname, clientpath)
1014
1100
            self.assertTrue(client.attributes["Enabled"])
1015
1101
 
1016
1102
class TestDisableCmd(TestCmd):
1017
1103
    def test_disable(self):
1018
 
        DisableCmd().run(None, self.clients)
1019
 
 
1020
 
        for client in self.clients:
 
1104
        DisableCmd().run(self.clients, self.bus)
 
1105
        for clientpath in self.clients:
 
1106
            client = self.bus.get_object(dbus_busname, clientpath)
1021
1107
            self.assertFalse(client.attributes["Enabled"])
1022
1108
 
1023
1109
class Unique(object):
1033
1119
                                self.values_to_set)
1034
1120
        for value_to_set, value_to_get in zip(self.values_to_set,
1035
1121
                                              values_to_get):
1036
 
            for client in self.clients:
1037
 
                old_value = client.attributes[self.property]
 
1122
            for clientpath in self.clients:
 
1123
                client = self.bus.get_object(dbus_busname, clientpath)
 
1124
                old_value = client.attributes[self.propname]
1038
1125
                self.assertNotIsInstance(old_value, Unique)
1039
 
                client.attributes[self.property] = Unique()
 
1126
                client.attributes[self.propname] = Unique()
1040
1127
            self.run_command(value_to_set, self.clients)
1041
 
            for client in self.clients:
1042
 
                value = client.attributes[self.property]
 
1128
            for clientpath in self.clients:
 
1129
                client = self.bus.get_object(dbus_busname, clientpath)
 
1130
                value = client.attributes[self.propname]
1043
1131
                self.assertNotIsInstance(value, Unique)
1044
1132
                self.assertEqual(value, value_to_get)
1045
1133
    def run_command(self, value, clients):
1046
 
        self.command().run(None, clients)
 
1134
        self.command().run(clients, self.bus)
1047
1135
 
1048
1136
class TestBumpTimeoutCmd(TestPropertyCmd):
1049
1137
    command = BumpTimeoutCmd
1050
 
    property = "LastCheckedOK"
 
1138
    propname = "LastCheckedOK"
1051
1139
    values_to_set = [""]
1052
1140
 
1053
1141
class TestStartCheckerCmd(TestPropertyCmd):
1054
1142
    command = StartCheckerCmd
1055
 
    property = "CheckerRunning"
 
1143
    propname = "CheckerRunning"
1056
1144
    values_to_set = [dbus.Boolean(True)]
1057
1145
 
1058
1146
class TestStopCheckerCmd(TestPropertyCmd):
1059
1147
    command = StopCheckerCmd
1060
 
    property = "CheckerRunning"
 
1148
    propname = "CheckerRunning"
1061
1149
    values_to_set = [dbus.Boolean(False)]
1062
1150
 
1063
1151
class TestApproveByDefaultCmd(TestPropertyCmd):
1064
1152
    command = ApproveByDefaultCmd
1065
 
    property = "ApprovedByDefault"
 
1153
    propname = "ApprovedByDefault"
1066
1154
    values_to_set = [dbus.Boolean(True)]
1067
1155
 
1068
1156
class TestDenyByDefaultCmd(TestPropertyCmd):
1069
1157
    command = DenyByDefaultCmd
1070
 
    property = "ApprovedByDefault"
 
1158
    propname = "ApprovedByDefault"
1071
1159
    values_to_set = [dbus.Boolean(False)]
1072
1160
 
1073
 
class TestValueArgumentPropertyCmd(TestPropertyCmd):
1074
 
    """Abstract class for tests of PropertyCmd classes using the
1075
 
ValueArgumentMixIn"""
 
1161
class TestPropertyValueCmd(TestPropertyCmd):
 
1162
    """Abstract class for tests of PropertyValueCmd classes"""
1076
1163
    def runTest(self):
1077
 
        if type(self) is TestValueArgumentPropertyCmd:
 
1164
        if type(self) is TestPropertyValueCmd:
1078
1165
            return
1079
 
        return super(TestValueArgumentPropertyCmd, self).runTest()
 
1166
        return super(TestPropertyValueCmd, self).runTest()
1080
1167
    def run_command(self, value, clients):
1081
 
        self.command(value).run(None, clients)
 
1168
        self.command(value).run(clients, self.bus)
1082
1169
 
1083
 
class TestSetCheckerCmd(TestValueArgumentPropertyCmd):
 
1170
class TestSetCheckerCmd(TestPropertyValueCmd):
1084
1171
    command = SetCheckerCmd
1085
 
    property = "Checker"
 
1172
    propname = "Checker"
1086
1173
    values_to_set = ["", ":", "fping -q -- %s"]
1087
1174
 
1088
 
class TestSetHostCmd(TestValueArgumentPropertyCmd):
 
1175
class TestSetHostCmd(TestPropertyValueCmd):
1089
1176
    command = SetHostCmd
1090
 
    property = "Host"
 
1177
    propname = "Host"
1091
1178
    values_to_set = ["192.0.2.3", "foo.example.org"]
1092
1179
 
1093
 
class TestSetSecretCmd(TestValueArgumentPropertyCmd):
 
1180
class TestSetSecretCmd(TestPropertyValueCmd):
1094
1181
    command = SetSecretCmd
1095
 
    property = "Secret"
1096
 
    values_to_set = [open("/dev/null", "rb"),
 
1182
    propname = "Secret"
 
1183
    values_to_set = [io.BytesIO(b""),
1097
1184
                     io.BytesIO(b"secret\0xyzzy\nbar")]
1098
1185
    values_to_get = [b"", b"secret\0xyzzy\nbar"]
1099
1186
 
1100
 
class TestSetTimeoutCmd(TestValueArgumentPropertyCmd):
 
1187
class TestSetTimeoutCmd(TestPropertyValueCmd):
1101
1188
    command = SetTimeoutCmd
1102
 
    property = "Timeout"
 
1189
    propname = "Timeout"
1103
1190
    values_to_set = [datetime.timedelta(),
1104
1191
                     datetime.timedelta(minutes=5),
1105
1192
                     datetime.timedelta(seconds=1),
1107
1194
                     datetime.timedelta(weeks=52)]
1108
1195
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1109
1196
 
1110
 
class TestSetExtendedTimeoutCmd(TestValueArgumentPropertyCmd):
 
1197
class TestSetExtendedTimeoutCmd(TestPropertyValueCmd):
1111
1198
    command = SetExtendedTimeoutCmd
1112
 
    property = "ExtendedTimeout"
 
1199
    propname = "ExtendedTimeout"
1113
1200
    values_to_set = [datetime.timedelta(),
1114
1201
                     datetime.timedelta(minutes=5),
1115
1202
                     datetime.timedelta(seconds=1),
1117
1204
                     datetime.timedelta(weeks=52)]
1118
1205
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1119
1206
 
1120
 
class TestSetIntervalCmd(TestValueArgumentPropertyCmd):
 
1207
class TestSetIntervalCmd(TestPropertyValueCmd):
1121
1208
    command = SetIntervalCmd
1122
 
    property = "Interval"
 
1209
    propname = "Interval"
1123
1210
    values_to_set = [datetime.timedelta(),
1124
1211
                     datetime.timedelta(minutes=5),
1125
1212
                     datetime.timedelta(seconds=1),
1127
1214
                     datetime.timedelta(weeks=52)]
1128
1215
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1129
1216
 
1130
 
class TestSetApprovalDelayCmd(TestValueArgumentPropertyCmd):
 
1217
class TestSetApprovalDelayCmd(TestPropertyValueCmd):
1131
1218
    command = SetApprovalDelayCmd
1132
 
    property = "ApprovalDelay"
 
1219
    propname = "ApprovalDelay"
1133
1220
    values_to_set = [datetime.timedelta(),
1134
1221
                     datetime.timedelta(minutes=5),
1135
1222
                     datetime.timedelta(seconds=1),
1137
1224
                     datetime.timedelta(weeks=52)]
1138
1225
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1139
1226
 
1140
 
class TestSetApprovalDurationCmd(TestValueArgumentPropertyCmd):
 
1227
class TestSetApprovalDurationCmd(TestPropertyValueCmd):
1141
1228
    command = SetApprovalDurationCmd
1142
 
    property = "ApprovalDuration"
 
1229
    propname = "ApprovalDuration"
1143
1230
    values_to_set = [datetime.timedelta(),
1144
1231
                     datetime.timedelta(minutes=5),
1145
1232
                     datetime.timedelta(seconds=1),
1151
1238
    def setUp(self):
1152
1239
        self.parser = argparse.ArgumentParser()
1153
1240
        add_command_line_options(self.parser)
1154
 
    def assert_command_from_args(self, args, command_cls, **cmd_attrs):
 
1241
    def assert_command_from_args(self, args, command_cls,
 
1242
                                 **cmd_attrs):
1155
1243
        """Assert that parsing ARGS should result in an instance of
1156
1244
COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
1157
1245
        options = self.parser.parse_args(args)
 
1246
        check_option_syntax(self.parser, options)
1158
1247
        commands = commands_from_options(options)
1159
1248
        self.assertEqual(len(commands), 1)
1160
1249
        command = commands[0]
1324
1413
    def test_is_enabled_short(self):
1325
1414
        self.assert_command_from_args(["-V", "foo"], IsEnabledCmd)
1326
1415
 
 
1416
    def test_deny_before_remove(self):
 
1417
        options = self.parser.parse_args(["--deny", "--remove",
 
1418
                                          "foo"])
 
1419
        check_option_syntax(self.parser, options)
 
1420
        commands = commands_from_options(options)
 
1421
        self.assertEqual(len(commands), 2)
 
1422
        self.assertIsInstance(commands[0], DenyCmd)
 
1423
        self.assertIsInstance(commands[1], RemoveCmd)
 
1424
 
 
1425
    def test_deny_before_remove_reversed(self):
 
1426
        options = self.parser.parse_args(["--remove", "--deny",
 
1427
                                          "--all"])
 
1428
        check_option_syntax(self.parser, options)
 
1429
        commands = commands_from_options(options)
 
1430
        self.assertEqual(len(commands), 2)
 
1431
        self.assertIsInstance(commands[0], DenyCmd)
 
1432
        self.assertIsInstance(commands[1], RemoveCmd)
 
1433
 
 
1434
 
 
1435
class Test_check_option_syntax(unittest.TestCase):
 
1436
    # This mostly corresponds to the definition from has_actions() in
 
1437
    # check_option_syntax()
 
1438
    actions = {
 
1439
        # The actual values set here are not that important, but we do
 
1440
        # at least stick to the correct types, even though they are
 
1441
        # never used
 
1442
        "enable": True,
 
1443
        "disable": True,
 
1444
        "bump_timeout": True,
 
1445
        "start_checker": True,
 
1446
        "stop_checker": True,
 
1447
        "is_enabled": True,
 
1448
        "remove": True,
 
1449
        "checker": "x",
 
1450
        "timeout": datetime.timedelta(),
 
1451
        "extended_timeout": datetime.timedelta(),
 
1452
        "interval": datetime.timedelta(),
 
1453
        "approved_by_default": True,
 
1454
        "approval_delay": datetime.timedelta(),
 
1455
        "approval_duration": datetime.timedelta(),
 
1456
        "host": "x",
 
1457
        "secret": io.BytesIO(b"x"),
 
1458
        "approve": True,
 
1459
        "deny": True,
 
1460
    }
 
1461
 
 
1462
    def setUp(self):
 
1463
        self.parser = argparse.ArgumentParser()
 
1464
        add_command_line_options(self.parser)
 
1465
 
 
1466
    @contextlib.contextmanager
 
1467
    def assertParseError(self):
 
1468
        with self.assertRaises(SystemExit) as e:
 
1469
            with self.temporarily_suppress_stderr():
 
1470
                yield
 
1471
        # Exit code from argparse is guaranteed to be "2".  Reference:
 
1472
        # https://docs.python.org/3/library
 
1473
        # /argparse.html#exiting-methods
 
1474
        self.assertEqual(e.exception.code, 2)
 
1475
 
 
1476
    @staticmethod
 
1477
    @contextlib.contextmanager
 
1478
    def temporarily_suppress_stderr():
 
1479
        null = os.open(os.path.devnull, os.O_RDWR)
 
1480
        stderrcopy = os.dup(sys.stderr.fileno())
 
1481
        os.dup2(null, sys.stderr.fileno())
 
1482
        os.close(null)
 
1483
        try:
 
1484
            yield
 
1485
        finally:
 
1486
            # restore stderr
 
1487
            os.dup2(stderrcopy, sys.stderr.fileno())
 
1488
            os.close(stderrcopy)
 
1489
 
 
1490
    def check_option_syntax(self, options):
 
1491
        check_option_syntax(self.parser, options)
 
1492
 
 
1493
    def test_actions_requires_client_or_all(self):
 
1494
        for action, value in self.actions.items():
 
1495
            options = self.parser.parse_args()
 
1496
            setattr(options, action, value)
 
1497
            with self.assertParseError():
 
1498
                self.check_option_syntax(options)
 
1499
 
 
1500
    def test_actions_conflicts_with_verbose(self):
 
1501
        for action, value in self.actions.items():
 
1502
            options = self.parser.parse_args()
 
1503
            setattr(options, action, value)
 
1504
            options.verbose = True
 
1505
            with self.assertParseError():
 
1506
                self.check_option_syntax(options)
 
1507
 
 
1508
    def test_dump_json_conflicts_with_verbose(self):
 
1509
        options = self.parser.parse_args()
 
1510
        options.dump_json = True
 
1511
        options.verbose = True
 
1512
        with self.assertParseError():
 
1513
            self.check_option_syntax(options)
 
1514
 
 
1515
    def test_dump_json_conflicts_with_action(self):
 
1516
        for action, value in self.actions.items():
 
1517
            options = self.parser.parse_args()
 
1518
            setattr(options, action, value)
 
1519
            options.dump_json = True
 
1520
            with self.assertParseError():
 
1521
                self.check_option_syntax(options)
 
1522
 
 
1523
    def test_all_can_not_be_alone(self):
 
1524
        options = self.parser.parse_args()
 
1525
        options.all = True
 
1526
        with self.assertParseError():
 
1527
            self.check_option_syntax(options)
 
1528
 
 
1529
    def test_all_is_ok_with_any_action(self):
 
1530
        for action, value in self.actions.items():
 
1531
            options = self.parser.parse_args()
 
1532
            setattr(options, action, value)
 
1533
            options.all = True
 
1534
            self.check_option_syntax(options)
 
1535
 
 
1536
    def test_is_enabled_fails_without_client(self):
 
1537
        options = self.parser.parse_args()
 
1538
        options.is_enabled = True
 
1539
        with self.assertParseError():
 
1540
            self.check_option_syntax(options)
 
1541
 
 
1542
    def test_is_enabled_works_with_one_client(self):
 
1543
        options = self.parser.parse_args()
 
1544
        options.is_enabled = True
 
1545
        options.client = ["foo"]
 
1546
        self.check_option_syntax(options)
 
1547
 
 
1548
    def test_is_enabled_fails_with_two_clients(self):
 
1549
        options = self.parser.parse_args()
 
1550
        options.is_enabled = True
 
1551
        options.client = ["foo", "barbar"]
 
1552
        with self.assertParseError():
 
1553
            self.check_option_syntax(options)
 
1554
 
 
1555
    def test_remove_can_only_be_combined_with_action_deny(self):
 
1556
        for action, value in self.actions.items():
 
1557
            if action in {"remove", "deny"}:
 
1558
                continue
 
1559
            options = self.parser.parse_args()
 
1560
            setattr(options, action, value)
 
1561
            options.all = True
 
1562
            options.remove = True
 
1563
            with self.assertParseError():
 
1564
                self.check_option_syntax(options)
 
1565
 
1327
1566
 
1328
1567
 
1329
1568
def should_only_run_tests():