/mandos/release

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

« back to all changes in this revision

Viewing changes to mandos-ctl

  • Committer: Teddy Hogeborn
  • Date: 2019-03-31 04:39:25 UTC
  • mto: This revision was merged to the branch mainline in revision 382.
  • Revision ID: teddy@recompile.se-20190331043925-0j9pdspo3hux5nka
mandos-ctl: Refactor: Move command list generation into argparse

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

Show diffs side-by-side

added added

removed removed

Lines of Context:
122
122
                        help="Select all clients")
123
123
    parser.add_argument("-v", "--verbose", action="store_true",
124
124
                        help="Print all fields")
125
 
    parser.add_argument("-j", "--dump-json", action="store_true",
 
125
    parser.add_argument("-j", "--dump-json", dest="commands",
 
126
                        action="append_const", default=[],
 
127
                        const=command.DumpJSON(),
126
128
                        help="Dump client data in JSON format")
127
129
    enable_disable = parser.add_mutually_exclusive_group()
128
 
    enable_disable.add_argument("-e", "--enable", action="store_true",
 
130
    enable_disable.add_argument("-e", "--enable", dest="commands",
 
131
                                action="append_const", default=[],
 
132
                                const=command.Enable(),
129
133
                                help="Enable client")
130
 
    enable_disable.add_argument("-d", "--disable",
131
 
                                action="store_true",
 
134
    enable_disable.add_argument("-d", "--disable", dest="commands",
 
135
                                action="append_const", default=[],
 
136
                                const=command.Disable(),
132
137
                                help="disable client")
133
 
    parser.add_argument("-b", "--bump-timeout", action="store_true",
 
138
    parser.add_argument("-b", "--bump-timeout", dest="commands",
 
139
                        action="append_const", default=[],
 
140
                        const=command.BumpTimeout(),
134
141
                        help="Bump timeout for client")
135
142
    start_stop_checker = parser.add_mutually_exclusive_group()
136
143
    start_stop_checker.add_argument("--start-checker",
137
 
                                    action="store_true",
 
144
                                    dest="commands",
 
145
                                    action="append_const", default=[],
 
146
                                    const=command.StartChecker(),
138
147
                                    help="Start checker for client")
139
 
    start_stop_checker.add_argument("--stop-checker",
140
 
                                    action="store_true",
 
148
    start_stop_checker.add_argument("--stop-checker", dest="commands",
 
149
                                    action="append_const", default=[],
 
150
                                    const=command.StopChecker(),
141
151
                                    help="Stop checker for client")
142
 
    parser.add_argument("-V", "--is-enabled", action="store_true",
 
152
    parser.add_argument("-V", "--is-enabled", dest="commands",
 
153
                        action="append_const", default=[],
 
154
                        const=command.IsEnabled(),
143
155
                        help="Check if client is enabled")
144
 
    parser.add_argument("-r", "--remove", action="store_true",
 
156
    parser.add_argument("-r", "--remove", dest="commands",
 
157
                        action="append_const", default=[],
 
158
                        const=command.Remove(),
145
159
                        help="Remove client")
146
 
    parser.add_argument("-c", "--checker",
 
160
    parser.add_argument("-c", "--checker", dest="commands",
 
161
                        action="append", default=[],
 
162
                        metavar="COMMAND", type=command.SetChecker,
147
163
                        help="Set checker command for client")
148
 
    parser.add_argument("-t", "--timeout", type=string_to_delta,
149
 
                        help="Set timeout for client")
150
 
    parser.add_argument("--extended-timeout", type=string_to_delta,
151
 
                        help="Set extended timeout for client")
152
 
    parser.add_argument("-i", "--interval", type=string_to_delta,
153
 
                        help="Set checker interval for client")
 
164
    parser.add_argument(
 
165
        "-t", "--timeout", dest="commands", action="append",
 
166
        default=[], metavar="TIME",
 
167
        type=command.SetTimeout.argparse(string_to_delta),
 
168
        help="Set timeout for client")
 
169
    parser.add_argument(
 
170
        "--extended-timeout", dest="commands", action="append",
 
171
        default=[], metavar="TIME",
 
172
        type=command.SetExtendedTimeout.argparse(string_to_delta),
 
173
        help="Set extended timeout for client")
 
174
    parser.add_argument(
 
175
        "-i", "--interval", dest="commands", action="append",
 
176
        default=[], metavar="TIME",
 
177
        type=command.SetInterval.argparse(string_to_delta),
 
178
        help="Set checker interval for client")
154
179
    approve_deny_default = parser.add_mutually_exclusive_group()
155
180
    approve_deny_default.add_argument(
156
 
        "--approve-by-default", action="store_true",
157
 
        default=None, dest="approved_by_default",
 
181
        "--approve-by-default", dest="commands",
 
182
        action="append_const", default=[],
 
183
        const=command.ApproveByDefault(),
158
184
        help="Set client to be approved by default")
159
185
    approve_deny_default.add_argument(
160
 
        "--deny-by-default", action="store_false",
161
 
        dest="approved_by_default",
 
186
        "--deny-by-default", dest="commands",
 
187
        action="append_const", default=[],
 
188
        const=command.DenyByDefault(),
162
189
        help="Set client to be denied by default")
163
 
    parser.add_argument("--approval-delay", type=string_to_delta,
164
 
                        help="Set delay before client approve/deny")
165
 
    parser.add_argument("--approval-duration", type=string_to_delta,
166
 
                        help="Set duration of one client approval")
167
 
    parser.add_argument("-H", "--host", help="Set host for client")
168
 
    parser.add_argument("-s", "--secret",
169
 
                        type=argparse.FileType(mode="rb"),
170
 
                        help="Set password blob (file) for client")
 
190
    parser.add_argument(
 
191
        "--approval-delay", dest="commands", action="append",
 
192
        default=[], metavar="TIME",
 
193
        type=command.SetApprovalDelay.argparse(string_to_delta),
 
194
        help="Set delay before client approve/deny")
 
195
    parser.add_argument(
 
196
        "--approval-duration", dest="commands", action="append",
 
197
        default=[], metavar="TIME",
 
198
        type=command.SetApprovalDuration.argparse(string_to_delta),
 
199
        help="Set duration of one client approval")
 
200
    parser.add_argument("-H", "--host", dest="commands",
 
201
                        action="append", default=[], metavar="STRING",
 
202
                        type=command.SetHost,
 
203
                        help="Set host for client")
 
204
    parser.add_argument(
 
205
        "-s", "--secret", dest="commands", action="append",
 
206
        default=[], metavar="FILENAME",
 
207
        type=command.SetSecret.argparse(argparse.FileType(mode="rb")),
 
208
        help="Set password blob (file) for client")
171
209
    approve_deny = parser.add_mutually_exclusive_group()
172
210
    approve_deny.add_argument(
173
 
        "-A", "--approve", action="store_true",
 
211
        "-A", "--approve", dest="commands", action="append_const",
 
212
        default=[], const=command.Approve(),
174
213
        help="Approve any current client request")
175
 
    approve_deny.add_argument("-D", "--deny", action="store_true",
 
214
    approve_deny.add_argument("-D", "--deny", dest="commands",
 
215
                              action="append_const", default=[],
 
216
                              const=command.Deny(),
176
217
                              help="Deny any current client request")
177
218
    parser.add_argument("--debug", action="store_true",
178
219
                        help="Debug mode (show D-Bus commands)")
369
410
    """Apply additional restrictions on options, not expressible in
370
411
argparse"""
371
412
 
372
 
    def has_actions(options):
373
 
        return any((options.enable,
374
 
                    options.disable,
375
 
                    options.bump_timeout,
376
 
                    options.start_checker,
377
 
                    options.stop_checker,
378
 
                    options.is_enabled,
379
 
                    options.remove,
380
 
                    options.checker is not None,
381
 
                    options.timeout is not None,
382
 
                    options.extended_timeout is not None,
383
 
                    options.interval is not None,
384
 
                    options.approved_by_default is not None,
385
 
                    options.approval_delay is not None,
386
 
                    options.approval_duration is not None,
387
 
                    options.host is not None,
388
 
                    options.secret is not None,
389
 
                    options.approve,
390
 
                    options.deny))
 
413
    def has_commands(options, commands=None):
 
414
        if commands is None:
 
415
            commands = (command.Enable,
 
416
                        command.Disable,
 
417
                        command.BumpTimeout,
 
418
                        command.StartChecker,
 
419
                        command.StopChecker,
 
420
                        command.IsEnabled,
 
421
                        command.Remove,
 
422
                        command.SetChecker,
 
423
                        command.SetTimeout,
 
424
                        command.SetExtendedTimeout,
 
425
                        command.SetInterval,
 
426
                        command.ApproveByDefault,
 
427
                        command.DenyByDefault,
 
428
                        command.SetApprovalDelay,
 
429
                        command.SetApprovalDuration,
 
430
                        command.SetHost,
 
431
                        command.SetSecret,
 
432
                        command.Approve,
 
433
                        command.Deny)
 
434
        return any(isinstance(cmd, commands)
 
435
                   for cmd in options.commands)
391
436
 
392
 
    if has_actions(options) and not (options.client or options.all):
 
437
    if has_commands(options) and not (options.client or options.all):
393
438
        parser.error("Options require clients names or --all.")
394
 
    if options.verbose and has_actions(options):
 
439
    if options.verbose and has_commands(options):
395
440
        parser.error("--verbose can only be used alone.")
396
 
    if options.dump_json and (options.verbose
397
 
                              or has_actions(options)):
 
441
    if (has_commands(options, (command.DumpJSON,))
 
442
        and (options.verbose or len(options.commands) > 1)):
398
443
        parser.error("--dump-json can only be used alone.")
399
 
    if options.all and not has_actions(options):
 
444
    if options.all and not has_commands(options):
400
445
        parser.error("--all requires an action.")
401
 
    if options.is_enabled and len(options.client) > 1:
 
446
    if (has_commands(options, (command.IsEnabled,))
 
447
        and len(options.client) > 1):
402
448
        parser.error("--is-enabled requires exactly one client")
403
 
    if options.remove:
404
 
        options.remove = False
405
 
        if has_actions(options) and not options.deny:
406
 
            parser.error("--remove can only be combined with --deny")
407
 
        options.remove = True
408
 
 
 
449
    if (len(options.commands) > 1
 
450
        and has_commands(options, (command.Remove,))
 
451
        and not has_commands(options, (command.Deny,))):
 
452
        parser.error("--remove can only be combined with --deny")
409
453
 
410
454
 
411
455
class dbus(object):
551
595
 
552
596
def commands_from_options(options):
553
597
 
554
 
    commands = []
555
 
 
556
 
    if options.is_enabled:
557
 
        commands.append(command.IsEnabled())
558
 
 
559
 
    if options.approve:
560
 
        commands.append(command.Approve())
561
 
 
562
 
    if options.deny:
563
 
        commands.append(command.Deny())
564
 
 
565
 
    if options.remove:
566
 
        commands.append(command.Remove())
567
 
 
568
 
    if options.dump_json:
569
 
        commands.append(command.DumpJSON())
570
 
 
571
 
    if options.enable:
572
 
        commands.append(command.Enable())
573
 
 
574
 
    if options.disable:
575
 
        commands.append(command.Disable())
576
 
 
577
 
    if options.bump_timeout:
578
 
        commands.append(command.BumpTimeout())
579
 
 
580
 
    if options.start_checker:
581
 
        commands.append(command.StartChecker())
582
 
 
583
 
    if options.stop_checker:
584
 
        commands.append(command.StopChecker())
585
 
 
586
 
    if options.approved_by_default is not None:
587
 
        if options.approved_by_default:
588
 
            commands.append(command.ApproveByDefault())
 
598
    commands = list(options.commands)
 
599
 
 
600
    def find_cmd(cmd, commands):
 
601
        i = 0
 
602
        for i, c in enumerate(commands):
 
603
            if isinstance(c, cmd):
 
604
                return i
 
605
        return i+1
 
606
 
 
607
    # If command.Remove is present, move any instances of command.Deny
 
608
    # to occur ahead of command.Remove.
 
609
    index_of_remove = find_cmd(command.Remove, commands)
 
610
    before_remove = commands[:index_of_remove]
 
611
    after_remove = commands[index_of_remove:]
 
612
    cleaned_after = []
 
613
    for cmd in after_remove:
 
614
        if isinstance(cmd, command.Deny):
 
615
            before_remove.append(cmd)
589
616
        else:
590
 
            commands.append(command.DenyByDefault())
591
 
 
592
 
    if options.checker is not None:
593
 
        commands.append(command.SetChecker(options.checker))
594
 
 
595
 
    if options.host is not None:
596
 
        commands.append(command.SetHost(options.host))
597
 
 
598
 
    if options.secret is not None:
599
 
        commands.append(command.SetSecret(options.secret))
600
 
 
601
 
    if options.timeout is not None:
602
 
        commands.append(command.SetTimeout(options.timeout))
603
 
 
604
 
    if options.extended_timeout:
605
 
        commands.append(
606
 
            command.SetExtendedTimeout(options.extended_timeout))
607
 
 
608
 
    if options.interval is not None:
609
 
        commands.append(command.SetInterval(options.interval))
610
 
 
611
 
    if options.approval_delay is not None:
612
 
        commands.append(
613
 
            command.SetApprovalDelay(options.approval_delay))
614
 
 
615
 
    if options.approval_duration is not None:
616
 
        commands.append(
617
 
            command.SetApprovalDuration(options.approval_duration))
 
617
            cleaned_after.append(cmd)
 
618
    if cleaned_after != after_remove:
 
619
        commands = before_remove + cleaned_after
618
620
 
619
621
    # If no command option has been given, show table of clients,
620
622
    # optionally verbosely
835
837
        def __init__(self, value):
836
838
            self.value_to_set = value
837
839
 
 
840
        @classmethod
 
841
        def argparse(cls, argtype):
 
842
            def cmdtype(arg):
 
843
                return cls(argtype(arg))
 
844
            return cmdtype
838
845
 
839
846
    class SetChecker(PropertySetterValue):
840
847
        propname = "Checker"
966
973
 
967
974
    def test_actions_requires_client_or_all(self):
968
975
        for action, value in self.actions.items():
969
 
            options = self.parser.parse_args()
970
 
            setattr(options, action, value)
 
976
            args = self.actionargs(action, value)
971
977
            with self.assertParseError():
972
 
                self.check_option_syntax(options)
 
978
                self.parse_args(args)
973
979
 
974
 
    # This mostly corresponds to the definition from has_actions() in
 
980
    # This mostly corresponds to the definition from has_commands() in
975
981
    # check_option_syntax()
976
982
    actions = {
977
 
        # The actual values set here are not that important, but we do
978
 
        # at least stick to the correct types, even though they are
979
 
        # never used
980
 
        "enable": True,
981
 
        "disable": True,
982
 
        "bump_timeout": True,
983
 
        "start_checker": True,
984
 
        "stop_checker": True,
985
 
        "is_enabled": True,
986
 
        "remove": True,
987
 
        "checker": "x",
988
 
        "timeout": datetime.timedelta(),
989
 
        "extended_timeout": datetime.timedelta(),
990
 
        "interval": datetime.timedelta(),
991
 
        "approved_by_default": True,
992
 
        "approval_delay": datetime.timedelta(),
993
 
        "approval_duration": datetime.timedelta(),
994
 
        "host": "x",
995
 
        "secret": io.BytesIO(b"x"),
996
 
        "approve": True,
997
 
        "deny": True,
 
983
        "--enable": None,
 
984
        "--disable": None,
 
985
        "--bump-timeout": None,
 
986
        "--start-checker": None,
 
987
        "--stop-checker": None,
 
988
        "--is-enabled": None,
 
989
        "--remove": None,
 
990
        "--checker": "x",
 
991
        "--timeout": "PT0S",
 
992
        "--extended-timeout": "PT0S",
 
993
        "--interval": "PT0S",
 
994
        "--approve-by-default": None,
 
995
        "--deny-by-default": None,
 
996
        "--approval-delay": "PT0S",
 
997
        "--approval-duration": "PT0S",
 
998
        "--host": "hostname",
 
999
        "--secret": "/dev/null",
 
1000
        "--approve": None,
 
1001
        "--deny": None,
998
1002
    }
999
1003
 
 
1004
    @staticmethod
 
1005
    def actionargs(action, value, *args):
 
1006
        if value is not None:
 
1007
            return [action, value] + list(args)
 
1008
        else:
 
1009
            return [action] + list(args)
 
1010
 
1000
1011
    @contextlib.contextmanager
1001
1012
    def assertParseError(self):
1002
1013
        with self.assertRaises(SystemExit) as e:
1007
1018
        # /argparse.html#exiting-methods
1008
1019
        self.assertEqual(2, e.exception.code)
1009
1020
 
 
1021
    def parse_args(self, args):
 
1022
        options = self.parser.parse_args(args)
 
1023
        check_option_syntax(self.parser, options)
 
1024
 
1010
1025
    @staticmethod
1011
1026
    @contextlib.contextmanager
1012
1027
    def redirect_stderr_to_devnull():
1023
1038
 
1024
1039
    def test_actions_all_conflicts_with_verbose(self):
1025
1040
        for action, value in self.actions.items():
1026
 
            options = self.parser.parse_args()
1027
 
            setattr(options, action, value)
1028
 
            options.all = True
1029
 
            options.verbose = True
 
1041
            args = self.actionargs(action, value, "--all",
 
1042
                                   "--verbose")
1030
1043
            with self.assertParseError():
1031
 
                self.check_option_syntax(options)
 
1044
                self.parse_args(args)
1032
1045
 
1033
1046
    def test_actions_with_client_conflicts_with_verbose(self):
1034
1047
        for action, value in self.actions.items():
1035
 
            options = self.parser.parse_args()
1036
 
            setattr(options, action, value)
1037
 
            options.verbose = True
1038
 
            options.client = ["client"]
 
1048
            args = self.actionargs(action, value, "--verbose",
 
1049
                                   "client")
1039
1050
            with self.assertParseError():
1040
 
                self.check_option_syntax(options)
 
1051
                self.parse_args(args)
1041
1052
 
1042
1053
    def test_dump_json_conflicts_with_verbose(self):
1043
 
        options = self.parser.parse_args()
1044
 
        options.dump_json = True
1045
 
        options.verbose = True
 
1054
        args = ["--dump-json", "--verbose"]
1046
1055
        with self.assertParseError():
1047
 
            self.check_option_syntax(options)
 
1056
            self.parse_args(args)
1048
1057
 
1049
1058
    def test_dump_json_conflicts_with_action(self):
1050
1059
        for action, value in self.actions.items():
1051
 
            options = self.parser.parse_args()
1052
 
            setattr(options, action, value)
1053
 
            options.dump_json = True
 
1060
            args = self.actionargs(action, value, "--dump-json")
1054
1061
            with self.assertParseError():
1055
 
                self.check_option_syntax(options)
 
1062
                self.parse_args(args)
1056
1063
 
1057
1064
    def test_all_can_not_be_alone(self):
1058
 
        options = self.parser.parse_args()
1059
 
        options.all = True
 
1065
        args = ["--all"]
1060
1066
        with self.assertParseError():
1061
 
            self.check_option_syntax(options)
 
1067
            self.parse_args(args)
1062
1068
 
1063
1069
    def test_all_is_ok_with_any_action(self):
1064
1070
        for action, value in self.actions.items():
1065
 
            options = self.parser.parse_args()
1066
 
            setattr(options, action, value)
1067
 
            options.all = True
1068
 
            self.check_option_syntax(options)
 
1071
            args = self.actionargs(action, value, "--all")
 
1072
            self.parse_args(args)
1069
1073
 
1070
1074
    def test_any_action_is_ok_with_one_client(self):
1071
1075
        for action, value in self.actions.items():
1072
 
            options = self.parser.parse_args()
1073
 
            setattr(options, action, value)
1074
 
            options.client = ["client"]
1075
 
            self.check_option_syntax(options)
 
1076
            args = self.actionargs(action, value, "client")
 
1077
            self.parse_args(args)
1076
1078
 
1077
1079
    def test_one_client_with_all_actions_except_is_enabled(self):
1078
 
        options = self.parser.parse_args()
1079
1080
        for action, value in self.actions.items():
1080
 
            if action == "is_enabled":
 
1081
            if action == "--is-enabled":
1081
1082
                continue
1082
 
            setattr(options, action, value)
1083
 
        options.client = ["client"]
1084
 
        self.check_option_syntax(options)
 
1083
            args = self.actionargs(action, value, "client")
 
1084
            self.parse_args(args)
1085
1085
 
1086
1086
    def test_two_clients_with_all_actions_except_is_enabled(self):
1087
 
        options = self.parser.parse_args()
1088
1087
        for action, value in self.actions.items():
1089
 
            if action == "is_enabled":
 
1088
            if action == "--is-enabled":
1090
1089
                continue
1091
 
            setattr(options, action, value)
1092
 
        options.client = ["client1", "client2"]
1093
 
        self.check_option_syntax(options)
 
1090
            args = self.actionargs(action, value, "client1",
 
1091
                                   "client2")
 
1092
            self.parse_args(args)
1094
1093
 
1095
1094
    def test_two_clients_are_ok_with_actions_except_is_enabled(self):
1096
1095
        for action, value in self.actions.items():
1097
 
            if action == "is_enabled":
 
1096
            if action == "--is-enabled":
1098
1097
                continue
1099
 
            options = self.parser.parse_args()
1100
 
            setattr(options, action, value)
1101
 
            options.client = ["client1", "client2"]
1102
 
            self.check_option_syntax(options)
 
1098
            args = self.actionargs(action, value, "client1",
 
1099
                                   "client2")
 
1100
            self.parse_args(args)
1103
1101
 
1104
1102
    def test_is_enabled_fails_without_client(self):
1105
 
        options = self.parser.parse_args()
1106
 
        options.is_enabled = True
 
1103
        args = ["--is-enabled"]
1107
1104
        with self.assertParseError():
1108
 
            self.check_option_syntax(options)
 
1105
            self.parse_args(args)
1109
1106
 
1110
1107
    def test_is_enabled_fails_with_two_clients(self):
1111
 
        options = self.parser.parse_args()
1112
 
        options.is_enabled = True
1113
 
        options.client = ["client1", "client2"]
 
1108
        args = ["--is-enabled", "client1", "client2"]
1114
1109
        with self.assertParseError():
1115
 
            self.check_option_syntax(options)
 
1110
            self.parse_args(args)
1116
1111
 
1117
1112
    def test_remove_can_only_be_combined_with_action_deny(self):
1118
1113
        for action, value in self.actions.items():
1119
 
            if action in {"remove", "deny"}:
 
1114
            if action in {"--remove", "--deny"}:
1120
1115
                continue
1121
 
            options = self.parser.parse_args()
1122
 
            setattr(options, action, value)
1123
 
            options.all = True
1124
 
            options.remove = True
 
1116
            args = self.actionargs(action, value, "--all",
 
1117
                                   "--remove")
1125
1118
            with self.assertParseError():
1126
 
                self.check_option_syntax(options)
 
1119
                self.parse_args(args)
1127
1120
 
1128
1121
 
1129
1122
class Test_dbus_exceptions(unittest.TestCase):