/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-08 23:55:34 UTC
  • Revision ID: teddy@recompile.se-20190308235534-kkiwc3oeyp0vxw5h
mandos-ctl: Add new --debug option to show D-Bus calls

* mandos-ctl (PropertyCmd, RemoveCmd, ApproveCmd, DenyCmd): Add debug
  output
  (add_command_line_options): Also add "--debug" options.
  (main): If debug option is set, set logging level to DEBUG.  Also
          add debug output when connecting to Mandos server.
* mandos-ctl.xml (SYNOPSIS): Show --debug option where appropriate.
  (OPTIONS): Document new "--debug" option.

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
 
299
300
    """Abstract class for Actions for setting one client property"""
300
301
    def run_on_one_client(self, client, properties):
301
302
        """Set the Client's D-Bus property"""
 
303
        log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", busname,
 
304
                  client.__dbus_object_path__,
 
305
                  dbus.PROPERTIES_IFACE, client_interface,
 
306
                  self.property, self.value_to_set
 
307
                  if not isinstance(self.value_to_set, dbus.Boolean)
 
308
                  else bool(self.value_to_set))
302
309
        client.Set(client_interface, self.property, self.value_to_set,
303
310
                   dbus_interface=dbus.PROPERTIES_IFACE)
304
311
 
430
437
 
431
438
class RemoveCmd(Command):
432
439
    def run_on_one_client(self, client, properties):
 
440
        log.debug("D-Bus: %s:%s:%s.RemoveClient(%r)", busname,
 
441
                  server_path, server_interface,
 
442
                  str(client.__dbus_object_path__))
433
443
        self.mandos.RemoveClient(client.__dbus_object_path__)
434
444
 
435
445
class ApproveCmd(Command):
436
446
    def run_on_one_client(self, client, properties):
 
447
        log.debug("D-Bus: %s:%s.Approve(True)",
 
448
                  client.__dbus_object_path__, client_interface)
437
449
        client.Approve(dbus.Boolean(True),
438
450
                       dbus_interface=client_interface)
439
451
 
440
452
class DenyCmd(Command):
441
453
    def run_on_one_client(self, client, properties):
 
454
        log.debug("D-Bus: %s:%s.Approve(False)",
 
455
                  client.__dbus_object_path__, client_interface)
442
456
        client.Approve(dbus.Boolean(False),
443
457
                       dbus_interface=client_interface)
444
458
 
565
579
        help="Approve any current client request")
566
580
    approve_deny.add_argument("-D", "--deny", action="store_true",
567
581
                              help="Deny any current client request")
 
582
    parser.add_argument("--debug", action="store_true",
 
583
                        help="Debug mode (show D-Bus commands)")
568
584
    parser.add_argument("--check", action="store_true",
569
585
                        help="Run self-test")
570
586
    parser.add_argument("client", nargs="*", help="Client name")
644
660
    return commands
645
661
 
646
662
 
647
 
def main():
648
 
    parser = argparse.ArgumentParser()
649
 
 
650
 
    add_command_line_options(parser)
651
 
 
652
 
    options = parser.parse_args()
 
663
def check_option_syntax(parser, options):
 
664
    """Apply additional restrictions on options, not expressible in
 
665
argparse"""
653
666
 
654
667
    def has_actions(options):
655
668
        return any((options.enable,
683
696
    if options.is_enabled and len(options.client) > 1:
684
697
        parser.error("--is-enabled requires exactly one client")
685
698
 
 
699
 
 
700
def main():
 
701
    parser = argparse.ArgumentParser()
 
702
 
 
703
    add_command_line_options(parser)
 
704
 
 
705
    options = parser.parse_args()
 
706
 
 
707
    check_option_syntax(parser, options)
 
708
 
686
709
    clientnames = options.client
687
710
 
 
711
    if options.debug:
 
712
        log.setLevel(logging.DEBUG)
 
713
 
688
714
    try:
689
715
        bus = dbus.SystemBus()
 
716
        log.debug("D-Bus: Connect to: (name=%r, path=%r)", busname,
 
717
                  server_path)
690
718
        mandos_dbus_objc = bus.get_object(busname, server_path)
691
719
    except dbus.exceptions.DBusException:
692
720
        log.critical("Could not connect to Mandos server")
705
733
    dbus_filter = NullFilter()
706
734
    try:
707
735
        dbus_logger.addFilter(dbus_filter)
 
736
        log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", busname,
 
737
                  server_path, dbus.OBJECT_MANAGER_IFACE)
708
738
        mandos_clients = {path: ifs_and_props[client_interface]
709
739
                          for path, ifs_and_props in
710
740
                          mandos_serv_object_manager
1093
1123
class TestSetSecretCmd(TestValueArgumentPropertyCmd):
1094
1124
    command = SetSecretCmd
1095
1125
    property = "Secret"
1096
 
    values_to_set = [open("/dev/null", "rb"),
 
1126
    values_to_set = [io.BytesIO(b""),
1097
1127
                     io.BytesIO(b"secret\0xyzzy\nbar")]
1098
1128
    values_to_get = [b"", b"secret\0xyzzy\nbar"]
1099
1129
 
1155
1185
        """Assert that parsing ARGS should result in an instance of
1156
1186
COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
1157
1187
        options = self.parser.parse_args(args)
 
1188
        check_option_syntax(self.parser, options)
1158
1189
        commands = commands_from_options(options)
1159
1190
        self.assertEqual(len(commands), 1)
1160
1191
        command = commands[0]
1325
1356
        self.assert_command_from_args(["-V", "foo"], IsEnabledCmd)
1326
1357
 
1327
1358
 
 
1359
class Test_check_option_syntax(unittest.TestCase):
 
1360
    # This mostly corresponds to the definition from has_actions() in
 
1361
    # check_option_syntax()
 
1362
    actions = {
 
1363
        # The actual values set here are not that important, but we do
 
1364
        # at least stick to the correct types, even though they are
 
1365
        # never used
 
1366
        "enable": True,
 
1367
        "disable": True,
 
1368
        "bump_timeout": True,
 
1369
        "start_checker": True,
 
1370
        "stop_checker": True,
 
1371
        "is_enabled": True,
 
1372
        "remove": True,
 
1373
        "checker": "x",
 
1374
        "timeout": datetime.timedelta(),
 
1375
        "extended_timeout": datetime.timedelta(),
 
1376
        "interval": datetime.timedelta(),
 
1377
        "approved_by_default": True,
 
1378
        "approval_delay": datetime.timedelta(),
 
1379
        "approval_duration": datetime.timedelta(),
 
1380
        "host": "x",
 
1381
        "secret": io.BytesIO(b"x"),
 
1382
        "approve": True,
 
1383
        "deny": True,
 
1384
    }
 
1385
 
 
1386
    def setUp(self):
 
1387
        self.parser = argparse.ArgumentParser()
 
1388
        add_command_line_options(self.parser)
 
1389
 
 
1390
    @contextlib.contextmanager
 
1391
    def assertParseError(self):
 
1392
        with self.assertRaises(SystemExit) as e:
 
1393
            with self.temporarily_suppress_stderr():
 
1394
                yield
 
1395
        # Exit code from argparse is guaranteed to be "2".  Reference:
 
1396
        # https://docs.python.org/3/library/argparse.html#exiting-methods
 
1397
        self.assertEqual(e.exception.code, 2)
 
1398
 
 
1399
    @staticmethod
 
1400
    @contextlib.contextmanager
 
1401
    def temporarily_suppress_stderr():
 
1402
        null = os.open(os.path.devnull, os.O_RDWR)
 
1403
        stderrcopy = os.dup(sys.stderr.fileno())
 
1404
        os.dup2(null, sys.stderr.fileno())
 
1405
        os.close(null)
 
1406
        try:
 
1407
            yield
 
1408
        finally:
 
1409
            # restore stderr
 
1410
            os.dup2(stderrcopy, sys.stderr.fileno())
 
1411
            os.close(stderrcopy)
 
1412
 
 
1413
    def check_option_syntax(self, options):
 
1414
        check_option_syntax(self.parser, options)
 
1415
 
 
1416
    def test_actions_requires_client_or_all(self):
 
1417
        for action, value in self.actions.items():
 
1418
            options = self.parser.parse_args()
 
1419
            setattr(options, action, value)
 
1420
            with self.assertParseError():
 
1421
                self.check_option_syntax(options)
 
1422
 
 
1423
    def test_actions_conflicts_with_verbose(self):
 
1424
        for action, value in self.actions.items():
 
1425
            options = self.parser.parse_args()
 
1426
            setattr(options, action, value)
 
1427
            options.verbose = True
 
1428
            with self.assertParseError():
 
1429
                self.check_option_syntax(options)
 
1430
 
 
1431
    def test_dump_json_conflicts_with_verbose(self):
 
1432
        options = self.parser.parse_args()
 
1433
        options.dump_json = True
 
1434
        options.verbose = True
 
1435
        with self.assertParseError():
 
1436
            self.check_option_syntax(options)
 
1437
 
 
1438
    def test_dump_json_conflicts_with_action(self):
 
1439
        for action, value in self.actions.items():
 
1440
            options = self.parser.parse_args()
 
1441
            setattr(options, action, value)
 
1442
            options.dump_json = True
 
1443
            with self.assertParseError():
 
1444
                self.check_option_syntax(options)
 
1445
 
 
1446
    def test_all_can_not_be_alone(self):
 
1447
        options = self.parser.parse_args()
 
1448
        options.all = True
 
1449
        with self.assertParseError():
 
1450
            self.check_option_syntax(options)
 
1451
 
 
1452
    def test_all_is_ok_with_any_action(self):
 
1453
        for action, value in self.actions.items():
 
1454
            options = self.parser.parse_args()
 
1455
            setattr(options, action, value)
 
1456
            options.all = True
 
1457
            self.check_option_syntax(options)
 
1458
 
 
1459
    def test_is_enabled_fails_without_client(self):
 
1460
        options = self.parser.parse_args()
 
1461
        options.is_enabled = True
 
1462
        with self.assertParseError():
 
1463
            self.check_option_syntax(options)
 
1464
 
 
1465
    def test_is_enabled_works_with_one_client(self):
 
1466
        options = self.parser.parse_args()
 
1467
        options.is_enabled = True
 
1468
        options.client = ["foo"]
 
1469
        self.check_option_syntax(options)
 
1470
 
 
1471
    def test_is_enabled_fails_with_two_clients(self):
 
1472
        options = self.parser.parse_args()
 
1473
        options.is_enabled = True
 
1474
        options.client = ["foo", "barbar"]
 
1475
        with self.assertParseError():
 
1476
            self.check_option_syntax(options)
 
1477
 
 
1478
 
1328
1479
 
1329
1480
def should_only_run_tests():
1330
1481
    parser = argparse.ArgumentParser(add_help=False)