/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-31 04:39:25 UTC
  • Revision ID: teddy@recompile.se-20190331043925-0j9pdspo3hux5nka
mandos-ctl: Refactor: Move command list generation into argparse

* mandos-ctl (add_command_line_options): Don't simply set flags for
                                         each option; instead, add
                                         command objects to a
                                         "commands" list.
  (check_option_syntax): Rework to deal with an already-generated
                         command list.
  (commands_from_options): Remove all code to generate command list,
                           but add more code to move command.Deny
                           ahead of command.Remove in command list.
  (command.PropertySetterValue.argparse): New; used in
                                          add_command_line_options.
  (Test_check_option_syntax): Refactor to pass actual string arguments
                              to parse_args() instead of modifying an
                              argparse.Namespace.

Show diffs side-by-side

added added

removed removed

Lines of Context:
45
45
import io
46
46
import tempfile
47
47
import contextlib
 
48
import abc
48
49
 
49
 
try:
50
 
    import pydbus
51
 
    import gi
52
 
    dbus_python = None
53
 
except ImportError:
54
 
    import dbus as dbus_python
55
 
    pydbus = None
56
 
    class gi(object):
57
 
        """Dummy gi module, for the tests"""
58
 
        class repository(object):
59
 
            class GLib(object):
60
 
                class Error(Exception):
61
 
                    pass
 
50
import dbus as dbus_python
62
51
 
63
52
# Show warnings by default
64
53
if not sys.warnoptions:
78
67
 
79
68
locale.setlocale(locale.LC_ALL, "")
80
69
 
81
 
version = "1.8.4"
 
70
version = "1.8.3"
82
71
 
83
72
 
84
73
def main():
93
82
    if options.debug:
94
83
        log.setLevel(logging.DEBUG)
95
84
 
96
 
    if pydbus is not None:
97
 
        bus = pydbus_adapter.CachingBus(pydbus)
98
 
    else:
99
 
        bus = dbus_python_adapter.CachingBus(dbus_python)
 
85
    bus = dbus_python_adapter.CachingBus(dbus_python)
100
86
 
101
87
    try:
102
88
        all_clients = bus.get_clients_and_properties()
571
557
                        for key, subval in value.items()}
572
558
            return value
573
559
 
574
 
        def set_client_property(self, objectpath, key, value):
575
 
            if key == "Secret":
576
 
                if not isinstance(value, bytes):
577
 
                    value = value.encode("utf-8")
578
 
                value = self.dbus_python.ByteArray(value)
579
 
            return self.set_property(self.busname, objectpath,
580
 
                                     self.client_interface, key,
581
 
                                     value)
582
560
 
583
561
    class SilenceLogger(object):
584
562
        "Simple context manager to silence a particular logger"
615
593
                return new_object
616
594
 
617
595
 
618
 
class pydbus_adapter(object):
619
 
    class SystemBus(dbus.MandosBus):
620
 
        def __init__(self, module=pydbus):
621
 
            self.pydbus = module
622
 
            self.bus = self.pydbus.SystemBus()
623
 
 
624
 
        @contextlib.contextmanager
625
 
        def convert_exception(self, exception_class=dbus.Error):
626
 
            try:
627
 
                yield
628
 
            except gi.repository.GLib.Error as e:
629
 
                # This does what "raise from" would do
630
 
                exc = exception_class(*e.args)
631
 
                exc.__cause__ = e
632
 
                raise exc
633
 
 
634
 
        def call_method(self, methodname, busname, objectpath,
635
 
                        interface, *args):
636
 
            proxy_object = self.get(busname, objectpath)
637
 
            log.debug("D-Bus: %s:%s:%s.%s(%s)", busname, objectpath,
638
 
                      interface, methodname,
639
 
                      ", ".join(repr(a) for a in args))
640
 
            method = getattr(proxy_object[interface], methodname)
641
 
            with self.convert_exception():
642
 
                return method(*args)
643
 
 
644
 
        def get(self, busname, objectpath):
645
 
            log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
646
 
                      busname, objectpath)
647
 
            with self.convert_exception(dbus.ConnectFailed):
648
 
                if sys.version_info.major <= 2:
649
 
                    with warnings.catch_warnings():
650
 
                        warnings.filterwarnings(
651
 
                            "ignore", "", DeprecationWarning,
652
 
                            r"^xml\.etree\.ElementTree$")
653
 
                        return self.bus.get(busname, objectpath)
654
 
                else:
655
 
                    return self.bus.get(busname, objectpath)
656
 
 
657
 
        def set_property(self, busname, objectpath, interface, key,
658
 
                         value):
659
 
            proxy_object = self.get(busname, objectpath)
660
 
            log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", busname,
661
 
                      objectpath, self.properties_iface, interface,
662
 
                      key, value)
663
 
            setattr(proxy_object[interface], key, value)
664
 
 
665
 
    class CachingBus(SystemBus):
666
 
        """A caching layer for pydbus_adapter.SystemBus"""
667
 
        def __init__(self, *args, **kwargs):
668
 
            self.object_cache = {}
669
 
            super(pydbus_adapter.CachingBus,
670
 
                  self).__init__(*args, **kwargs)
671
 
        def get(self, busname, objectpath):
672
 
            try:
673
 
                return self.object_cache[(busname, objectpath)]
674
 
            except KeyError:
675
 
                new_object = (super(pydbus_adapter.CachingBus, self)
676
 
                              .get(busname, objectpath))
677
 
                self.object_cache[(busname, objectpath)]  = new_object
678
 
                return new_object
679
 
 
680
 
 
681
596
def commands_from_options(options):
682
597
 
683
598
    commands = list(options.commands)
1320
1235
                @staticmethod
1321
1236
                def get_object(busname, objectpath):
1322
1237
                    DBusObject = collections.namedtuple(
1323
 
                        "DBusObject", ("methodname", "Set"))
 
1238
                        "DBusObject", ("methodname",))
1324
1239
                    def method(*args, **kwargs):
1325
1240
                        self.assertEqual({"dbus_interface":
1326
1241
                                          "interface"},
1327
1242
                                         kwargs)
1328
1243
                        return func(*args)
1329
 
                    def set_property(interface, key, value,
1330
 
                                     dbus_interface=None):
1331
 
                        self.assertEqual(
1332
 
                            "org.freedesktop.DBus.Properties",
1333
 
                            dbus_interface)
1334
 
                        self.assertEqual("Secret", key)
1335
 
                        return func(interface, key, value,
1336
 
                                    dbus_interface=dbus_interface)
1337
 
                    return DBusObject(methodname=method,
1338
 
                                      Set=set_property)
 
1244
                    return DBusObject(methodname=method)
1339
1245
            class Boolean(object):
1340
1246
                def __init__(self, value):
1341
1247
                    self.value = bool(value)
1347
1253
                pass
1348
1254
            class Dictionary(dict):
1349
1255
                pass
1350
 
            class ByteArray(bytes):
1351
 
                pass
1352
1256
        return mock_dbus_python
1353
1257
 
1354
1258
    def call_method(self, bus, methodname, busname, objectpath,
1531
1435
        # Make sure the dbus logger was suppressed
1532
1436
        self.assertEqual(0, counting_handler.count)
1533
1437
 
1534
 
    def test_Set_Secret_sends_bytearray(self):
1535
 
        ret = [None]
1536
 
        def func(*args, **kwargs):
1537
 
            ret[0] = (args, kwargs)
1538
 
        mock_dbus_python = self.MockDBusPython_func(func)
1539
 
        bus = dbus_python_adapter.SystemBus(mock_dbus_python)
1540
 
        bus.set_client_property("objectpath", "Secret", "value")
1541
 
        expected_call = (("se.recompile.Mandos.Client", "Secret",
1542
 
                          mock_dbus_python.ByteArray(b"value")),
1543
 
                         {"dbus_interface":
1544
 
                          "org.freedesktop.DBus.Properties"})
1545
 
        self.assertEqual(expected_call, ret[0])
1546
 
        if sys.version_info.major == 2:
1547
 
            self.assertIsInstance(ret[0][0][-1],
1548
 
                                  mock_dbus_python.ByteArray)
1549
 
 
1550
1438
    def test_get_object_converts_to_correct_exception(self):
1551
1439
        bus = dbus_python_adapter.SystemBus(
1552
1440
            self.fake_dbus_python_raises_exception_on_connect)
1621
1509
        self.assertIs(obj1, obj1b)
1622
1510
 
1623
1511
 
1624
 
class Test_pydbus_adapter_SystemBus(TestCaseWithAssertLogs):
1625
 
 
1626
 
    def Stub_pydbus_func(self, func):
1627
 
        class stub_pydbus(object):
1628
 
            """stub pydbus module"""
1629
 
            class SystemBus(object):
1630
 
                @staticmethod
1631
 
                def get(busname, objectpath):
1632
 
                    DBusObject = collections.namedtuple(
1633
 
                        "DBusObject", ("methodname",))
1634
 
                    return {"interface":
1635
 
                            DBusObject(methodname=func)}
1636
 
        return stub_pydbus
1637
 
 
1638
 
    def call_method(self, bus, methodname, busname, objectpath,
1639
 
                    interface, *args):
1640
 
        with self.assertLogs(log, logging.DEBUG):
1641
 
            return bus.call_method(methodname, busname, objectpath,
1642
 
                                   interface, *args)
1643
 
 
1644
 
    def test_call_method_returns(self):
1645
 
        expected_method_return = Unique()
1646
 
        method_args = (Unique(), Unique())
1647
 
        def func(*args):
1648
 
            self.assertEqual(len(method_args), len(args))
1649
 
            for marg, arg in zip(method_args, args):
1650
 
                self.assertIs(marg, arg)
1651
 
            return expected_method_return
1652
 
        stub_pydbus = self.Stub_pydbus_func(func)
1653
 
        bus = pydbus_adapter.SystemBus(stub_pydbus)
1654
 
        ret = self.call_method(bus, "methodname", "busname",
1655
 
                               "objectpath", "interface",
1656
 
                               *method_args)
1657
 
        self.assertIs(ret, expected_method_return)
1658
 
 
1659
 
    def test_call_method_handles_exception(self):
1660
 
        dbus_logger = logging.getLogger("dbus.proxies")
1661
 
 
1662
 
        def func():
1663
 
            raise gi.repository.GLib.Error()
1664
 
 
1665
 
        stub_pydbus = self.Stub_pydbus_func(func)
1666
 
        bus = pydbus_adapter.SystemBus(stub_pydbus)
1667
 
 
1668
 
        with self.assertRaises(dbus.Error) as e:
1669
 
            self.call_method(bus, "methodname", "busname",
1670
 
                             "objectpath", "interface")
1671
 
 
1672
 
        self.assertNotIsInstance(e, dbus.ConnectFailed)
1673
 
 
1674
 
    def test_get_converts_to_correct_exception(self):
1675
 
        bus = pydbus_adapter.SystemBus(
1676
 
            self.fake_pydbus_raises_exception_on_connect)
1677
 
        with self.assertRaises(dbus.ConnectFailed):
1678
 
            self.call_method(bus, "methodname", "busname",
1679
 
                             "objectpath", "interface")
1680
 
 
1681
 
    class fake_pydbus_raises_exception_on_connect(object):
1682
 
        """fake dbus-python module"""
1683
 
        @classmethod
1684
 
        def SystemBus(cls):
1685
 
            def get(busname, objectpath):
1686
 
                raise gi.repository.GLib.Error()
1687
 
            Bus = collections.namedtuple("Bus", ["get"])
1688
 
            return Bus(get=get)
1689
 
 
1690
 
    def test_set_property_uses_setattr(self):
1691
 
        class Object(object):
1692
 
            pass
1693
 
        obj = Object()
1694
 
        class pydbus_spy(object):
1695
 
            class SystemBus(object):
1696
 
                @staticmethod
1697
 
                def get(busname, objectpath):
1698
 
                    return {"interface": obj}
1699
 
        bus = pydbus_adapter.SystemBus(pydbus_spy)
1700
 
        value = Unique()
1701
 
        bus.set_property("busname", "objectpath", "interface", "key",
1702
 
                         value)
1703
 
        self.assertIs(value, obj.key)
1704
 
 
1705
 
    def test_get_suppresses_xml_deprecation_warning(self):
1706
 
        if sys.version_info.major >= 3:
1707
 
            return
1708
 
        class stub_pydbus_get(object):
1709
 
            class SystemBus(object):
1710
 
                @staticmethod
1711
 
                def get(busname, objectpath):
1712
 
                    warnings.warn_explicit(
1713
 
                        "deprecated", DeprecationWarning,
1714
 
                        "xml.etree.ElementTree", 0)
1715
 
        bus = pydbus_adapter.SystemBus(stub_pydbus_get)
1716
 
        with warnings.catch_warnings(record=True) as w:
1717
 
            warnings.simplefilter("always")
1718
 
            bus.get("busname", "objectpath")
1719
 
            self.assertEqual(0, len(w))
1720
 
 
1721
 
 
1722
 
class Test_pydbus_adapter_CachingBus(unittest.TestCase):
1723
 
    class stub_pydbus(object):
1724
 
        """stub pydbus module"""
1725
 
        class SystemBus(object):
1726
 
            @staticmethod
1727
 
            def get(busname, objectpath):
1728
 
                return Unique()
1729
 
 
1730
 
    def setUp(self):
1731
 
        self.bus = pydbus_adapter.CachingBus(self.stub_pydbus)
1732
 
 
1733
 
    def test_returns_distinct_objectpaths(self):
1734
 
        obj1 = self.bus.get("busname", "objectpath1")
1735
 
        self.assertIsInstance(obj1, Unique)
1736
 
        obj2 = self.bus.get("busname", "objectpath2")
1737
 
        self.assertIsInstance(obj2, Unique)
1738
 
        self.assertIsNot(obj1, obj2)
1739
 
 
1740
 
    def test_returns_distinct_busnames(self):
1741
 
        obj1 = self.bus.get("busname1", "objectpath")
1742
 
        self.assertIsInstance(obj1, Unique)
1743
 
        obj2 = self.bus.get("busname2", "objectpath")
1744
 
        self.assertIsInstance(obj2, Unique)
1745
 
        self.assertIsNot(obj1, obj2)
1746
 
 
1747
 
    def test_returns_distinct_both(self):
1748
 
        obj1 = self.bus.get("busname1", "objectpath")
1749
 
        self.assertIsInstance(obj1, Unique)
1750
 
        obj2 = self.bus.get("busname2", "objectpath")
1751
 
        self.assertIsInstance(obj2, Unique)
1752
 
        self.assertIsNot(obj1, obj2)
1753
 
 
1754
 
    def test_returns_same(self):
1755
 
        obj1 = self.bus.get("busname", "objectpath")
1756
 
        self.assertIsInstance(obj1, Unique)
1757
 
        obj2 = self.bus.get("busname", "objectpath")
1758
 
        self.assertIsInstance(obj2, Unique)
1759
 
        self.assertIs(obj1, obj2)
1760
 
 
1761
 
    def test_returns_same_old(self):
1762
 
        obj1 = self.bus.get("busname1", "objectpath1")
1763
 
        self.assertIsInstance(obj1, Unique)
1764
 
        obj2 = self.bus.get("busname2", "objectpath2")
1765
 
        self.assertIsInstance(obj2, Unique)
1766
 
        obj1b = self.bus.get("busname1", "objectpath1")
1767
 
        self.assertIsInstance(obj1b, Unique)
1768
 
        self.assertIsNot(obj1, obj2)
1769
 
        self.assertIsNot(obj2, obj1b)
1770
 
        self.assertIs(obj1, obj1b)
1771
 
 
1772
 
 
1773
1512
class Test_commands_from_options(unittest.TestCase):
1774
1513
 
1775
1514
    def setUp(self):
1780
1519
        self.assert_command_from_args(["--is-enabled", "client"],
1781
1520
                                      command.IsEnabled)
1782
1521
 
1783
 
    def assert_command_from_args(self, args, command_cls, length=1,
1784
 
                                 clients=None, **cmd_attrs):
 
1522
    def assert_command_from_args(self, args, command_cls,
 
1523
                                 **cmd_attrs):
1785
1524
        """Assert that parsing ARGS should result in an instance of
1786
1525
COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
1787
1526
        options = self.parser.parse_args(args)
1788
1527
        check_option_syntax(self.parser, options)
1789
1528
        commands = commands_from_options(options)
1790
 
        self.assertEqual(length, len(commands))
1791
 
        for command in commands:
1792
 
            if isinstance(command, command_cls):
1793
 
                break
1794
 
        else:
1795
 
            self.assertIsInstance(command, command_cls)
1796
 
        if clients is not None:
1797
 
            self.assertEqual(clients, options.client)
 
1529
        self.assertEqual(1, len(commands))
 
1530
        command = commands[0]
 
1531
        self.assertIsInstance(command, command_cls)
1798
1532
        for key, value in cmd_attrs.items():
1799
1533
            self.assertEqual(value, getattr(command, key))
1800
1534
 
1801
 
    def assert_commands_from_args(self, args, commands, clients=None):
1802
 
        for cmd in commands:
1803
 
            self.assert_command_from_args(args, cmd,
1804
 
                                          length=len(commands),
1805
 
                                          clients=clients)
1806
 
 
1807
1535
    def test_is_enabled_short(self):
1808
1536
        self.assert_command_from_args(["-V", "client"],
1809
1537
                                      command.IsEnabled)
2000
1728
                                      verbose=True)
2001
1729
 
2002
1730
 
2003
 
    def test_manual_page_example_1(self):
2004
 
        self.assert_command_from_args("--verbose".split(),
2005
 
                                      command.PrintTable,
2006
 
                                      clients=[],
2007
 
                                      verbose=True)
2008
 
 
2009
 
    def test_manual_page_example_2(self):
2010
 
        self.assert_command_from_args(
2011
 
            "--verbose foo1.example.org foo2.example.org".split(),
2012
 
            command.PrintTable, clients=["foo1.example.org",
2013
 
                                         "foo2.example.org"],
2014
 
            verbose=True)
2015
 
 
2016
 
    def test_manual_page_example_3(self):
2017
 
        self.assert_command_from_args("--enable --all".split(),
2018
 
                                      command.Enable,
2019
 
                                      clients=[])
2020
 
 
2021
 
    def test_manual_page_example_4(self):
2022
 
        self.assert_commands_from_args(
2023
 
            ("--timeout=PT5M --interval=PT1M foo1.example.org"
2024
 
             " foo2.example.org").split(),
2025
 
            [command.SetTimeout, command.SetInterval],
2026
 
            clients=["foo1.example.org", "foo2.example.org"])
2027
 
 
2028
 
    def test_manual_page_example_5(self):
2029
 
        self.assert_command_from_args("--approve --all".split(),
2030
 
                                      command.Approve,
2031
 
                                      clients=[])
2032
 
 
2033
 
 
2034
1731
class TestCommand(unittest.TestCase):
2035
1732
    """Abstract class for tests of command classes"""
2036
1733