/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-16 00:10:31 UTC
  • Revision ID: teddy@recompile.se-20190316001031-c5v8it0q3qbvpskx
mandos-ctl: Refactor

* mandos-ctl (main): Call get_managed_objects() to get all client
                     paths and properties.
  (get_managed_objects): New.
  (Test_get_managed_objects): - '' -

Show diffs side-by-side

added added

removed removed

Lines of Context:
93
93
    if options.debug:
94
94
        log.setLevel(logging.DEBUG)
95
95
 
96
 
    try:
97
 
        bus = dbus.SystemBus()
98
 
        log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
99
 
                  dbus_busname, server_dbus_path)
100
 
        mandos_dbus_objc = bus.get_object(dbus_busname,
101
 
                                          server_dbus_path)
102
 
    except dbus.exceptions.DBusException:
103
 
        log.critical("Could not connect to Mandos server")
104
 
        sys.exit(1)
105
 
 
106
 
    mandos_serv = dbus.Interface(mandos_dbus_objc,
107
 
                                 dbus_interface=server_dbus_interface)
 
96
    bus = dbus.SystemBus()
 
97
 
 
98
    mandos_dbus_object = get_mandos_dbus_object(bus)
 
99
 
 
100
    mandos_serv = dbus.Interface(
 
101
        mandos_dbus_object, dbus_interface=server_dbus_interface)
108
102
    mandos_serv_object_manager = dbus.Interface(
109
 
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
110
 
 
111
 
    # Filter out log message from dbus module
112
 
    dbus_logger = logging.getLogger("dbus.proxies")
113
 
    class NullFilter(logging.Filter):
114
 
        def filter(self, record):
115
 
            return False
116
 
    dbus_filter = NullFilter()
117
 
    try:
118
 
        dbus_logger.addFilter(dbus_filter)
119
 
        log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", dbus_busname,
120
 
                  server_dbus_path, dbus.OBJECT_MANAGER_IFACE)
121
 
        mandos_clients = {path: ifs_and_props[client_dbus_interface]
122
 
                          for path, ifs_and_props in
123
 
                          mandos_serv_object_manager
124
 
                          .GetManagedObjects().items()
125
 
                          if client_dbus_interface in ifs_and_props}
126
 
    except dbus.exceptions.DBusException as e:
127
 
        log.critical("Failed to access Mandos server through D-Bus:"
128
 
                     "\n%s", e)
129
 
        sys.exit(1)
130
 
    finally:
131
 
        # restore dbus logger
132
 
        dbus_logger.removeFilter(dbus_filter)
133
 
 
134
 
    # Compile dict of (clients: properties) to process
135
 
    clients = {}
136
 
 
 
103
        mandos_dbus_object, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
 
104
 
 
105
    managed_objects = get_managed_objects(mandos_serv_object_manager)
 
106
 
 
107
    all_clients = {}
 
108
    for path, ifs_and_props in managed_objects.items():
 
109
        try:
 
110
            all_clients[path] = ifs_and_props[client_dbus_interface]
 
111
        except KeyError:
 
112
            pass
 
113
 
 
114
    # Compile dict of (clientpath: properties) to process
137
115
    if not clientnames:
138
 
        clients = {objpath: properties
139
 
                   for objpath, properties in mandos_clients.items()}
 
116
        clients = all_clients
140
117
    else:
 
118
        clients = {}
141
119
        for name in clientnames:
142
 
            for objpath, properties in mandos_clients.items():
 
120
            for objpath, properties in all_clients.items():
143
121
                if properties["Name"] == name:
144
122
                    clients[objpath] = properties
145
123
                    break
446
424
        options.remove = True
447
425
 
448
426
 
 
427
def get_mandos_dbus_object(bus):
 
428
    try:
 
429
        log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
430
                  dbus_busname, server_dbus_path)
 
431
        mandos_dbus_object = bus.get_object(dbus_busname,
 
432
                                            server_dbus_path)
 
433
    except dbus.exceptions.DBusException:
 
434
        log.critical("Could not connect to Mandos server")
 
435
        sys.exit(1)
 
436
 
 
437
    return mandos_dbus_object
 
438
 
 
439
 
 
440
def get_managed_objects(object_manager):
 
441
    log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", dbus_busname,
 
442
              server_dbus_path, dbus.OBJECT_MANAGER_IFACE)
 
443
    try:
 
444
        with SilenceLogger("dbus.proxies"):
 
445
            managed_objects = object_manager.GetManagedObjects()
 
446
    except dbus.exceptions.DBusException as e:
 
447
        log.critical("Failed to access Mandos server through D-Bus:"
 
448
                     "\n%s", e)
 
449
        sys.exit(1)
 
450
    return managed_objects
 
451
 
 
452
 
 
453
class SilenceLogger(object):
 
454
    "Simple context manager to silence a particular logger"
 
455
    def __init__(self, loggername):
 
456
        self.logger = logging.getLogger(loggername)
 
457
 
 
458
    def __enter__(self):
 
459
        self.logger.addFilter(self.nullfilter)
 
460
        return self
 
461
 
 
462
    class NullFilter(logging.Filter):
 
463
        def filter(self, record):
 
464
            return False
 
465
 
 
466
    nullfilter = NullFilter()
 
467
 
 
468
    def __exit__(self, exc_type, exc_val, exc_tb):
 
469
        self.logger.removeFilter(self.nullfilter)
 
470
 
 
471
 
449
472
def commands_from_options(options):
450
473
 
451
474
    commands = []
577
600
                    "LastApprovalRequest", "ApprovalDelay",
578
601
                    "ApprovalDuration", "Checker", "ExtendedTimeout",
579
602
                    "Expires", "LastCheckerStatus")
 
603
 
580
604
    def run(self, clients, bus=None, mandos=None):
581
605
        print(self.output(clients.values()))
 
606
 
582
607
    def output(self, clients):
583
608
        raise NotImplementedError()
584
609
 
588
613
        data = {client["Name"]:
589
614
                {key: self.dbus_boolean_to_bool(client[key])
590
615
                 for key in self.all_keywords}
591
 
                for client in clients.values()}
 
616
                for client in clients}
592
617
        return json.dumps(data, indent=4, separators=(',', ': '))
 
618
 
593
619
    @staticmethod
594
620
    def dbus_boolean_to_bool(value):
595
621
        if isinstance(value, dbus.Boolean):
633
659
            "LastCheckerStatus": "Last Checker Status",
634
660
        }
635
661
 
636
 
        def __init__(self, clients, keywords, tableheaders=None):
 
662
        def __init__(self, clients, keywords):
637
663
            self.clients = clients
638
664
            self.keywords = keywords
639
 
            if tableheaders is not None:
640
 
                self.tableheaders = tableheaders
641
665
 
642
666
        def __str__(self):
643
667
            return "\n".join(self.rows())
696
720
 
697
721
class PropertyCmd(Command):
698
722
    """Abstract class for Actions for setting one client property"""
 
723
 
699
724
    def run_on_one_client(self, client, properties):
700
725
        """Set the Client's D-Bus property"""
701
726
        log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", dbus_busname,
707
732
        client.Set(client_dbus_interface, self.propname,
708
733
                   self.value_to_set,
709
734
                   dbus_interface=dbus.PROPERTIES_IFACE)
 
735
 
710
736
    @property
711
737
    def propname(self):
712
738
        raise NotImplementedError()
763
789
 
764
790
class SetSecretCmd(PropertyValueCmd):
765
791
    propname = "Secret"
 
792
 
766
793
    @property
767
794
    def value_to_set(self):
768
795
        return self._vts
 
796
 
769
797
    @value_to_set.setter
770
798
    def value_to_set(self, value):
771
799
        """When setting, read data from supplied file object"""
776
804
class MillisecondsPropertyValueArgumentCmd(PropertyValueCmd):
777
805
    """Abstract class for PropertyValueCmd taking a value argument as
778
806
a datetime.timedelta() but should store it as milliseconds."""
 
807
 
779
808
    @property
780
809
    def value_to_set(self):
781
810
        return self._vts
 
811
 
782
812
    @value_to_set.setter
783
813
    def value_to_set(self, value):
784
814
        """When setting, convert value from a datetime.timedelta"""
816
846
                         datetime.timedelta(0, 1))
817
847
        self.assertEqual(string_to_delta("PT2H"),
818
848
                         datetime.timedelta(0, 7200))
 
849
 
819
850
    def test_falls_back_to_pre_1_6_1_with_warning(self):
820
851
        # assertLogs only exists in Python 3.4
821
852
        if hasattr(self, "assertLogs"):
971
1002
                self.check_option_syntax(options)
972
1003
 
973
1004
 
 
1005
class Test_get_mandos_dbus_object(unittest.TestCase):
 
1006
    def test_calls_and_returns_get_object_on_bus(self):
 
1007
        class MockBus(object):
 
1008
            called = False
 
1009
            def get_object(mockbus_self, busname, dbus_path):
 
1010
                # Note that "self" is still the testcase instance,
 
1011
                # this MockBus instance is in "mockbus_self".
 
1012
                self.assertEqual(busname, dbus_busname)
 
1013
                self.assertEqual(dbus_path, server_dbus_path)
 
1014
                mockbus_self.called = True
 
1015
                return mockbus_self
 
1016
 
 
1017
        mockbus = get_mandos_dbus_object(bus=MockBus())
 
1018
        self.assertIsInstance(mockbus, MockBus)
 
1019
        self.assertTrue(mockbus.called)
 
1020
 
 
1021
    def test_logs_and_exits_on_dbus_error(self):
 
1022
        class MockBusFailing(object):
 
1023
            def get_object(self, busname, dbus_path):
 
1024
                raise dbus.exceptions.DBusException("Test")
 
1025
 
 
1026
        # assertLogs only exists in Python 3.4
 
1027
        if hasattr(self, "assertLogs"):
 
1028
            with self.assertLogs(log, logging.CRITICAL):
 
1029
                with self.assertRaises(SystemExit) as e:
 
1030
                    bus = get_mandos_dbus_object(bus=MockBus())
 
1031
        else:
 
1032
            critical_filter = self.CriticalFilter()
 
1033
            log.addFilter(critical_filter)
 
1034
            try:
 
1035
                with self.assertRaises(SystemExit) as e:
 
1036
                    get_mandos_dbus_object(bus=MockBusFailing())
 
1037
            finally:
 
1038
                log.removeFilter(critical_filter)
 
1039
            self.assertTrue(critical_filter.found)
 
1040
        if isinstance(e.exception.code, int):
 
1041
            self.assertNotEqual(e.exception.code, 0)
 
1042
        else:
 
1043
            self.assertIsNotNone(e.exception.code)
 
1044
 
 
1045
    class CriticalFilter(logging.Filter):
 
1046
        """Don't show, but register, critical messages"""
 
1047
        found = False
 
1048
        def filter(self, record):
 
1049
            is_critical = record.levelno >= logging.CRITICAL
 
1050
            self.found = is_critical or self.found
 
1051
            return not is_critical
 
1052
 
 
1053
 
 
1054
class Test_get_managed_objects(unittest.TestCase):
 
1055
    def test_calls_and_returns_GetManagedObjects(self):
 
1056
        managed_objects = {"/clients/foo": { "Name": "foo"}}
 
1057
        class MockObjectManager(object):
 
1058
            @staticmethod
 
1059
            def GetManagedObjects():
 
1060
                return managed_objects
 
1061
        retval = get_managed_objects(MockObjectManager())
 
1062
        self.assertDictEqual(managed_objects, retval)
 
1063
 
 
1064
    def test_logs_and_exits_on_dbus_error(self):
 
1065
        class MockObjectManagerFailing(object):
 
1066
            @staticmethod
 
1067
            def GetManagedObjects():
 
1068
                raise dbus.exceptions.DBusException("Test")
 
1069
 
 
1070
        if hasattr(self, "assertLogs"):
 
1071
            with self.assertLogs(log, logging.CRITICAL):
 
1072
                with self.assertRaises(SystemExit):
 
1073
                    get_managed_objects(MockObjectManagerFailing())
 
1074
        else:
 
1075
            critical_filter = self.CriticalFilter()
 
1076
            log.addFilter(critical_filter)
 
1077
            try:
 
1078
                with self.assertRaises(SystemExit) as e:
 
1079
                    get_managed_objects(MockObjectManagerFailing())
 
1080
            finally:
 
1081
                log.removeFilter(critical_filter)
 
1082
            self.assertTrue(critical_filter.found)
 
1083
        if isinstance(e.exception.code, int):
 
1084
            self.assertNotEqual(e.exception.code, 0)
 
1085
        else:
 
1086
            self.assertIsNotNone(e.exception.code)
 
1087
 
 
1088
    class CriticalFilter(logging.Filter):
 
1089
        """Don't show, but register, critical messages"""
 
1090
        found = False
 
1091
        def filter(self, record):
 
1092
            is_critical = record.levelno >= logging.CRITICAL
 
1093
            self.found = is_critical or self.found
 
1094
            return not is_critical
 
1095
 
 
1096
 
 
1097
class Test_SilenceLogger(unittest.TestCase):
 
1098
    loggername = "mandos-ctl.Test_SilenceLogger"
 
1099
    log = logging.getLogger(loggername)
 
1100
    log.propagate = False
 
1101
    log.addHandler(logging.NullHandler())
 
1102
 
 
1103
    def setUp(self):
 
1104
        self.counting_filter = self.CountingFilter()
 
1105
 
 
1106
    class CountingFilter(logging.Filter):
 
1107
        "Count number of records"
 
1108
        count = 0
 
1109
        def filter(self, record):
 
1110
            self.count += 1
 
1111
            return True
 
1112
 
 
1113
    def test_should_filter_records_only_when_active(self):
 
1114
        try:
 
1115
            with SilenceLogger(self.loggername):
 
1116
                self.log.addFilter(self.counting_filter)
 
1117
                self.log.info("Filtered log message 1")
 
1118
            self.log.info("Non-filtered message 2")
 
1119
            self.log.info("Non-filtered message 3")
 
1120
        finally:
 
1121
            self.log.removeFilter(self.counting_filter)
 
1122
        self.assertEqual(self.counting_filter.count, 2)
 
1123
 
 
1124
 
974
1125
class Test_commands_from_options(unittest.TestCase):
975
1126
    def setUp(self):
976
1127
        self.parser = argparse.ArgumentParser()
1173
1324
 
1174
1325
class TestCmd(unittest.TestCase):
1175
1326
    """Abstract class for tests of command classes"""
 
1327
 
1176
1328
    def setUp(self):
1177
1329
        testcase = self
1178
1330
        class MockClient(object):
1250
1402
                ("/clients/barbar", self.other_client.attributes),
1251
1403
            ])
1252
1404
        self.one_client = {"/clients/foo": self.client.attributes}
 
1405
 
1253
1406
    @property
1254
1407
    def bus(self):
1255
1408
        class Bus(object):
1257
1410
            def get_object(client_bus_name, path):
1258
1411
                self.assertEqual(client_bus_name, dbus_busname)
1259
1412
                return {
 
1413
                    # Note: "self" here is the TestCmd instance, not
 
1414
                    # the Bus instance, since this is a static method!
1260
1415
                    "/clients/foo": self.client,
1261
1416
                    "/clients/barbar": self.other_client,
1262
1417
                }[path]
1269
1424
                                                      properties)
1270
1425
                            for client, properties
1271
1426
                            in self.clients.items()))
 
1427
 
1272
1428
    def test_is_enabled_run_exits_successfully(self):
1273
1429
        with self.assertRaises(SystemExit) as e:
1274
1430
            IsEnabledCmd().run(self.one_client)
1276
1432
            self.assertEqual(e.exception.code, 0)
1277
1433
        else:
1278
1434
            self.assertIsNone(e.exception.code)
 
1435
 
1279
1436
    def test_is_enabled_run_exits_with_failure(self):
1280
1437
        self.client.attributes["Enabled"] = dbus.Boolean(False)
1281
1438
        with self.assertRaises(SystemExit) as e:
1303
1460
            self.assertIn(("Approve", (False, client_dbus_interface)),
1304
1461
                          client.calls)
1305
1462
 
 
1463
 
1306
1464
class TestRemoveCmd(TestCmd):
1307
1465
    def test_remove(self):
1308
1466
        class MockMandos(object):
1372
1530
            },
1373
1531
        }
1374
1532
        return super(TestDumpJSONCmd, self).setUp()
 
1533
 
1375
1534
    def test_normal(self):
1376
 
        json_data = json.loads(DumpJSONCmd().output(self.clients))
 
1535
        output = DumpJSONCmd().output(self.clients.values())
 
1536
        json_data = json.loads(output)
1377
1537
        self.assertDictEqual(json_data, self.expected_json)
 
1538
 
1378
1539
    def test_one_client(self):
1379
 
        clients = self.one_client
1380
 
        json_data = json.loads(DumpJSONCmd().output(clients))
 
1540
        output = DumpJSONCmd().output(self.one_client.values())
 
1541
        json_data = json.loads(output)
1381
1542
        expected_json = {"foo": self.expected_json["foo"]}
1382
1543
        self.assertDictEqual(json_data, expected_json)
1383
1544
 
1391
1552
            "barbar Yes     00:05:00 2019-02-04T00:00:00  ",
1392
1553
        ))
1393
1554
        self.assertEqual(output, expected_output)
 
1555
 
1394
1556
    def test_verbose(self):
1395
1557
        output = PrintTableCmd(verbose=True).output(
1396
1558
            self.clients.values())
1485
1647
                                            for rows in columns)
1486
1648
                                    for line in range(num_lines))
1487
1649
        self.assertEqual(output, expected_output)
 
1650
 
1488
1651
    def test_one_client(self):
1489
1652
        output = PrintTableCmd().output(self.one_client.values())
1490
1653
        expected_output = "\n".join((
1494
1657
        self.assertEqual(output, expected_output)
1495
1658
 
1496
1659
 
1497
 
class Unique(object):
1498
 
    """Class for objects which exist only to be unique objects, since
1499
 
unittest.mock.sentinel only exists in Python 3.3"""
1500
 
 
1501
 
 
1502
1660
class TestPropertyCmd(TestCmd):
1503
1661
    """Abstract class for tests of PropertyCmd classes"""
1504
1662
    def runTest(self):
1511
1669
            for clientpath in self.clients:
1512
1670
                client = self.bus.get_object(dbus_busname, clientpath)
1513
1671
                old_value = client.attributes[self.propname]
1514
 
                self.assertNotIsInstance(old_value, Unique)
1515
 
                client.attributes[self.propname] = Unique()
 
1672
                self.assertNotIsInstance(old_value, self.Unique)
 
1673
                client.attributes[self.propname] = self.Unique()
1516
1674
            self.run_command(value_to_set, self.clients)
1517
1675
            for clientpath in self.clients:
1518
1676
                client = self.bus.get_object(dbus_busname, clientpath)
1519
1677
                value = client.attributes[self.propname]
1520
 
                self.assertNotIsInstance(value, Unique)
 
1678
                self.assertNotIsInstance(value, self.Unique)
1521
1679
                self.assertEqual(value, value_to_get)
 
1680
 
 
1681
    class Unique(object):
 
1682
        """Class for objects which exist only to be unique objects,
 
1683
since unittest.mock.sentinel only exists in Python 3.3"""
 
1684
 
1522
1685
    def run_command(self, value, clients):
1523
1686
        self.command().run(clients, self.bus)
1524
1687
 
1567
1730
 
1568
1731
class TestPropertyValueCmd(TestPropertyCmd):
1569
1732
    """Abstract class for tests of PropertyValueCmd classes"""
 
1733
 
1570
1734
    def runTest(self):
1571
1735
        if type(self) is TestPropertyValueCmd:
1572
1736
            return
1573
1737
        return super(TestPropertyValueCmd, self).runTest()
 
1738
 
1574
1739
    def run_command(self, value, clients):
1575
1740
        self.command(value).run(clients, self.bus)
1576
1741
 
1668
1833
    return tests
1669
1834
 
1670
1835
if __name__ == "__main__":
1671
 
    if should_only_run_tests():
1672
 
        # Call using ./tdd-python-script --check [--verbose]
1673
 
        unittest.main()
1674
 
    else:
1675
 
        main()
 
1836
    try:
 
1837
        if should_only_run_tests():
 
1838
            # Call using ./tdd-python-script --check [--verbose]
 
1839
            unittest.main()
 
1840
        else:
 
1841
            main()
 
1842
    finally:
 
1843
        logging.shutdown()