/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 17:11:12 UTC
  • Revision ID: teddy@recompile.se-20190316171112-0tpfk9ved7a2a5x2
mandos-ctl: Refactor and add a few more tests

* mandos-ctl (main): Remove comment and add empty line.
  (rfc3339_duration_to_delta): Change ValueError exception message to
                               use \"{}\"" instead of "{!r}" so Python
                               2 and Python 3 output is the same.
  (Test_check_option_syntax.temporarily_suppress_stderr): Rename to
                                         "redirect_stderr_to_devnull".

  (Test_check_option_syntax
  .test_actions_except_is_enabled_are_ok_with_two_clients):  Rename to
             "test_two_clients_are_ok_with_actions_except_is_enabled".
  (Test_check_option_syntax
   .test_one_client_with_all_actions_except_is_enabled): New.
  (Test_check_option_syntax
   .test_two_clients_with_all_actions_except_is_enabled): - '' -

Show diffs side-by-side

added added

removed removed

Lines of Context:
42
42
import json
43
43
import unittest
44
44
import logging
 
45
import io
 
46
import tempfile
 
47
import contextlib
45
48
 
46
49
import dbus
47
50
 
61
64
 
62
65
locale.setlocale(locale.LC_ALL, "")
63
66
 
64
 
domain = "se.recompile"
65
 
busname = domain + ".Mandos"
66
 
server_path = "/"
67
 
server_interface = domain + ".Mandos"
68
 
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
69
73
version = "1.8.3"
70
74
 
71
75
 
75
79
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
76
80
 
77
81
 
78
 
def milliseconds_to_string(ms):
79
 
    td = datetime.timedelta(0, 0, 0, ms)
80
 
    return ("{days}{hours:02}:{minutes:02}:{seconds:02}"
81
 
            .format(days="{}T".format(td.days) if td.days else "",
82
 
                    hours=td.seconds // 3600,
83
 
                    minutes=(td.seconds % 3600) // 60,
84
 
                    seconds=td.seconds % 60))
 
82
def main():
 
83
    parser = argparse.ArgumentParser()
 
84
 
 
85
    add_command_line_options(parser)
 
86
 
 
87
    options = parser.parse_args()
 
88
 
 
89
    check_option_syntax(parser, options)
 
90
 
 
91
    clientnames = options.client
 
92
 
 
93
    if options.debug:
 
94
        log.setLevel(logging.DEBUG)
 
95
 
 
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)
 
102
    mandos_serv_object_manager = dbus.Interface(
 
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
 
115
    if not clientnames:
 
116
        clients = all_clients
 
117
    else:
 
118
        clients = {}
 
119
        for name in clientnames:
 
120
            for objpath, properties in all_clients.items():
 
121
                if properties["Name"] == name:
 
122
                    clients[objpath] = properties
 
123
                    break
 
124
            else:
 
125
                log.critical("Client not found on server: %r", name)
 
126
                sys.exit(1)
 
127
 
 
128
    commands = commands_from_options(options)
 
129
 
 
130
    for command in commands:
 
131
        command.run(clients, bus, mandos_serv)
 
132
 
 
133
 
 
134
def add_command_line_options(parser):
 
135
    parser.add_argument("--version", action="version",
 
136
                        version="%(prog)s {}".format(version),
 
137
                        help="show version number and exit")
 
138
    parser.add_argument("-a", "--all", action="store_true",
 
139
                        help="Select all clients")
 
140
    parser.add_argument("-v", "--verbose", action="store_true",
 
141
                        help="Print all fields")
 
142
    parser.add_argument("-j", "--dump-json", action="store_true",
 
143
                        help="Dump client data in JSON format")
 
144
    enable_disable = parser.add_mutually_exclusive_group()
 
145
    enable_disable.add_argument("-e", "--enable", action="store_true",
 
146
                                help="Enable client")
 
147
    enable_disable.add_argument("-d", "--disable",
 
148
                                action="store_true",
 
149
                                help="disable client")
 
150
    parser.add_argument("-b", "--bump-timeout", action="store_true",
 
151
                        help="Bump timeout for client")
 
152
    start_stop_checker = parser.add_mutually_exclusive_group()
 
153
    start_stop_checker.add_argument("--start-checker",
 
154
                                    action="store_true",
 
155
                                    help="Start checker for client")
 
156
    start_stop_checker.add_argument("--stop-checker",
 
157
                                    action="store_true",
 
158
                                    help="Stop checker for client")
 
159
    parser.add_argument("-V", "--is-enabled", action="store_true",
 
160
                        help="Check if client is enabled")
 
161
    parser.add_argument("-r", "--remove", action="store_true",
 
162
                        help="Remove client")
 
163
    parser.add_argument("-c", "--checker",
 
164
                        help="Set checker command for client")
 
165
    parser.add_argument("-t", "--timeout", type=string_to_delta,
 
166
                        help="Set timeout for client")
 
167
    parser.add_argument("--extended-timeout", type=string_to_delta,
 
168
                        help="Set extended timeout for client")
 
169
    parser.add_argument("-i", "--interval", type=string_to_delta,
 
170
                        help="Set checker interval for client")
 
171
    approve_deny_default = parser.add_mutually_exclusive_group()
 
172
    approve_deny_default.add_argument(
 
173
        "--approve-by-default", action="store_true",
 
174
        default=None, dest="approved_by_default",
 
175
        help="Set client to be approved by default")
 
176
    approve_deny_default.add_argument(
 
177
        "--deny-by-default", action="store_false",
 
178
        dest="approved_by_default",
 
179
        help="Set client to be denied by default")
 
180
    parser.add_argument("--approval-delay", type=string_to_delta,
 
181
                        help="Set delay before client approve/deny")
 
182
    parser.add_argument("--approval-duration", type=string_to_delta,
 
183
                        help="Set duration of one client approval")
 
184
    parser.add_argument("-H", "--host", help="Set host for client")
 
185
    parser.add_argument("-s", "--secret",
 
186
                        type=argparse.FileType(mode="rb"),
 
187
                        help="Set password blob (file) for client")
 
188
    approve_deny = parser.add_mutually_exclusive_group()
 
189
    approve_deny.add_argument(
 
190
        "-A", "--approve", action="store_true",
 
191
        help="Approve any current client request")
 
192
    approve_deny.add_argument("-D", "--deny", action="store_true",
 
193
                              help="Deny any current client request")
 
194
    parser.add_argument("--debug", action="store_true",
 
195
                        help="Debug mode (show D-Bus commands)")
 
196
    parser.add_argument("--check", action="store_true",
 
197
                        help="Run self-test")
 
198
    parser.add_argument("client", nargs="*", help="Client name")
 
199
 
 
200
 
 
201
def string_to_delta(interval):
 
202
    """Parse a string and return a datetime.timedelta"""
 
203
 
 
204
    try:
 
205
        return rfc3339_duration_to_delta(interval)
 
206
    except ValueError as e:
 
207
        log.warning("%s - Parsing as pre-1.6.1 interval instead",
 
208
                    ' '.join(e.args))
 
209
    return parse_pre_1_6_1_interval(interval)
85
210
 
86
211
 
87
212
def rfc3339_duration_to_delta(duration):
107
232
    >>> rfc3339_duration_to_delta("")
108
233
    Traceback (most recent call last):
109
234
    ...
110
 
    ValueError: Invalid RFC 3339 duration: u''
 
235
    ValueError: Invalid RFC 3339 duration: ""
111
236
    >>> # Must start with "P":
112
237
    >>> rfc3339_duration_to_delta("1D")
113
238
    Traceback (most recent call last):
114
239
    ...
115
 
    ValueError: Invalid RFC 3339 duration: u'1D'
 
240
    ValueError: Invalid RFC 3339 duration: "1D"
116
241
    >>> # Must use correct order
117
242
    >>> rfc3339_duration_to_delta("PT1S2M")
118
243
    Traceback (most recent call last):
119
244
    ...
120
 
    ValueError: Invalid RFC 3339 duration: u'PT1S2M'
 
245
    ValueError: Invalid RFC 3339 duration: "PT1S2M"
121
246
    >>> # Time needs time marker
122
247
    >>> rfc3339_duration_to_delta("P1H2S")
123
248
    Traceback (most recent call last):
124
249
    ...
125
 
    ValueError: Invalid RFC 3339 duration: u'P1H2S'
 
250
    ValueError: Invalid RFC 3339 duration: "P1H2S"
126
251
    >>> # Weeks can not be combined with anything else
127
252
    >>> rfc3339_duration_to_delta("P1D2W")
128
253
    Traceback (most recent call last):
129
254
    ...
130
 
    ValueError: Invalid RFC 3339 duration: u'P1D2W'
 
255
    ValueError: Invalid RFC 3339 duration: "P1D2W"
131
256
    >>> rfc3339_duration_to_delta("P2W2H")
132
257
    Traceback (most recent call last):
133
258
    ...
134
 
    ValueError: Invalid RFC 3339 duration: u'P2W2H'
 
259
    ValueError: Invalid RFC 3339 duration: "P2W2H"
135
260
    """
136
261
 
137
262
    # Parsing an RFC 3339 duration with regular expressions is not
208
333
                break
209
334
        else:
210
335
            # No currently valid tokens were found
211
 
            raise ValueError("Invalid RFC 3339 duration: {!r}"
 
336
            raise ValueError("Invalid RFC 3339 duration: \"{}\""
212
337
                             .format(duration))
213
338
    # End token found
214
339
    return value
215
340
 
216
341
 
217
 
def string_to_delta(interval):
218
 
    """Parse a string and return a datetime.timedelta"""
219
 
 
220
 
    try:
221
 
        return rfc3339_duration_to_delta(interval)
222
 
    except ValueError as e:
223
 
        log.warning("%s - Parsing as pre-1.6.1 interval instead",
224
 
                    ' '.join(e.args))
225
 
    return parse_pre_1_6_1_interval(interval)
226
 
 
227
 
 
228
342
def parse_pre_1_6_1_interval(interval):
229
343
    """Parse an interval string as documented by Mandos before 1.6.1,
230
344
    and return a datetime.timedelta
268
382
    return value
269
383
 
270
384
 
271
 
## Classes for commands.
272
 
 
273
 
# Abstract classes first
 
385
def check_option_syntax(parser, options):
 
386
    """Apply additional restrictions on options, not expressible in
 
387
argparse"""
 
388
 
 
389
    def has_actions(options):
 
390
        return any((options.enable,
 
391
                    options.disable,
 
392
                    options.bump_timeout,
 
393
                    options.start_checker,
 
394
                    options.stop_checker,
 
395
                    options.is_enabled,
 
396
                    options.remove,
 
397
                    options.checker is not None,
 
398
                    options.timeout is not None,
 
399
                    options.extended_timeout is not None,
 
400
                    options.interval is not None,
 
401
                    options.approved_by_default is not None,
 
402
                    options.approval_delay is not None,
 
403
                    options.approval_duration is not None,
 
404
                    options.host is not None,
 
405
                    options.secret is not None,
 
406
                    options.approve,
 
407
                    options.deny))
 
408
 
 
409
    if has_actions(options) and not (options.client or options.all):
 
410
        parser.error("Options require clients names or --all.")
 
411
    if options.verbose and has_actions(options):
 
412
        parser.error("--verbose can only be used alone.")
 
413
    if options.dump_json and (options.verbose
 
414
                              or has_actions(options)):
 
415
        parser.error("--dump-json can only be used alone.")
 
416
    if options.all and not has_actions(options):
 
417
        parser.error("--all requires an action.")
 
418
    if options.is_enabled and len(options.client) > 1:
 
419
        parser.error("--is-enabled requires exactly one client")
 
420
    if options.remove:
 
421
        options.remove = False
 
422
        if has_actions(options) and not options.deny:
 
423
            parser.error("--remove can only be combined with --deny")
 
424
        options.remove = True
 
425
 
 
426
 
 
427
def get_mandos_dbus_object(bus):
 
428
    log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
429
              dbus_busname, server_dbus_path)
 
430
    with if_dbus_exception_log_with_exception_and_exit(
 
431
            "Could not connect to Mandos server: %s"):
 
432
        mandos_dbus_object = bus.get_object(dbus_busname,
 
433
                                            server_dbus_path)
 
434
    return mandos_dbus_object
 
435
 
 
436
 
 
437
@contextlib.contextmanager
 
438
def if_dbus_exception_log_with_exception_and_exit(*args, **kwargs):
 
439
    try:
 
440
        yield
 
441
    except dbus.exceptions.DBusException as e:
 
442
        log.critical(*(args + (e,)), **kwargs)
 
443
        sys.exit(1)
 
444
 
 
445
 
 
446
def get_managed_objects(object_manager):
 
447
    log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", dbus_busname,
 
448
              server_dbus_path, dbus.OBJECT_MANAGER_IFACE)
 
449
    with if_dbus_exception_log_with_exception_and_exit(
 
450
            "Failed to access Mandos server through D-Bus:\n%s"):
 
451
        with SilenceLogger("dbus.proxies"):
 
452
            managed_objects = object_manager.GetManagedObjects()
 
453
    return managed_objects
 
454
 
 
455
 
 
456
class SilenceLogger(object):
 
457
    "Simple context manager to silence a particular logger"
 
458
    def __init__(self, loggername):
 
459
        self.logger = logging.getLogger(loggername)
 
460
 
 
461
    def __enter__(self):
 
462
        self.logger.addFilter(self.nullfilter)
 
463
        return self
 
464
 
 
465
    class NullFilter(logging.Filter):
 
466
        def filter(self, record):
 
467
            return False
 
468
 
 
469
    nullfilter = NullFilter()
 
470
 
 
471
    def __exit__(self, exc_type, exc_val, exc_tb):
 
472
        self.logger.removeFilter(self.nullfilter)
 
473
 
 
474
 
 
475
def commands_from_options(options):
 
476
 
 
477
    commands = []
 
478
 
 
479
    if options.is_enabled:
 
480
        commands.append(IsEnabledCmd())
 
481
 
 
482
    if options.approve:
 
483
        commands.append(ApproveCmd())
 
484
 
 
485
    if options.deny:
 
486
        commands.append(DenyCmd())
 
487
 
 
488
    if options.remove:
 
489
        commands.append(RemoveCmd())
 
490
 
 
491
    if options.dump_json:
 
492
        commands.append(DumpJSONCmd())
 
493
 
 
494
    if options.enable:
 
495
        commands.append(EnableCmd())
 
496
 
 
497
    if options.disable:
 
498
        commands.append(DisableCmd())
 
499
 
 
500
    if options.bump_timeout:
 
501
        commands.append(BumpTimeoutCmd())
 
502
 
 
503
    if options.start_checker:
 
504
        commands.append(StartCheckerCmd())
 
505
 
 
506
    if options.stop_checker:
 
507
        commands.append(StopCheckerCmd())
 
508
 
 
509
    if options.approved_by_default is not None:
 
510
        if options.approved_by_default:
 
511
            commands.append(ApproveByDefaultCmd())
 
512
        else:
 
513
            commands.append(DenyByDefaultCmd())
 
514
 
 
515
    if options.checker is not None:
 
516
        commands.append(SetCheckerCmd(options.checker))
 
517
 
 
518
    if options.host is not None:
 
519
        commands.append(SetHostCmd(options.host))
 
520
 
 
521
    if options.secret is not None:
 
522
        commands.append(SetSecretCmd(options.secret))
 
523
 
 
524
    if options.timeout is not None:
 
525
        commands.append(SetTimeoutCmd(options.timeout))
 
526
 
 
527
    if options.extended_timeout:
 
528
        commands.append(
 
529
            SetExtendedTimeoutCmd(options.extended_timeout))
 
530
 
 
531
    if options.interval is not None:
 
532
        commands.append(SetIntervalCmd(options.interval))
 
533
 
 
534
    if options.approval_delay is not None:
 
535
        commands.append(SetApprovalDelayCmd(options.approval_delay))
 
536
 
 
537
    if options.approval_duration is not None:
 
538
        commands.append(
 
539
            SetApprovalDurationCmd(options.approval_duration))
 
540
 
 
541
    # If no command option has been given, show table of clients,
 
542
    # optionally verbosely
 
543
    if not commands:
 
544
        commands.append(PrintTableCmd(verbose=options.verbose))
 
545
 
 
546
    return commands
 
547
 
 
548
 
274
549
class Command(object):
275
550
    """Abstract class for commands"""
276
 
    def run(self, mandos, clients):
 
551
    def run(self, clients, bus=None, mandos=None):
277
552
        """Normal commands should implement run_on_one_client(), but
278
553
        commands which want to operate on all clients at the same time
279
554
        can override this run() method instead."""
280
555
        self.mandos = mandos
281
 
        for client, properties in clients.items():
 
556
        for clientpath, properties in clients.items():
 
557
            log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
558
                      dbus_busname, str(clientpath))
 
559
            client = bus.get_object(dbus_busname, clientpath)
282
560
            self.run_on_one_client(client, properties)
283
561
 
284
 
class PrintCmd(Command):
285
 
    """Abstract class for commands printing client details"""
 
562
 
 
563
class IsEnabledCmd(Command):
 
564
    def run(self, clients, bus=None, mandos=None):
 
565
        client, properties = next(iter(clients.items()))
 
566
        if self.is_enabled(client, properties):
 
567
            sys.exit(0)
 
568
        sys.exit(1)
 
569
    def is_enabled(self, client, properties):
 
570
        return properties["Enabled"]
 
571
 
 
572
 
 
573
class ApproveCmd(Command):
 
574
    def run_on_one_client(self, client, properties):
 
575
        log.debug("D-Bus: %s:%s:%s.Approve(True)", dbus_busname,
 
576
                  client.__dbus_object_path__, client_dbus_interface)
 
577
        client.Approve(dbus.Boolean(True),
 
578
                       dbus_interface=client_dbus_interface)
 
579
 
 
580
 
 
581
class DenyCmd(Command):
 
582
    def run_on_one_client(self, client, properties):
 
583
        log.debug("D-Bus: %s:%s:%s.Approve(False)", dbus_busname,
 
584
                  client.__dbus_object_path__, client_dbus_interface)
 
585
        client.Approve(dbus.Boolean(False),
 
586
                       dbus_interface=client_dbus_interface)
 
587
 
 
588
 
 
589
class RemoveCmd(Command):
 
590
    def run_on_one_client(self, client, properties):
 
591
        log.debug("D-Bus: %s:%s:%s.RemoveClient(%r)", dbus_busname,
 
592
                  server_dbus_path, server_dbus_interface,
 
593
                  str(client.__dbus_object_path__))
 
594
        self.mandos.RemoveClient(client.__dbus_object_path__)
 
595
 
 
596
 
 
597
class OutputCmd(Command):
 
598
    """Abstract class for commands outputting client details"""
286
599
    all_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
287
600
                    "Created", "Interval", "Host", "KeyID",
288
601
                    "Fingerprint", "CheckerRunning", "LastEnabled",
290
603
                    "LastApprovalRequest", "ApprovalDelay",
291
604
                    "ApprovalDuration", "Checker", "ExtendedTimeout",
292
605
                    "Expires", "LastCheckerStatus")
293
 
    def run(self, mandos, clients):
294
 
        print(self.output(clients))
295
 
 
296
 
class PropertyCmd(Command):
297
 
    """Abstract class for Actions for setting one client property"""
298
 
    def run_on_one_client(self, client, properties):
299
 
        """Set the Client's D-Bus property"""
300
 
        client.Set(client_interface, self.property, self.value_to_set,
301
 
                   dbus_interface=dbus.PROPERTIES_IFACE)
302
 
 
303
 
class ValueArgumentMixIn(object):
304
 
    """Mixin class for commands taking a value as argument"""
305
 
    def __init__(self, value):
306
 
        self.value_to_set = value
307
 
 
308
 
class MillisecondsValueArgumentMixIn(ValueArgumentMixIn):
309
 
    """Mixin class for commands taking a value argument as
310
 
    milliseconds."""
311
 
    @property
312
 
    def value_to_set(self):
313
 
        return self._vts
314
 
    @value_to_set.setter
315
 
    def value_to_set(self, value):
316
 
        """When setting, convert value to a datetime.timedelta"""
317
 
        self._vts = string_to_delta(value).total_seconds() * 1000
318
 
 
319
 
# Actual (non-abstract) command classes
320
 
 
321
 
class PrintTableCmd(PrintCmd):
 
606
 
 
607
    def run(self, clients, bus=None, mandos=None):
 
608
        print(self.output(clients.values()))
 
609
 
 
610
    def output(self, clients):
 
611
        raise NotImplementedError()
 
612
 
 
613
 
 
614
class DumpJSONCmd(OutputCmd):
 
615
    def output(self, clients):
 
616
        data = {client["Name"]:
 
617
                {key: self.dbus_boolean_to_bool(client[key])
 
618
                 for key in self.all_keywords}
 
619
                for client in clients}
 
620
        return json.dumps(data, indent=4, separators=(',', ': '))
 
621
 
 
622
    @staticmethod
 
623
    def dbus_boolean_to_bool(value):
 
624
        if isinstance(value, dbus.Boolean):
 
625
            value = bool(value)
 
626
        return value
 
627
 
 
628
 
 
629
class PrintTableCmd(OutputCmd):
322
630
    def __init__(self, verbose=False):
323
631
        self.verbose = verbose
324
632
 
325
633
    def output(self, clients):
326
 
        default_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
 
634
        default_keywords = ("Name", "Enabled", "Timeout",
 
635
                            "LastCheckedOK")
327
636
        keywords = default_keywords
328
637
        if self.verbose:
329
638
            keywords = self.all_keywords
330
 
        return str(self.TableOfClients(clients.values(), keywords))
 
639
        return str(self.TableOfClients(clients, keywords))
331
640
 
332
641
    class TableOfClients(object):
333
642
        tableheaders = {
353
662
            "LastCheckerStatus": "Last Checker Status",
354
663
        }
355
664
 
356
 
        def __init__(self, clients, keywords, tableheaders=None):
 
665
        def __init__(self, clients, keywords):
357
666
            self.clients = clients
358
667
            self.keywords = keywords
359
 
            if tableheaders is not None:
360
 
                self.tableheaders = tableheaders
361
668
 
362
669
        def __str__(self):
363
670
            return "\n".join(self.rows())
386
693
        def string_from_client(self, client, key):
387
694
            return self.valuetostring(client[key], key)
388
695
 
389
 
        @staticmethod
390
 
        def valuetostring(value, keyword):
 
696
        @classmethod
 
697
        def valuetostring(cls, value, keyword):
391
698
            if isinstance(value, dbus.Boolean):
392
699
                return "Yes" if value else "No"
393
700
            if keyword in ("Timeout", "Interval", "ApprovalDelay",
394
701
                           "ApprovalDuration", "ExtendedTimeout"):
395
 
                return milliseconds_to_string(value)
 
702
                return cls.milliseconds_to_string(value)
396
703
            return str(value)
397
704
 
398
705
        def header_line(self, format_string):
403
710
                **{key: self.string_from_client(client, key)
404
711
                   for key in self.keywords})
405
712
 
406
 
 
407
 
 
408
 
class DumpJSONCmd(PrintCmd):
409
 
    def output(self, clients):
410
 
        data = {client["Name"]:
411
 
                {key: self.dbus_boolean_to_bool(client[key])
412
 
                 for key in self.all_keywords}
413
 
                for client in clients.values()}
414
 
        return json.dumps(data, indent=4, separators=(',', ': '))
415
 
    @staticmethod
416
 
    def dbus_boolean_to_bool(value):
417
 
        if isinstance(value, dbus.Boolean):
418
 
            value = bool(value)
419
 
        return value
420
 
 
421
 
class IsEnabledCmd(Command):
422
 
    def run_on_one_client(self, client, properties):
423
 
        if self.is_enabled(client, properties):
424
 
            sys.exit(0)
425
 
        sys.exit(1)
426
 
    def is_enabled(self, client, properties):
427
 
        return bool(properties["Enabled"])
428
 
 
429
 
class RemoveCmd(Command):
430
 
    def run_on_one_client(self, client, properties):
431
 
        self.mandos.RemoveClient(client.__dbus_object_path__)
432
 
 
433
 
class ApproveCmd(Command):
434
 
    def run_on_one_client(self, client, properties):
435
 
        client.Approve(dbus.Boolean(True),
436
 
                       dbus_interface=client_interface)
437
 
 
438
 
class DenyCmd(Command):
439
 
    def run_on_one_client(self, client, properties):
440
 
        client.Approve(dbus.Boolean(False),
441
 
                       dbus_interface=client_interface)
 
713
        @staticmethod
 
714
        def milliseconds_to_string(ms):
 
715
            td = datetime.timedelta(0, 0, 0, ms)
 
716
            return ("{days}{hours:02}:{minutes:02}:{seconds:02}"
 
717
                    .format(days="{}T".format(td.days)
 
718
                            if td.days else "",
 
719
                            hours=td.seconds // 3600,
 
720
                            minutes=(td.seconds % 3600) // 60,
 
721
                            seconds=td.seconds % 60))
 
722
 
 
723
 
 
724
class PropertyCmd(Command):
 
725
    """Abstract class for Actions for setting one client property"""
 
726
 
 
727
    def run_on_one_client(self, client, properties):
 
728
        """Set the Client's D-Bus property"""
 
729
        log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", dbus_busname,
 
730
                  client.__dbus_object_path__,
 
731
                  dbus.PROPERTIES_IFACE, client_dbus_interface,
 
732
                  self.propname, self.value_to_set
 
733
                  if not isinstance(self.value_to_set, dbus.Boolean)
 
734
                  else bool(self.value_to_set))
 
735
        client.Set(client_dbus_interface, self.propname,
 
736
                   self.value_to_set,
 
737
                   dbus_interface=dbus.PROPERTIES_IFACE)
 
738
 
 
739
    @property
 
740
    def propname(self):
 
741
        raise NotImplementedError()
 
742
 
442
743
 
443
744
class EnableCmd(PropertyCmd):
444
 
    property = "Enabled"
 
745
    propname = "Enabled"
445
746
    value_to_set = dbus.Boolean(True)
446
747
 
 
748
 
447
749
class DisableCmd(PropertyCmd):
448
 
    property = "Enabled"
 
750
    propname = "Enabled"
449
751
    value_to_set = dbus.Boolean(False)
450
752
 
 
753
 
451
754
class BumpTimeoutCmd(PropertyCmd):
452
 
    property = "LastCheckedOK"
 
755
    propname = "LastCheckedOK"
453
756
    value_to_set = ""
454
757
 
 
758
 
455
759
class StartCheckerCmd(PropertyCmd):
456
 
    property = "CheckerRunning"
 
760
    propname = "CheckerRunning"
457
761
    value_to_set = dbus.Boolean(True)
458
762
 
 
763
 
459
764
class StopCheckerCmd(PropertyCmd):
460
 
    property = "CheckerRunning"
 
765
    propname = "CheckerRunning"
461
766
    value_to_set = dbus.Boolean(False)
462
767
 
 
768
 
463
769
class ApproveByDefaultCmd(PropertyCmd):
464
 
    property = "ApprovedByDefault"
 
770
    propname = "ApprovedByDefault"
465
771
    value_to_set = dbus.Boolean(True)
466
772
 
 
773
 
467
774
class DenyByDefaultCmd(PropertyCmd):
468
 
    property = "ApprovedByDefault"
 
775
    propname = "ApprovedByDefault"
469
776
    value_to_set = dbus.Boolean(False)
470
777
 
471
 
class SetCheckerCmd(PropertyCmd, ValueArgumentMixIn):
472
 
    property = "Checker"
473
 
 
474
 
class SetHostCmd(PropertyCmd, ValueArgumentMixIn):
475
 
    property = "Host"
476
 
 
477
 
class SetSecretCmd(PropertyCmd, ValueArgumentMixIn):
478
 
    property = "Secret"
479
 
 
480
 
class SetTimeoutCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
481
 
    property = "Timeout"
482
 
 
483
 
class SetExtendedTimeoutCmd(PropertyCmd,
484
 
                            MillisecondsValueArgumentMixIn):
485
 
    property = "ExtendedTimeout"
486
 
 
487
 
class SetIntervalCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
488
 
    property = "Interval"
489
 
 
490
 
class SetApprovalDelayCmd(PropertyCmd,
491
 
                          MillisecondsValueArgumentMixIn):
492
 
    property = "ApprovalDelay"
493
 
 
494
 
class SetApprovalDurationCmd(PropertyCmd,
495
 
                             MillisecondsValueArgumentMixIn):
496
 
    property = "ApprovalDuration"
497
 
 
498
 
def has_actions(options):
499
 
    return any((options.enable,
500
 
                options.disable,
501
 
                options.bump_timeout,
502
 
                options.start_checker,
503
 
                options.stop_checker,
504
 
                options.is_enabled,
505
 
                options.remove,
506
 
                options.checker is not None,
507
 
                options.timeout is not None,
508
 
                options.extended_timeout is not None,
509
 
                options.interval is not None,
510
 
                options.approved_by_default is not None,
511
 
                options.approval_delay is not None,
512
 
                options.approval_duration is not None,
513
 
                options.host is not None,
514
 
                options.secret is not None,
515
 
                options.approve,
516
 
                options.deny))
517
 
 
518
 
def add_command_line_options(parser):
519
 
    parser.add_argument("--version", action="version",
520
 
                        version="%(prog)s {}".format(version),
521
 
                        help="show version number and exit")
522
 
    parser.add_argument("-a", "--all", action="store_true",
523
 
                        help="Select all clients")
524
 
    parser.add_argument("-v", "--verbose", action="store_true",
525
 
                        help="Print all fields")
526
 
    parser.add_argument("-j", "--dump-json", action="store_true",
527
 
                        help="Dump client data in JSON format")
528
 
    enable_disable = parser.add_mutually_exclusive_group()
529
 
    enable_disable.add_argument("-e", "--enable", action="store_true",
530
 
                                help="Enable client")
531
 
    enable_disable.add_argument("-d", "--disable",
532
 
                                action="store_true",
533
 
                                help="disable client")
534
 
    parser.add_argument("-b", "--bump-timeout", action="store_true",
535
 
                        help="Bump timeout for client")
536
 
    start_stop_checker = parser.add_mutually_exclusive_group()
537
 
    start_stop_checker.add_argument("--start-checker",
538
 
                                    action="store_true",
539
 
                                    help="Start checker for client")
540
 
    start_stop_checker.add_argument("--stop-checker",
541
 
                                    action="store_true",
542
 
                                    help="Stop checker for client")
543
 
    parser.add_argument("-V", "--is-enabled", action="store_true",
544
 
                        help="Check if client is enabled")
545
 
    parser.add_argument("-r", "--remove", action="store_true",
546
 
                        help="Remove client")
547
 
    parser.add_argument("-c", "--checker",
548
 
                        help="Set checker command for client")
549
 
    parser.add_argument("-t", "--timeout",
550
 
                        help="Set timeout for client")
551
 
    parser.add_argument("--extended-timeout",
552
 
                        help="Set extended timeout for client")
553
 
    parser.add_argument("-i", "--interval",
554
 
                        help="Set checker interval for client")
555
 
    approve_deny_default = parser.add_mutually_exclusive_group()
556
 
    approve_deny_default.add_argument(
557
 
        "--approve-by-default", action="store_true",
558
 
        default=None, dest="approved_by_default",
559
 
        help="Set client to be approved by default")
560
 
    approve_deny_default.add_argument(
561
 
        "--deny-by-default", action="store_false",
562
 
        dest="approved_by_default",
563
 
        help="Set client to be denied by default")
564
 
    parser.add_argument("--approval-delay",
565
 
                        help="Set delay before client approve/deny")
566
 
    parser.add_argument("--approval-duration",
567
 
                        help="Set duration of one client approval")
568
 
    parser.add_argument("-H", "--host", help="Set host for client")
569
 
    parser.add_argument("-s", "--secret",
570
 
                        type=argparse.FileType(mode="rb"),
571
 
                        help="Set password blob (file) for client")
572
 
    approve_deny = parser.add_mutually_exclusive_group()
573
 
    approve_deny.add_argument(
574
 
        "-A", "--approve", action="store_true",
575
 
        help="Approve any current client request")
576
 
    approve_deny.add_argument("-D", "--deny", action="store_true",
577
 
                              help="Deny any current client request")
578
 
    parser.add_argument("--check", action="store_true",
579
 
                        help="Run self-test")
580
 
    parser.add_argument("client", nargs="*", help="Client name")
581
 
 
582
 
 
583
 
def commands_from_options(options):
584
 
 
585
 
    commands = []
586
 
 
587
 
    if options.dump_json:
588
 
        commands.append(DumpJSONCmd())
589
 
 
590
 
    if options.enable:
591
 
        commands.append(EnableCmd())
592
 
 
593
 
    if options.disable:
594
 
        commands.append(DisableCmd())
595
 
 
596
 
    if options.bump_timeout:
597
 
        commands.append(BumpTimeoutCmd())
598
 
 
599
 
    if options.start_checker:
600
 
        commands.append(StartCheckerCmd())
601
 
 
602
 
    if options.stop_checker:
603
 
        commands.append(StopCheckerCmd())
604
 
 
605
 
    if options.is_enabled:
606
 
        commands.append(IsEnabledCmd())
607
 
 
608
 
    if options.remove:
609
 
        commands.append(RemoveCmd())
610
 
 
611
 
    if options.checker is not None:
612
 
        commands.append(SetCheckerCmd())
613
 
 
614
 
    if options.timeout is not None:
615
 
        commands.append(SetTimeoutCmd(options.timeout))
616
 
 
617
 
    if options.extended_timeout:
618
 
        commands.append(
619
 
            SetExtendedTimeoutCmd(options.extended_timeout))
620
 
 
621
 
    if options.interval is not None:
622
 
        command.append(SetIntervalCmd(options.interval))
623
 
 
624
 
    if options.approved_by_default is not None:
625
 
        if options.approved_by_default:
626
 
            command.append(ApproveByDefaultCmd())
627
 
        else:
628
 
            command.append(DenyByDefaultCmd())
629
 
 
630
 
    if options.approval_delay is not None:
631
 
        command.append(SetApprovalDelayCmd(options.approval_delay))
632
 
 
633
 
    if options.approval_duration is not None:
634
 
        command.append(
635
 
            SetApprovalDurationCmd(options.approval_duration))
636
 
 
637
 
    if options.host is not None:
638
 
        command.append(SetHostCmd(options.host))
639
 
 
640
 
    if options.secret is not None:
641
 
        command.append(SetSecretCmd(options.secret))
642
 
 
643
 
    if options.approve:
644
 
        commands.append(ApproveCmd())
645
 
 
646
 
    if options.deny:
647
 
        commands.append(DenyCmd())
648
 
 
649
 
    # If no command option has been given, show table of clients,
650
 
    # optionally verbosely
651
 
    if not commands:
652
 
        commands.append(PrintTableCmd(verbose=options.verbose))
653
 
 
654
 
    return commands
655
 
 
656
 
 
657
 
def main():
658
 
    parser = argparse.ArgumentParser()
659
 
 
660
 
    add_command_line_options(parser)
661
 
 
662
 
    options = parser.parse_args()
663
 
 
664
 
    if has_actions(options) and not (options.client or options.all):
665
 
        parser.error("Options require clients names or --all.")
666
 
    if options.verbose and has_actions(options):
667
 
        parser.error("--verbose can only be used alone.")
668
 
    if options.dump_json and (options.verbose
669
 
                              or has_actions(options)):
670
 
        parser.error("--dump-json can only be used alone.")
671
 
    if options.all and not has_actions(options):
672
 
        parser.error("--all requires an action.")
673
 
    if options.is_enabled and len(options.client) > 1:
674
 
        parser.error("--is-enabled requires exactly one client")
675
 
 
676
 
    clientnames = options.client
677
 
 
678
 
    try:
679
 
        bus = dbus.SystemBus()
680
 
        mandos_dbus_objc = bus.get_object(busname, server_path)
681
 
    except dbus.exceptions.DBusException:
682
 
        log.critical("Could not connect to Mandos server")
683
 
        sys.exit(1)
684
 
 
685
 
    mandos_serv = dbus.Interface(mandos_dbus_objc,
686
 
                                 dbus_interface=server_interface)
687
 
    mandos_serv_object_manager = dbus.Interface(
688
 
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
689
 
 
690
 
    # Filter out log message from dbus module
691
 
    dbus_logger = logging.getLogger("dbus.proxies")
692
 
    class NullFilter(logging.Filter):
693
 
        def filter(self, record):
694
 
            return False
695
 
    dbus_filter = NullFilter()
696
 
    try:
697
 
        dbus_logger.addFilter(dbus_filter)
698
 
        mandos_clients = {path: ifs_and_props[client_interface]
699
 
                          for path, ifs_and_props in
700
 
                          mandos_serv_object_manager
701
 
                          .GetManagedObjects().items()
702
 
                          if client_interface in ifs_and_props}
703
 
    except dbus.exceptions.DBusException as e:
704
 
        log.critical("Failed to access Mandos server through D-Bus:"
705
 
                     "\n%s", e)
706
 
        sys.exit(1)
707
 
    finally:
708
 
        # restore dbus logger
709
 
        dbus_logger.removeFilter(dbus_filter)
710
 
 
711
 
    # Compile dict of (clients: properties) to process
712
 
    clients = {}
713
 
 
714
 
    if not clientnames:
715
 
        clients = {bus.get_object(busname, path): properties
716
 
                   for path, properties in mandos_clients.items()}
717
 
    else:
718
 
        for name in clientnames:
719
 
            for path, client in mandos_clients.items():
720
 
                if client["Name"] == name:
721
 
                    client_objc = bus.get_object(busname, path)
722
 
                    clients[client_objc] = client
723
 
                    break
724
 
            else:
725
 
                log.critical("Client not found on server: %r", name)
726
 
                sys.exit(1)
727
 
 
728
 
    # Run all commands on clients
729
 
    commands = commands_from_options(options)
730
 
    for command in commands:
731
 
        command.run(mandos_serv, clients)
 
778
 
 
779
class PropertyValueCmd(PropertyCmd):
 
780
    """Abstract class for PropertyCmd recieving a value as argument"""
 
781
    def __init__(self, value):
 
782
        self.value_to_set = value
 
783
 
 
784
 
 
785
class SetCheckerCmd(PropertyValueCmd):
 
786
    propname = "Checker"
 
787
 
 
788
 
 
789
class SetHostCmd(PropertyValueCmd):
 
790
    propname = "Host"
 
791
 
 
792
 
 
793
class SetSecretCmd(PropertyValueCmd):
 
794
    propname = "Secret"
 
795
 
 
796
    @property
 
797
    def value_to_set(self):
 
798
        return self._vts
 
799
 
 
800
    @value_to_set.setter
 
801
    def value_to_set(self, value):
 
802
        """When setting, read data from supplied file object"""
 
803
        self._vts = value.read()
 
804
        value.close()
 
805
 
 
806
 
 
807
class MillisecondsPropertyValueArgumentCmd(PropertyValueCmd):
 
808
    """Abstract class for PropertyValueCmd taking a value argument as
 
809
a datetime.timedelta() but should store it as milliseconds."""
 
810
 
 
811
    @property
 
812
    def value_to_set(self):
 
813
        return self._vts
 
814
 
 
815
    @value_to_set.setter
 
816
    def value_to_set(self, value):
 
817
        """When setting, convert value from a datetime.timedelta"""
 
818
        self._vts = int(round(value.total_seconds() * 1000))
 
819
 
 
820
 
 
821
class SetTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
 
822
    propname = "Timeout"
 
823
 
 
824
 
 
825
class SetExtendedTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
 
826
    propname = "ExtendedTimeout"
 
827
 
 
828
 
 
829
class SetIntervalCmd(MillisecondsPropertyValueArgumentCmd):
 
830
    propname = "Interval"
 
831
 
 
832
 
 
833
class SetApprovalDelayCmd(MillisecondsPropertyValueArgumentCmd):
 
834
    propname = "ApprovalDelay"
 
835
 
 
836
 
 
837
class SetApprovalDurationCmd(MillisecondsPropertyValueArgumentCmd):
 
838
    propname = "ApprovalDuration"
 
839
 
732
840
 
733
841
 
734
 
class Test_milliseconds_to_string(unittest.TestCase):
735
 
    def test_all(self):
736
 
        self.assertEqual(milliseconds_to_string(93785000),
737
 
                         "1T02:03:05")
738
 
    def test_no_days(self):
739
 
        self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
740
 
    def test_all_zero(self):
741
 
        self.assertEqual(milliseconds_to_string(0), "00:00:00")
742
 
    def test_no_fractional_seconds(self):
743
 
        self.assertEqual(milliseconds_to_string(400), "00:00:00")
744
 
        self.assertEqual(milliseconds_to_string(900), "00:00:00")
745
 
        self.assertEqual(milliseconds_to_string(1900), "00:00:01")
746
 
 
747
 
class Test_string_to_delta(unittest.TestCase):
 
842
class TestCaseWithAssertLogs(unittest.TestCase):
 
843
    """unittest.TestCase.assertLogs only exists in Python 3.4"""
 
844
 
 
845
    if not hasattr(unittest.TestCase, "assertLogs"):
 
846
        @contextlib.contextmanager
 
847
        def assertLogs(self, logger, level=logging.INFO):
 
848
            capturing_handler = self.CapturingLevelHandler(level)
 
849
            old_level = logger.level
 
850
            old_propagate = logger.propagate
 
851
            logger.addHandler(capturing_handler)
 
852
            logger.setLevel(level)
 
853
            logger.propagate = False
 
854
            try:
 
855
                yield capturing_handler.watcher
 
856
            finally:
 
857
                logger.propagate = old_propagate
 
858
                logger.removeHandler(capturing_handler)
 
859
                logger.setLevel(old_level)
 
860
            self.assertGreater(len(capturing_handler.watcher.records),
 
861
                               0)
 
862
 
 
863
        class CapturingLevelHandler(logging.Handler):
 
864
            def __init__(self, level, *args, **kwargs):
 
865
                logging.Handler.__init__(self, *args, **kwargs)
 
866
                self.watcher = self.LoggingWatcher([], [])
 
867
            def emit(self, record):
 
868
                self.watcher.records.append(record)
 
869
                self.watcher.output.append(self.format(record))
 
870
 
 
871
            LoggingWatcher = collections.namedtuple("LoggingWatcher",
 
872
                                                    ("records",
 
873
                                                     "output"))
 
874
 
 
875
 
 
876
class Test_string_to_delta(TestCaseWithAssertLogs):
748
877
    def test_handles_basic_rfc3339(self):
749
878
        self.assertEqual(string_to_delta("PT0S"),
750
879
                         datetime.timedelta())
754
883
                         datetime.timedelta(0, 1))
755
884
        self.assertEqual(string_to_delta("PT2H"),
756
885
                         datetime.timedelta(0, 7200))
 
886
 
757
887
    def test_falls_back_to_pre_1_6_1_with_warning(self):
758
 
        # assertLogs only exists in Python 3.4
759
 
        if hasattr(self, "assertLogs"):
760
 
            with self.assertLogs(log, logging.WARNING):
761
 
                value = string_to_delta("2h")
762
 
        else:
763
 
            class WarningFilter(logging.Filter):
764
 
                """Don't show, but record the presence of, warnings"""
765
 
                def filter(self, record):
766
 
                    is_warning = record.levelno >= logging.WARNING
767
 
                    self.found = is_warning or getattr(self, "found",
768
 
                                                       False)
769
 
                    return not is_warning
770
 
            warning_filter = WarningFilter()
771
 
            log.addFilter(warning_filter)
772
 
            try:
773
 
                value = string_to_delta("2h")
774
 
            finally:
775
 
                log.removeFilter(warning_filter)
776
 
            self.assertTrue(getattr(warning_filter, "found", False))
 
888
        with self.assertLogs(log, logging.WARNING):
 
889
            value = string_to_delta("2h")
777
890
        self.assertEqual(value, datetime.timedelta(0, 7200))
778
891
 
779
892
 
 
893
class Test_check_option_syntax(unittest.TestCase):
 
894
    def setUp(self):
 
895
        self.parser = argparse.ArgumentParser()
 
896
        add_command_line_options(self.parser)
 
897
 
 
898
    def test_actions_requires_client_or_all(self):
 
899
        for action, value in self.actions.items():
 
900
            options = self.parser.parse_args()
 
901
            setattr(options, action, value)
 
902
            with self.assertParseError():
 
903
                self.check_option_syntax(options)
 
904
 
 
905
    # This mostly corresponds to the definition from has_actions() in
 
906
    # check_option_syntax()
 
907
    actions = {
 
908
        # The actual values set here are not that important, but we do
 
909
        # at least stick to the correct types, even though they are
 
910
        # never used
 
911
        "enable": True,
 
912
        "disable": True,
 
913
        "bump_timeout": True,
 
914
        "start_checker": True,
 
915
        "stop_checker": True,
 
916
        "is_enabled": True,
 
917
        "remove": True,
 
918
        "checker": "x",
 
919
        "timeout": datetime.timedelta(),
 
920
        "extended_timeout": datetime.timedelta(),
 
921
        "interval": datetime.timedelta(),
 
922
        "approved_by_default": True,
 
923
        "approval_delay": datetime.timedelta(),
 
924
        "approval_duration": datetime.timedelta(),
 
925
        "host": "x",
 
926
        "secret": io.BytesIO(b"x"),
 
927
        "approve": True,
 
928
        "deny": True,
 
929
    }
 
930
 
 
931
    @contextlib.contextmanager
 
932
    def assertParseError(self):
 
933
        with self.assertRaises(SystemExit) as e:
 
934
            with self.redirect_stderr_to_devnull():
 
935
                yield
 
936
        # Exit code from argparse is guaranteed to be "2".  Reference:
 
937
        # https://docs.python.org/3/library
 
938
        # /argparse.html#exiting-methods
 
939
        self.assertEqual(e.exception.code, 2)
 
940
 
 
941
    @staticmethod
 
942
    @contextlib.contextmanager
 
943
    def redirect_stderr_to_devnull():
 
944
        null = os.open(os.path.devnull, os.O_RDWR)
 
945
        stderrcopy = os.dup(sys.stderr.fileno())
 
946
        os.dup2(null, sys.stderr.fileno())
 
947
        os.close(null)
 
948
        try:
 
949
            yield
 
950
        finally:
 
951
            # restore stderr
 
952
            os.dup2(stderrcopy, sys.stderr.fileno())
 
953
            os.close(stderrcopy)
 
954
 
 
955
    def check_option_syntax(self, options):
 
956
        check_option_syntax(self.parser, options)
 
957
 
 
958
    def test_actions_all_conflicts_with_verbose(self):
 
959
        for action, value in self.actions.items():
 
960
            options = self.parser.parse_args()
 
961
            setattr(options, action, value)
 
962
            options.all = True
 
963
            options.verbose = True
 
964
            with self.assertParseError():
 
965
                self.check_option_syntax(options)
 
966
 
 
967
    def test_actions_with_client_conflicts_with_verbose(self):
 
968
        for action, value in self.actions.items():
 
969
            options = self.parser.parse_args()
 
970
            setattr(options, action, value)
 
971
            options.verbose = True
 
972
            options.client = ["foo"]
 
973
            with self.assertParseError():
 
974
                self.check_option_syntax(options)
 
975
 
 
976
    def test_dump_json_conflicts_with_verbose(self):
 
977
        options = self.parser.parse_args()
 
978
        options.dump_json = True
 
979
        options.verbose = True
 
980
        with self.assertParseError():
 
981
            self.check_option_syntax(options)
 
982
 
 
983
    def test_dump_json_conflicts_with_action(self):
 
984
        for action, value in self.actions.items():
 
985
            options = self.parser.parse_args()
 
986
            setattr(options, action, value)
 
987
            options.dump_json = True
 
988
            with self.assertParseError():
 
989
                self.check_option_syntax(options)
 
990
 
 
991
    def test_all_can_not_be_alone(self):
 
992
        options = self.parser.parse_args()
 
993
        options.all = True
 
994
        with self.assertParseError():
 
995
            self.check_option_syntax(options)
 
996
 
 
997
    def test_all_is_ok_with_any_action(self):
 
998
        for action, value in self.actions.items():
 
999
            options = self.parser.parse_args()
 
1000
            setattr(options, action, value)
 
1001
            options.all = True
 
1002
            self.check_option_syntax(options)
 
1003
 
 
1004
    def test_any_action_is_ok_with_one_client(self):
 
1005
        for action, value in self.actions.items():
 
1006
            options = self.parser.parse_args()
 
1007
            setattr(options, action, value)
 
1008
            options.client = ["foo"]
 
1009
            self.check_option_syntax(options)
 
1010
 
 
1011
    def test_one_client_with_all_actions_except_is_enabled(self):
 
1012
        options = self.parser.parse_args()
 
1013
        for action, value in self.actions.items():
 
1014
            if action == "is_enabled":
 
1015
                continue
 
1016
            setattr(options, action, value)
 
1017
        options.client = ["foo"]
 
1018
        self.check_option_syntax(options)
 
1019
 
 
1020
    def test_two_clients_with_all_actions_except_is_enabled(self):
 
1021
        options = self.parser.parse_args()
 
1022
        for action, value in self.actions.items():
 
1023
            if action == "is_enabled":
 
1024
                continue
 
1025
            setattr(options, action, value)
 
1026
        options.client = ["foo", "barbar"]
 
1027
        self.check_option_syntax(options)
 
1028
 
 
1029
    def test_two_clients_are_ok_with_actions_except_is_enabled(self):
 
1030
        for action, value in self.actions.items():
 
1031
            if action == "is_enabled":
 
1032
                continue
 
1033
            options = self.parser.parse_args()
 
1034
            setattr(options, action, value)
 
1035
            options.client = ["foo", "barbar"]
 
1036
            self.check_option_syntax(options)
 
1037
 
 
1038
    def test_is_enabled_fails_without_client(self):
 
1039
        options = self.parser.parse_args()
 
1040
        options.is_enabled = True
 
1041
        with self.assertParseError():
 
1042
            self.check_option_syntax(options)
 
1043
 
 
1044
    def test_is_enabled_fails_with_two_clients(self):
 
1045
        options = self.parser.parse_args()
 
1046
        options.is_enabled = True
 
1047
        options.client = ["foo", "barbar"]
 
1048
        with self.assertParseError():
 
1049
            self.check_option_syntax(options)
 
1050
 
 
1051
    def test_remove_can_only_be_combined_with_action_deny(self):
 
1052
        for action, value in self.actions.items():
 
1053
            if action in {"remove", "deny"}:
 
1054
                continue
 
1055
            options = self.parser.parse_args()
 
1056
            setattr(options, action, value)
 
1057
            options.all = True
 
1058
            options.remove = True
 
1059
            with self.assertParseError():
 
1060
                self.check_option_syntax(options)
 
1061
 
 
1062
 
 
1063
class Test_get_mandos_dbus_object(TestCaseWithAssertLogs):
 
1064
    def test_calls_and_returns_get_object_on_bus(self):
 
1065
        class MockBus(object):
 
1066
            called = False
 
1067
            def get_object(mockbus_self, busname, dbus_path):
 
1068
                # Note that "self" is still the testcase instance,
 
1069
                # this MockBus instance is in "mockbus_self".
 
1070
                self.assertEqual(busname, dbus_busname)
 
1071
                self.assertEqual(dbus_path, server_dbus_path)
 
1072
                mockbus_self.called = True
 
1073
                return mockbus_self
 
1074
 
 
1075
        mockbus = get_mandos_dbus_object(bus=MockBus())
 
1076
        self.assertIsInstance(mockbus, MockBus)
 
1077
        self.assertTrue(mockbus.called)
 
1078
 
 
1079
    def test_logs_and_exits_on_dbus_error(self):
 
1080
        class MockBusFailing(object):
 
1081
            def get_object(self, busname, dbus_path):
 
1082
                raise dbus.exceptions.DBusException("Test")
 
1083
 
 
1084
        with self.assertLogs(log, logging.CRITICAL):
 
1085
            with self.assertRaises(SystemExit) as e:
 
1086
                bus = get_mandos_dbus_object(bus=MockBusFailing())
 
1087
 
 
1088
        if isinstance(e.exception.code, int):
 
1089
            self.assertNotEqual(e.exception.code, 0)
 
1090
        else:
 
1091
            self.assertIsNotNone(e.exception.code)
 
1092
 
 
1093
 
 
1094
class Test_get_managed_objects(TestCaseWithAssertLogs):
 
1095
    def test_calls_and_returns_GetManagedObjects(self):
 
1096
        managed_objects = {"/clients/foo": { "Name": "foo"}}
 
1097
        class MockObjectManager(object):
 
1098
            def GetManagedObjects(self):
 
1099
                return managed_objects
 
1100
        retval = get_managed_objects(MockObjectManager())
 
1101
        self.assertDictEqual(managed_objects, retval)
 
1102
 
 
1103
    def test_logs_and_exits_on_dbus_error(self):
 
1104
        dbus_logger = logging.getLogger("dbus.proxies")
 
1105
 
 
1106
        class MockObjectManagerFailing(object):
 
1107
            def GetManagedObjects(self):
 
1108
                dbus_logger.error("Test")
 
1109
                raise dbus.exceptions.DBusException("Test")
 
1110
 
 
1111
        class CountingHandler(logging.Handler):
 
1112
            count = 0
 
1113
            def emit(self, record):
 
1114
                self.count += 1
 
1115
 
 
1116
        counting_handler = CountingHandler()
 
1117
 
 
1118
        dbus_logger.addHandler(counting_handler)
 
1119
 
 
1120
        try:
 
1121
            with self.assertLogs(log, logging.CRITICAL) as watcher:
 
1122
                with self.assertRaises(SystemExit) as e:
 
1123
                    get_managed_objects(MockObjectManagerFailing())
 
1124
        finally:
 
1125
            dbus_logger.removeFilter(counting_handler)
 
1126
 
 
1127
        # Make sure the dbus logger was suppressed
 
1128
        self.assertEqual(counting_handler.count, 0)
 
1129
 
 
1130
        # Test that the dbus_logger still works
 
1131
        with self.assertLogs(dbus_logger, logging.ERROR):
 
1132
            dbus_logger.error("Test")
 
1133
 
 
1134
        if isinstance(e.exception.code, int):
 
1135
            self.assertNotEqual(e.exception.code, 0)
 
1136
        else:
 
1137
            self.assertIsNotNone(e.exception.code)
 
1138
 
 
1139
 
 
1140
class Test_commands_from_options(unittest.TestCase):
 
1141
    def setUp(self):
 
1142
        self.parser = argparse.ArgumentParser()
 
1143
        add_command_line_options(self.parser)
 
1144
 
 
1145
    def test_is_enabled(self):
 
1146
        self.assert_command_from_args(["--is-enabled", "foo"],
 
1147
                                      IsEnabledCmd)
 
1148
 
 
1149
    def assert_command_from_args(self, args, command_cls,
 
1150
                                 **cmd_attrs):
 
1151
        """Assert that parsing ARGS should result in an instance of
 
1152
COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
 
1153
        options = self.parser.parse_args(args)
 
1154
        check_option_syntax(self.parser, options)
 
1155
        commands = commands_from_options(options)
 
1156
        self.assertEqual(len(commands), 1)
 
1157
        command = commands[0]
 
1158
        self.assertIsInstance(command, command_cls)
 
1159
        for key, value in cmd_attrs.items():
 
1160
            self.assertEqual(getattr(command, key), value)
 
1161
 
 
1162
    def test_is_enabled_short(self):
 
1163
        self.assert_command_from_args(["-V", "foo"], IsEnabledCmd)
 
1164
 
 
1165
    def test_approve(self):
 
1166
        self.assert_command_from_args(["--approve", "foo"],
 
1167
                                      ApproveCmd)
 
1168
 
 
1169
    def test_approve_short(self):
 
1170
        self.assert_command_from_args(["-A", "foo"], ApproveCmd)
 
1171
 
 
1172
    def test_deny(self):
 
1173
        self.assert_command_from_args(["--deny", "foo"], DenyCmd)
 
1174
 
 
1175
    def test_deny_short(self):
 
1176
        self.assert_command_from_args(["-D", "foo"], DenyCmd)
 
1177
 
 
1178
    def test_remove(self):
 
1179
        self.assert_command_from_args(["--remove", "foo"],
 
1180
                                      RemoveCmd)
 
1181
 
 
1182
    def test_deny_before_remove(self):
 
1183
        options = self.parser.parse_args(["--deny", "--remove",
 
1184
                                          "foo"])
 
1185
        check_option_syntax(self.parser, options)
 
1186
        commands = commands_from_options(options)
 
1187
        self.assertEqual(len(commands), 2)
 
1188
        self.assertIsInstance(commands[0], DenyCmd)
 
1189
        self.assertIsInstance(commands[1], RemoveCmd)
 
1190
 
 
1191
    def test_deny_before_remove_reversed(self):
 
1192
        options = self.parser.parse_args(["--remove", "--deny",
 
1193
                                          "--all"])
 
1194
        check_option_syntax(self.parser, options)
 
1195
        commands = commands_from_options(options)
 
1196
        self.assertEqual(len(commands), 2)
 
1197
        self.assertIsInstance(commands[0], DenyCmd)
 
1198
        self.assertIsInstance(commands[1], RemoveCmd)
 
1199
 
 
1200
    def test_remove_short(self):
 
1201
        self.assert_command_from_args(["-r", "foo"], RemoveCmd)
 
1202
 
 
1203
    def test_dump_json(self):
 
1204
        self.assert_command_from_args(["--dump-json"], DumpJSONCmd)
 
1205
 
 
1206
    def test_enable(self):
 
1207
        self.assert_command_from_args(["--enable", "foo"], EnableCmd)
 
1208
 
 
1209
    def test_enable_short(self):
 
1210
        self.assert_command_from_args(["-e", "foo"], EnableCmd)
 
1211
 
 
1212
    def test_disable(self):
 
1213
        self.assert_command_from_args(["--disable", "foo"],
 
1214
                                      DisableCmd)
 
1215
 
 
1216
    def test_disable_short(self):
 
1217
        self.assert_command_from_args(["-d", "foo"], DisableCmd)
 
1218
 
 
1219
    def test_bump_timeout(self):
 
1220
        self.assert_command_from_args(["--bump-timeout", "foo"],
 
1221
                                      BumpTimeoutCmd)
 
1222
 
 
1223
    def test_bump_timeout_short(self):
 
1224
        self.assert_command_from_args(["-b", "foo"], BumpTimeoutCmd)
 
1225
 
 
1226
    def test_start_checker(self):
 
1227
        self.assert_command_from_args(["--start-checker", "foo"],
 
1228
                                      StartCheckerCmd)
 
1229
 
 
1230
    def test_stop_checker(self):
 
1231
        self.assert_command_from_args(["--stop-checker", "foo"],
 
1232
                                      StopCheckerCmd)
 
1233
 
 
1234
    def test_approve_by_default(self):
 
1235
        self.assert_command_from_args(["--approve-by-default", "foo"],
 
1236
                                      ApproveByDefaultCmd)
 
1237
 
 
1238
    def test_deny_by_default(self):
 
1239
        self.assert_command_from_args(["--deny-by-default", "foo"],
 
1240
                                      DenyByDefaultCmd)
 
1241
 
 
1242
    def test_checker(self):
 
1243
        self.assert_command_from_args(["--checker", ":", "foo"],
 
1244
                                      SetCheckerCmd, value_to_set=":")
 
1245
 
 
1246
    def test_checker_empty(self):
 
1247
        self.assert_command_from_args(["--checker", "", "foo"],
 
1248
                                      SetCheckerCmd, value_to_set="")
 
1249
 
 
1250
    def test_checker_short(self):
 
1251
        self.assert_command_from_args(["-c", ":", "foo"],
 
1252
                                      SetCheckerCmd, value_to_set=":")
 
1253
 
 
1254
    def test_host(self):
 
1255
        self.assert_command_from_args(["--host", "foo.example.org",
 
1256
                                       "foo"], SetHostCmd,
 
1257
                                      value_to_set="foo.example.org")
 
1258
 
 
1259
    def test_host_short(self):
 
1260
        self.assert_command_from_args(["-H", "foo.example.org",
 
1261
                                       "foo"], SetHostCmd,
 
1262
                                      value_to_set="foo.example.org")
 
1263
 
 
1264
    def test_secret_devnull(self):
 
1265
        self.assert_command_from_args(["--secret", os.path.devnull,
 
1266
                                       "foo"], SetSecretCmd,
 
1267
                                      value_to_set=b"")
 
1268
 
 
1269
    def test_secret_tempfile(self):
 
1270
        with tempfile.NamedTemporaryFile(mode="r+b") as f:
 
1271
            value = b"secret\0xyzzy\nbar"
 
1272
            f.write(value)
 
1273
            f.seek(0)
 
1274
            self.assert_command_from_args(["--secret", f.name,
 
1275
                                           "foo"], SetSecretCmd,
 
1276
                                          value_to_set=value)
 
1277
 
 
1278
    def test_secret_devnull_short(self):
 
1279
        self.assert_command_from_args(["-s", os.path.devnull, "foo"],
 
1280
                                      SetSecretCmd, value_to_set=b"")
 
1281
 
 
1282
    def test_secret_tempfile_short(self):
 
1283
        with tempfile.NamedTemporaryFile(mode="r+b") as f:
 
1284
            value = b"secret\0xyzzy\nbar"
 
1285
            f.write(value)
 
1286
            f.seek(0)
 
1287
            self.assert_command_from_args(["-s", f.name, "foo"],
 
1288
                                          SetSecretCmd,
 
1289
                                          value_to_set=value)
 
1290
 
 
1291
    def test_timeout(self):
 
1292
        self.assert_command_from_args(["--timeout", "PT5M", "foo"],
 
1293
                                      SetTimeoutCmd,
 
1294
                                      value_to_set=300000)
 
1295
 
 
1296
    def test_timeout_short(self):
 
1297
        self.assert_command_from_args(["-t", "PT5M", "foo"],
 
1298
                                      SetTimeoutCmd,
 
1299
                                      value_to_set=300000)
 
1300
 
 
1301
    def test_extended_timeout(self):
 
1302
        self.assert_command_from_args(["--extended-timeout", "PT15M",
 
1303
                                       "foo"],
 
1304
                                      SetExtendedTimeoutCmd,
 
1305
                                      value_to_set=900000)
 
1306
 
 
1307
    def test_interval(self):
 
1308
        self.assert_command_from_args(["--interval", "PT2M", "foo"],
 
1309
                                      SetIntervalCmd,
 
1310
                                      value_to_set=120000)
 
1311
 
 
1312
    def test_interval_short(self):
 
1313
        self.assert_command_from_args(["-i", "PT2M", "foo"],
 
1314
                                      SetIntervalCmd,
 
1315
                                      value_to_set=120000)
 
1316
 
 
1317
    def test_approval_delay(self):
 
1318
        self.assert_command_from_args(["--approval-delay", "PT30S",
 
1319
                                       "foo"], SetApprovalDelayCmd,
 
1320
                                      value_to_set=30000)
 
1321
 
 
1322
    def test_approval_duration(self):
 
1323
        self.assert_command_from_args(["--approval-duration", "PT1S",
 
1324
                                       "foo"], SetApprovalDurationCmd,
 
1325
                                      value_to_set=1000)
 
1326
 
 
1327
    def test_print_table(self):
 
1328
        self.assert_command_from_args([], PrintTableCmd,
 
1329
                                      verbose=False)
 
1330
 
 
1331
    def test_print_table_verbose(self):
 
1332
        self.assert_command_from_args(["--verbose"], PrintTableCmd,
 
1333
                                      verbose=True)
 
1334
 
 
1335
    def test_print_table_verbose_short(self):
 
1336
        self.assert_command_from_args(["-v"], PrintTableCmd,
 
1337
                                      verbose=True)
 
1338
 
 
1339
 
780
1340
class TestCmd(unittest.TestCase):
781
1341
    """Abstract class for tests of command classes"""
 
1342
 
782
1343
    def setUp(self):
783
1344
        testcase = self
784
1345
        class MockClient(object):
785
1346
            def __init__(self, name, **attributes):
786
 
                self.__dbus_object_path__ = "objpath_{}".format(name)
 
1347
                self.__dbus_object_path__ = "/clients/{}".format(name)
787
1348
                self.attributes = attributes
788
1349
                self.attributes["Name"] = name
789
1350
                self.calls = []
790
 
            def Set(self, interface, property, value, dbus_interface):
791
 
                testcase.assertEqual(interface, client_interface)
792
 
                testcase.assertEqual(dbus_interface,
793
 
                                     dbus.PROPERTIES_IFACE)
794
 
                self.attributes[property] = value
795
 
            def Get(self, interface, property, dbus_interface):
796
 
                testcase.assertEqual(interface, client_interface)
797
 
                testcase.assertEqual(dbus_interface,
798
 
                                     dbus.PROPERTIES_IFACE)
799
 
                return self.attributes[property]
 
1351
            def Set(self, interface, propname, value, dbus_interface):
 
1352
                testcase.assertEqual(interface, client_dbus_interface)
 
1353
                testcase.assertEqual(dbus_interface,
 
1354
                                     dbus.PROPERTIES_IFACE)
 
1355
                self.attributes[propname] = value
 
1356
            def Get(self, interface, propname, dbus_interface):
 
1357
                testcase.assertEqual(interface, client_dbus_interface)
 
1358
                testcase.assertEqual(dbus_interface,
 
1359
                                     dbus.PROPERTIES_IFACE)
 
1360
                return self.attributes[propname]
800
1361
            def Approve(self, approve, dbus_interface):
801
 
                testcase.assertEqual(dbus_interface, client_interface)
 
1362
                testcase.assertEqual(dbus_interface,
 
1363
                                     client_dbus_interface)
802
1364
                self.calls.append(("Approve", (approve,
803
1365
                                               dbus_interface)))
804
1366
        self.client = MockClient(
844
1406
            ApprovedByDefault=dbus.Boolean(False),
845
1407
            LastApprovalRequest="2019-01-03T00:00:00",
846
1408
            ApprovalDelay=30000,
847
 
            ApprovalDuration=1000,
 
1409
            ApprovalDuration=93785000,
848
1410
            Checker=":",
849
1411
            ExtendedTimeout=900000,
850
1412
            Expires="2019-02-05T00:00:00",
851
1413
            LastCheckerStatus=-2)
852
1414
        self.clients =  collections.OrderedDict(
853
1415
            [
854
 
                (self.client, self.client.attributes),
855
 
                (self.other_client, self.other_client.attributes),
 
1416
                ("/clients/foo", self.client.attributes),
 
1417
                ("/clients/barbar", self.other_client.attributes),
856
1418
            ])
857
 
        self.one_client = {self.client: self.client.attributes}
858
 
 
859
 
class TestPrintTableCmd(TestCmd):
860
 
    def test_normal(self):
861
 
        output = PrintTableCmd().output(self.clients)
862
 
        expected_output = """
863
 
Name   Enabled Timeout  Last Successful Check
864
 
foo    Yes     00:05:00 2019-02-03T00:00:00  
865
 
barbar Yes     00:05:00 2019-02-04T00:00:00  
866
 
"""[1:-1]
867
 
        self.assertEqual(output, expected_output)
868
 
    def test_verbose(self):
869
 
        output = PrintTableCmd(verbose=True).output(self.clients)
870
 
        expected_output = """
871
 
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
872
 
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                  
873
 
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                 
874
 
"""[1:-1]
875
 
        self.assertEqual(output, expected_output)
876
 
    def test_one_client(self):
877
 
        output = PrintTableCmd().output(self.one_client)
878
 
        expected_output = """
879
 
Name Enabled Timeout  Last Successful Check
880
 
foo  Yes     00:05:00 2019-02-03T00:00:00  
881
 
"""[1:-1]
882
 
        self.assertEqual(output, expected_output)
 
1419
        self.one_client = {"/clients/foo": self.client.attributes}
 
1420
 
 
1421
    @property
 
1422
    def bus(self):
 
1423
        class Bus(object):
 
1424
            @staticmethod
 
1425
            def get_object(client_bus_name, path):
 
1426
                self.assertEqual(client_bus_name, dbus_busname)
 
1427
                return {
 
1428
                    # Note: "self" here is the TestCmd instance, not
 
1429
                    # the Bus instance, since this is a static method!
 
1430
                    "/clients/foo": self.client,
 
1431
                    "/clients/barbar": self.other_client,
 
1432
                }[path]
 
1433
        return Bus()
 
1434
 
 
1435
 
 
1436
class TestIsEnabledCmd(TestCmd):
 
1437
    def test_is_enabled(self):
 
1438
        self.assertTrue(all(IsEnabledCmd().is_enabled(client,
 
1439
                                                      properties)
 
1440
                            for client, properties
 
1441
                            in self.clients.items()))
 
1442
 
 
1443
    def test_is_enabled_run_exits_successfully(self):
 
1444
        with self.assertRaises(SystemExit) as e:
 
1445
            IsEnabledCmd().run(self.one_client)
 
1446
        if e.exception.code is not None:
 
1447
            self.assertEqual(e.exception.code, 0)
 
1448
        else:
 
1449
            self.assertIsNone(e.exception.code)
 
1450
 
 
1451
    def test_is_enabled_run_exits_with_failure(self):
 
1452
        self.client.attributes["Enabled"] = dbus.Boolean(False)
 
1453
        with self.assertRaises(SystemExit) as e:
 
1454
            IsEnabledCmd().run(self.one_client)
 
1455
        if isinstance(e.exception.code, int):
 
1456
            self.assertNotEqual(e.exception.code, 0)
 
1457
        else:
 
1458
            self.assertIsNotNone(e.exception.code)
 
1459
 
 
1460
 
 
1461
class TestApproveCmd(TestCmd):
 
1462
    def test_approve(self):
 
1463
        ApproveCmd().run(self.clients, self.bus)
 
1464
        for clientpath in self.clients:
 
1465
            client = self.bus.get_object(dbus_busname, clientpath)
 
1466
            self.assertIn(("Approve", (True, client_dbus_interface)),
 
1467
                          client.calls)
 
1468
 
 
1469
 
 
1470
class TestDenyCmd(TestCmd):
 
1471
    def test_deny(self):
 
1472
        DenyCmd().run(self.clients, self.bus)
 
1473
        for clientpath in self.clients:
 
1474
            client = self.bus.get_object(dbus_busname, clientpath)
 
1475
            self.assertIn(("Approve", (False, client_dbus_interface)),
 
1476
                          client.calls)
 
1477
 
 
1478
 
 
1479
class TestRemoveCmd(TestCmd):
 
1480
    def test_remove(self):
 
1481
        class MockMandos(object):
 
1482
            def __init__(self):
 
1483
                self.calls = []
 
1484
            def RemoveClient(self, dbus_path):
 
1485
                self.calls.append(("RemoveClient", (dbus_path,)))
 
1486
        mandos = MockMandos()
 
1487
        super(TestRemoveCmd, self).setUp()
 
1488
        RemoveCmd().run(self.clients, self.bus, mandos)
 
1489
        self.assertEqual(len(mandos.calls), 2)
 
1490
        for clientpath in self.clients:
 
1491
            self.assertIn(("RemoveClient", (clientpath,)),
 
1492
                          mandos.calls)
 
1493
 
883
1494
 
884
1495
class TestDumpJSONCmd(TestCmd):
885
1496
    def setUp(self):
926
1537
                "ApprovedByDefault": False,
927
1538
                "LastApprovalRequest": "2019-01-03T00:00:00",
928
1539
                "ApprovalDelay": 30000,
929
 
                "ApprovalDuration": 1000,
 
1540
                "ApprovalDuration": 93785000,
930
1541
                "Checker": ":",
931
1542
                "ExtendedTimeout": 900000,
932
1543
                "Expires": "2019-02-05T00:00:00",
934
1545
            },
935
1546
        }
936
1547
        return super(TestDumpJSONCmd, self).setUp()
 
1548
 
937
1549
    def test_normal(self):
938
 
        json_data = json.loads(DumpJSONCmd().output(self.clients))
 
1550
        output = DumpJSONCmd().output(self.clients.values())
 
1551
        json_data = json.loads(output)
939
1552
        self.assertDictEqual(json_data, self.expected_json)
 
1553
 
940
1554
    def test_one_client(self):
941
 
        clients = self.one_client
942
 
        json_data = json.loads(DumpJSONCmd().output(clients))
 
1555
        output = DumpJSONCmd().output(self.one_client.values())
 
1556
        json_data = json.loads(output)
943
1557
        expected_json = {"foo": self.expected_json["foo"]}
944
1558
        self.assertDictEqual(json_data, expected_json)
945
1559
 
946
 
class TestIsEnabledCmd(TestCmd):
947
 
    def test_is_enabled(self):
948
 
        self.assertTrue(all(IsEnabledCmd().is_enabled(client, properties)
949
 
                            for client, properties in self.clients.items()))
950
 
    def test_is_enabled_run_exits_successfully(self):
951
 
        with self.assertRaises(SystemExit) as e:
952
 
            IsEnabledCmd().run(None, self.one_client)
953
 
        if e.exception.code is not None:
954
 
            self.assertEqual(e.exception.code, 0)
955
 
        else:
956
 
            self.assertIsNone(e.exception.code)
957
 
    def test_is_enabled_run_exits_with_failure(self):
958
 
        self.client.attributes["Enabled"] = dbus.Boolean(False)
959
 
        with self.assertRaises(SystemExit) as e:
960
 
            IsEnabledCmd().run(None, self.one_client)
961
 
        if isinstance(e.exception.code, int):
962
 
            self.assertNotEqual(e.exception.code, 0)
963
 
        else:
964
 
            self.assertIsNotNone(e.exception.code)
965
 
 
966
 
class TestRemoveCmd(TestCmd):
967
 
    def test_remove(self):
968
 
        class MockMandos(object):
969
 
            def __init__(self):
970
 
                self.calls = []
971
 
            def RemoveClient(self, dbus_path):
972
 
                self.calls.append(("RemoveClient", (dbus_path,)))
973
 
        mandos = MockMandos()
974
 
        super(TestRemoveCmd, self).setUp()
975
 
        RemoveCmd().run(mandos, self.clients)
976
 
        self.assertEqual(len(mandos.calls), 2)
977
 
        for client in self.clients:
978
 
            self.assertIn(("RemoveClient",
979
 
                           (client.__dbus_object_path__,)),
980
 
                          mandos.calls)
981
 
 
982
 
class TestApproveCmd(TestCmd):
983
 
    def test_approve(self):
984
 
        ApproveCmd().run(None, self.clients)
985
 
        for client in self.clients:
986
 
            self.assertIn(("Approve", (True, client_interface)),
987
 
                          client.calls)
988
 
 
989
 
class TestDenyCmd(TestCmd):
990
 
    def test_deny(self):
991
 
        DenyCmd().run(None, self.clients)
992
 
        for client in self.clients:
993
 
            self.assertIn(("Approve", (False, client_interface)),
994
 
                          client.calls)
995
 
 
996
 
class TestEnableCmd(TestCmd):
997
 
    def test_enable(self):
998
 
        for client in self.clients:
999
 
            client.attributes["Enabled"] = False
1000
 
 
1001
 
        EnableCmd().run(None, self.clients)
1002
 
 
1003
 
        for client in self.clients:
1004
 
            self.assertTrue(client.attributes["Enabled"])
1005
 
 
1006
 
class TestDisableCmd(TestCmd):
1007
 
    def test_disable(self):
1008
 
        DisableCmd().run(None, self.clients)
1009
 
 
1010
 
        for client in self.clients:
1011
 
            self.assertFalse(client.attributes["Enabled"])
1012
 
 
1013
 
class Unique(object):
1014
 
    """Class for objects which exist only to be unique objects, since
1015
 
unittest.mock.sentinel only exists in Python 3.3"""
 
1560
 
 
1561
class TestPrintTableCmd(TestCmd):
 
1562
    def test_normal(self):
 
1563
        output = PrintTableCmd().output(self.clients.values())
 
1564
        expected_output = "\n".join((
 
1565
            "Name   Enabled Timeout  Last Successful Check",
 
1566
            "foo    Yes     00:05:00 2019-02-03T00:00:00  ",
 
1567
            "barbar Yes     00:05:00 2019-02-04T00:00:00  ",
 
1568
        ))
 
1569
        self.assertEqual(output, expected_output)
 
1570
 
 
1571
    def test_verbose(self):
 
1572
        output = PrintTableCmd(verbose=True).output(
 
1573
            self.clients.values())
 
1574
        columns = (
 
1575
            (
 
1576
                "Name   ",
 
1577
                "foo    ",
 
1578
                "barbar ",
 
1579
            ),(
 
1580
                "Enabled ",
 
1581
                "Yes     ",
 
1582
                "Yes     ",
 
1583
            ),(
 
1584
                "Timeout  ",
 
1585
                "00:05:00 ",
 
1586
                "00:05:00 ",
 
1587
            ),(
 
1588
                "Last Successful Check ",
 
1589
                "2019-02-03T00:00:00   ",
 
1590
                "2019-02-04T00:00:00   ",
 
1591
            ),(
 
1592
                "Created             ",
 
1593
                "2019-01-02T00:00:00 ",
 
1594
                "2019-01-03T00:00:00 ",
 
1595
            ),(
 
1596
                "Interval ",
 
1597
                "00:02:00 ",
 
1598
                "00:02:00 ",
 
1599
            ),(
 
1600
                "Host            ",
 
1601
                "foo.example.org ",
 
1602
                "192.0.2.3       ",
 
1603
            ),(
 
1604
                ("Key ID                                             "
 
1605
                 "              "),
 
1606
                ("92ed150794387c03ce684574b1139a6594a34f895daaaf09fd8"
 
1607
                 "ea90a27cddb12 "),
 
1608
                ("0558568eedd67d622f5c83b35a115f796ab612cff5ad227247e"
 
1609
                 "46c2b020f441c "),
 
1610
            ),(
 
1611
                "Fingerprint                              ",
 
1612
                "778827225BA7DE539C5A7CFA59CFF7CDBD9A5920 ",
 
1613
                "3E393AEAEFB84C7E89E2F547B3A107558FCA3A27 ",
 
1614
            ),(
 
1615
                "Check Is Running ",
 
1616
                "No               ",
 
1617
                "Yes              ",
 
1618
            ),(
 
1619
                "Last Enabled        ",
 
1620
                "2019-01-03T00:00:00 ",
 
1621
                "2019-01-04T00:00:00 ",
 
1622
            ),(
 
1623
                "Approval Is Pending ",
 
1624
                "No                  ",
 
1625
                "No                  ",
 
1626
            ),(
 
1627
                "Approved By Default ",
 
1628
                "Yes                 ",
 
1629
                "No                  ",
 
1630
            ),(
 
1631
                "Last Approval Request ",
 
1632
                "                      ",
 
1633
                "2019-01-03T00:00:00   ",
 
1634
            ),(
 
1635
                "Approval Delay ",
 
1636
                "00:00:00       ",
 
1637
                "00:00:30       ",
 
1638
            ),(
 
1639
                "Approval Duration ",
 
1640
                "00:00:01          ",
 
1641
                "1T02:03:05        ",
 
1642
            ),(
 
1643
                "Checker              ",
 
1644
                "fping -q -- %(host)s ",
 
1645
                ":                    ",
 
1646
            ),(
 
1647
                "Extended Timeout ",
 
1648
                "00:15:00         ",
 
1649
                "00:15:00         ",
 
1650
            ),(
 
1651
                "Expires             ",
 
1652
                "2019-02-04T00:00:00 ",
 
1653
                "2019-02-05T00:00:00 ",
 
1654
            ),(
 
1655
                "Last Checker Status",
 
1656
                "0                  ",
 
1657
                "-2                 ",
 
1658
            )
 
1659
        )
 
1660
        num_lines = max(len(rows) for rows in columns)
 
1661
        expected_output = "\n".join("".join(rows[line]
 
1662
                                            for rows in columns)
 
1663
                                    for line in range(num_lines))
 
1664
        self.assertEqual(output, expected_output)
 
1665
 
 
1666
    def test_one_client(self):
 
1667
        output = PrintTableCmd().output(self.one_client.values())
 
1668
        expected_output = "\n".join((
 
1669
            "Name Enabled Timeout  Last Successful Check",
 
1670
            "foo  Yes     00:05:00 2019-02-03T00:00:00  ",
 
1671
        ))
 
1672
        self.assertEqual(output, expected_output)
 
1673
 
1016
1674
 
1017
1675
class TestPropertyCmd(TestCmd):
1018
1676
    """Abstract class for tests of PropertyCmd classes"""
1023
1681
                                self.values_to_set)
1024
1682
        for value_to_set, value_to_get in zip(self.values_to_set,
1025
1683
                                              values_to_get):
1026
 
            for client in self.clients:
1027
 
                old_value = client.attributes[self.property]
1028
 
                self.assertNotIsInstance(old_value, Unique)
1029
 
                client.attributes[self.property] = Unique()
 
1684
            for clientpath in self.clients:
 
1685
                client = self.bus.get_object(dbus_busname, clientpath)
 
1686
                old_value = client.attributes[self.propname]
 
1687
                self.assertNotIsInstance(old_value, self.Unique)
 
1688
                client.attributes[self.propname] = self.Unique()
1030
1689
            self.run_command(value_to_set, self.clients)
1031
 
            for client in self.clients:
1032
 
                value = client.attributes[self.property]
1033
 
                self.assertNotIsInstance(value, Unique)
 
1690
            for clientpath in self.clients:
 
1691
                client = self.bus.get_object(dbus_busname, clientpath)
 
1692
                value = client.attributes[self.propname]
 
1693
                self.assertNotIsInstance(value, self.Unique)
1034
1694
                self.assertEqual(value, value_to_get)
 
1695
 
 
1696
    class Unique(object):
 
1697
        """Class for objects which exist only to be unique objects,
 
1698
since unittest.mock.sentinel only exists in Python 3.3"""
 
1699
 
1035
1700
    def run_command(self, value, clients):
1036
 
        self.command().run(None, clients)
 
1701
        self.command().run(clients, self.bus)
 
1702
 
 
1703
 
 
1704
class TestEnableCmd(TestPropertyCmd):
 
1705
    command = EnableCmd
 
1706
    propname = "Enabled"
 
1707
    values_to_set = [dbus.Boolean(True)]
 
1708
 
 
1709
 
 
1710
class TestDisableCmd(TestPropertyCmd):
 
1711
    command = DisableCmd
 
1712
    propname = "Enabled"
 
1713
    values_to_set = [dbus.Boolean(False)]
 
1714
 
1037
1715
 
1038
1716
class TestBumpTimeoutCmd(TestPropertyCmd):
1039
1717
    command = BumpTimeoutCmd
1040
 
    property = "LastCheckedOK"
 
1718
    propname = "LastCheckedOK"
1041
1719
    values_to_set = [""]
1042
1720
 
 
1721
 
1043
1722
class TestStartCheckerCmd(TestPropertyCmd):
1044
1723
    command = StartCheckerCmd
1045
 
    property = "CheckerRunning"
 
1724
    propname = "CheckerRunning"
1046
1725
    values_to_set = [dbus.Boolean(True)]
1047
1726
 
 
1727
 
1048
1728
class TestStopCheckerCmd(TestPropertyCmd):
1049
1729
    command = StopCheckerCmd
1050
 
    property = "CheckerRunning"
 
1730
    propname = "CheckerRunning"
1051
1731
    values_to_set = [dbus.Boolean(False)]
1052
1732
 
 
1733
 
1053
1734
class TestApproveByDefaultCmd(TestPropertyCmd):
1054
1735
    command = ApproveByDefaultCmd
1055
 
    property = "ApprovedByDefault"
 
1736
    propname = "ApprovedByDefault"
1056
1737
    values_to_set = [dbus.Boolean(True)]
1057
1738
 
 
1739
 
1058
1740
class TestDenyByDefaultCmd(TestPropertyCmd):
1059
1741
    command = DenyByDefaultCmd
1060
 
    property = "ApprovedByDefault"
 
1742
    propname = "ApprovedByDefault"
1061
1743
    values_to_set = [dbus.Boolean(False)]
1062
1744
 
1063
 
class TestValueArgumentPropertyCmd(TestPropertyCmd):
1064
 
    """Abstract class for tests of PropertyCmd classes using the
1065
 
ValueArgumentMixIn"""
 
1745
 
 
1746
class TestPropertyValueCmd(TestPropertyCmd):
 
1747
    """Abstract class for tests of PropertyValueCmd classes"""
 
1748
 
1066
1749
    def runTest(self):
1067
 
        if type(self) is TestValueArgumentPropertyCmd:
 
1750
        if type(self) is TestPropertyValueCmd:
1068
1751
            return
1069
 
        return super(TestValueArgumentPropertyCmd, self).runTest()
 
1752
        return super(TestPropertyValueCmd, self).runTest()
 
1753
 
1070
1754
    def run_command(self, value, clients):
1071
 
        self.command(value).run(None, clients)
1072
 
 
1073
 
class TestSetCheckerCmd(TestValueArgumentPropertyCmd):
 
1755
        self.command(value).run(clients, self.bus)
 
1756
 
 
1757
 
 
1758
class TestSetCheckerCmd(TestPropertyValueCmd):
1074
1759
    command = SetCheckerCmd
1075
 
    property = "Checker"
 
1760
    propname = "Checker"
1076
1761
    values_to_set = ["", ":", "fping -q -- %s"]
1077
1762
 
1078
 
class TestSetHostCmd(TestValueArgumentPropertyCmd):
 
1763
 
 
1764
class TestSetHostCmd(TestPropertyValueCmd):
1079
1765
    command = SetHostCmd
1080
 
    property = "Host"
 
1766
    propname = "Host"
1081
1767
    values_to_set = ["192.0.2.3", "foo.example.org"]
1082
1768
 
1083
 
class TestSetSecretCmd(TestValueArgumentPropertyCmd):
 
1769
 
 
1770
class TestSetSecretCmd(TestPropertyValueCmd):
1084
1771
    command = SetSecretCmd
1085
 
    property = "Secret"
1086
 
    values_to_set = [b"", b"secret"]
1087
 
 
1088
 
class TestSetTimeoutCmd(TestValueArgumentPropertyCmd):
 
1772
    propname = "Secret"
 
1773
    values_to_set = [io.BytesIO(b""),
 
1774
                     io.BytesIO(b"secret\0xyzzy\nbar")]
 
1775
    values_to_get = [b"", b"secret\0xyzzy\nbar"]
 
1776
 
 
1777
 
 
1778
class TestSetTimeoutCmd(TestPropertyValueCmd):
1089
1779
    command = SetTimeoutCmd
1090
 
    property = "Timeout"
1091
 
    values_to_set = ["P0D", "PT5M", "PT1S", "PT120S", "P1Y"]
1092
 
    values_to_get = [0, 300000, 1000, 120000, 31449600000]
1093
 
 
1094
 
class TestSetExtendedTimeoutCmd(TestValueArgumentPropertyCmd):
 
1780
    propname = "Timeout"
 
1781
    values_to_set = [datetime.timedelta(),
 
1782
                     datetime.timedelta(minutes=5),
 
1783
                     datetime.timedelta(seconds=1),
 
1784
                     datetime.timedelta(weeks=1),
 
1785
                     datetime.timedelta(weeks=52)]
 
1786
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
 
1787
 
 
1788
 
 
1789
class TestSetExtendedTimeoutCmd(TestPropertyValueCmd):
1095
1790
    command = SetExtendedTimeoutCmd
1096
 
    property = "ExtendedTimeout"
1097
 
    values_to_set = ["P0D", "PT5M", "PT1S", "PT120S", "P1Y"]
1098
 
    values_to_get = [0, 300000, 1000, 120000, 31449600000]
1099
 
 
1100
 
class TestSetIntervalCmd(TestValueArgumentPropertyCmd):
 
1791
    propname = "ExtendedTimeout"
 
1792
    values_to_set = [datetime.timedelta(),
 
1793
                     datetime.timedelta(minutes=5),
 
1794
                     datetime.timedelta(seconds=1),
 
1795
                     datetime.timedelta(weeks=1),
 
1796
                     datetime.timedelta(weeks=52)]
 
1797
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
 
1798
 
 
1799
 
 
1800
class TestSetIntervalCmd(TestPropertyValueCmd):
1101
1801
    command = SetIntervalCmd
1102
 
    property = "Interval"
1103
 
    values_to_set = ["P0D", "PT5M", "PT1S", "PT120S", "P1Y"]
1104
 
    values_to_get = [0, 300000, 1000, 120000, 31449600000]
1105
 
 
1106
 
class TestSetApprovalDelayCmd(TestValueArgumentPropertyCmd):
 
1802
    propname = "Interval"
 
1803
    values_to_set = [datetime.timedelta(),
 
1804
                     datetime.timedelta(minutes=5),
 
1805
                     datetime.timedelta(seconds=1),
 
1806
                     datetime.timedelta(weeks=1),
 
1807
                     datetime.timedelta(weeks=52)]
 
1808
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
 
1809
 
 
1810
 
 
1811
class TestSetApprovalDelayCmd(TestPropertyValueCmd):
1107
1812
    command = SetApprovalDelayCmd
1108
 
    property = "ApprovalDelay"
1109
 
    values_to_set = ["P0D", "PT5M", "PT1S", "PT120S", "P1Y"]
1110
 
    values_to_get = [0, 300000, 1000, 120000, 31449600000]
1111
 
 
1112
 
class TestSetApprovalDurationCmd(TestValueArgumentPropertyCmd):
 
1813
    propname = "ApprovalDelay"
 
1814
    values_to_set = [datetime.timedelta(),
 
1815
                     datetime.timedelta(minutes=5),
 
1816
                     datetime.timedelta(seconds=1),
 
1817
                     datetime.timedelta(weeks=1),
 
1818
                     datetime.timedelta(weeks=52)]
 
1819
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
 
1820
 
 
1821
 
 
1822
class TestSetApprovalDurationCmd(TestPropertyValueCmd):
1113
1823
    command = SetApprovalDurationCmd
1114
 
    property = "ApprovalDuration"
1115
 
    values_to_set = ["P0D", "PT5M", "PT1S", "PT120S", "P1Y"]
1116
 
    values_to_get = [0, 300000, 1000, 120000, 31449600000]
1117
 
 
1118
 
class TestOptions(unittest.TestCase):
1119
 
    def setUp(self):
1120
 
        self.parser = argparse.ArgumentParser()
1121
 
        add_command_line_options(self.parser)
1122
 
    def assert_command_from_args(self, args, command_cls, **cmd_attrs):
1123
 
        """Assert that parsing ARGS should result in an instance of
1124
 
COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
1125
 
        options = self.parser.parse_args(args)
1126
 
        commands = commands_from_options(options)
1127
 
        self.assertEqual(len(commands), 1)
1128
 
        command = commands[0]
1129
 
        self.assertIsInstance(command, command_cls)
1130
 
        for key, value in cmd_attrs.items():
1131
 
            self.assertEqual(getattr(command, key), value)
1132
 
    def test_default_is_show_table(self):
1133
 
        self.assert_command_from_args([], PrintTableCmd,
1134
 
                                      verbose=False)
1135
 
    def test_show_table_verbose(self):
1136
 
        self.assert_command_from_args(["--verbose"], PrintTableCmd,
1137
 
                                      verbose=True)
1138
 
    def test_enable(self):
1139
 
        self.assert_command_from_args(["--enable"], EnableCmd)
1140
 
    def test_disable(self):
1141
 
        self.assert_command_from_args(["--disable"], DisableCmd)
 
1824
    propname = "ApprovalDuration"
 
1825
    values_to_set = [datetime.timedelta(),
 
1826
                     datetime.timedelta(minutes=5),
 
1827
                     datetime.timedelta(seconds=1),
 
1828
                     datetime.timedelta(weeks=1),
 
1829
                     datetime.timedelta(weeks=52)]
 
1830
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1142
1831
 
1143
1832
 
1144
1833
 
1159
1848
    return tests
1160
1849
 
1161
1850
if __name__ == "__main__":
1162
 
    if should_only_run_tests():
1163
 
        # Call using ./tdd-python-script --check [--verbose]
1164
 
        unittest.main()
1165
 
    else:
1166
 
        main()
 
1851
    try:
 
1852
        if should_only_run_tests():
 
1853
            # Call using ./tdd-python-script --check [--verbose]
 
1854
            unittest.main()
 
1855
        else:
 
1856
            main()
 
1857
    finally:
 
1858
        logging.shutdown()