/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-02-09 23:23:26 UTC
  • Revision ID: teddy@recompile.se-20190209232326-z1z2kzpgfixz7iaj
Add support for using raw public keys in TLS (RFC 7250)

Since GnuTLS removed support for OpenPGP keys in TLS (RFC 6091), and
no other library supports it, we have to change the protocol to use
something else.  We choose to use "raw public keys" (RFC 7250).  Since
we still use OpenPGP keys to decrypt the secret password, this means
that each client will have two keys: One OpenPGP key and one TLS
public/private key, and the key ID of the latter key is used to
identify clients instead of the fingerprint of the OpenPGP key.

Note that this code is still compatible with GnuTLS before version
3.6.0 (when OpenPGP key support was removed).  This commit merely adds
support for using raw pulic keys instead with GnuTLS 3.6.6. or later.

* DBUS-API (Signals/ClientNotFound): Change name of first parameter
                                     from "Fingerprint" to "KeyID".
  (Mandos Client Interface/Properties/KeyID): New.
* INSTALL: Document conflict with GnuTLS 3.6.0 (which removed OpenPGP
           key support) up until 3.6.6, when support for raw public
           keys was added.  Also document new dependency of client on
           "gnutls-bin" package (for certtool).
* Makefile (run-client): Depend on TLS key files, and also pass them
                         as arguments to client.
  (keydir/tls-privkey.pem, keydir/tls-pubkey.pem): New.
  (confdir/clients.conf): Add dependency on TLS public key.
  (purge-client): Add removal of TLS key files.
* clients.conf ([foo]/key_id, [bar]/key_id): New.
* debian/control (Source: mandos/Build-Depends): Also allow
                                                 libgnutls30 (>= 3.6.6)
  (Package: mandos/Depends): - '' -
  (Package: mandos/Description): Alter description to match new
                                 design.
  (Package: mandos-client/Description): - '' -
  (Package: mandos-client/Depends): Move "gnutls-bin | openssl" to
                                    here from "Recommends".
* debian/mandos-client.README.Debian: Add --tls-privkey and
                                      --tls-pubkey options to test
                                      command.
* debian/mandos-client.postinst (create_key): Renamed to "create_keys"
                                             (all callers changed),
                                             and also create TLS key.
* debian/mandos-client.postrm (purge): Also remove TLS key files.
* intro.xml (DESCRIPTION): Describe new dual-key design.
* mandos (GnuTLS): Define different functions depending on whether
                   support for raw public keys is detected.
  (Client.key_id): New attribute.
  (ClientDBus.KeyID_dbus_property): New method.
  (ProxyClient.__init__): Take new "key_id" parameter.
  (ClientHandler.handle): Use key IDs when using raw public keys and
                          use fingerprints when using OpenPGP keys.
  (ClientHandler.peer_certificate): Also handle raw public keys.
  (ClientHandler.key_id): New.
  (MandosServer.handle_ipc): Pass key ID over the pipe IPC.  Also
                             check for key ID matches when looking up
                             clients.
  (main): Default GnuTLS priority string depends on whether we are
          using raw public keys or not.  When unpickling clients, set
          key_id if not set in the pickle.
  (main/MandosDBusService.ClientNotFound): Change name of first
                                           parameter from
                                           "Fingerprint" to "KeyID".
* mandos-clients.conf.xml (OPTIONS): Document new "key_id" option.
  (OPTIONS/secret): Mention new key ID matchning.
  (EXPANSION/RUNTIME EXPANSION): Add new "key_id" option.
  (EXAMPLE): - '' -
* mandos-ctl (tablewords, main/keywords): Add new "KeyID" property.
* mandos-keygen: Create TLS key files.  New "--tls-keytype" (-T)
                 option.  Alter help text to be more clear about key
                 types.  When in password mode, also output "key_id"
                 option.
* mandos-keygen.xml (SYNOPSIS): Add new "--tls-keytype" (-T) option.
  (DESCRIPTION): Alter to match new dual-key design.
  (OVERVIEW): - '' -
  (FILES): Add TLS key files.
* mandos-options.xml (priority): Document new default priority string
                                 when using raw public keys.
* mandos.xml (NETWORK PROTOCOL): Describe new protocol using key ID.
  (BUGS): Remove issue about checking expire times of OpenPGP keys,
          since TLS public keys do not have expiration times.
  (SECURITY/CLIENT): Alter description to match new design.
  (SEE ALSO/GnuTLS): - '' -
  (SEE ALSO): Add reference to RFC 7250, and alter description of when
              RFC 6091 is used.
* overview.xml: Alter text to match new design.
* plugin-runner.xml (EXAMPLE): Add --tls-pubkey and --tls-privkey
                               options to mandos-client options.
* plugins.d/mandos-client.c: Use raw public keys when compiling with
                             supporting GnuTLS versions. Add new
                             "--tls-pubkey" and "--tls-privkey"
                             options (which do nothing if GnuTLS
                             library does not support raw public
                             keys).  Alter text throughout to reflect
                             new design.  Only generate new DH
                             parameters (based on size of OpenPGP key)
                             when using OpenPGP in TLS.  Default
                             GnuTLS priority string depends on whether
                             we are using raw public keys or not.
* plugins.d/mandos-client.xml (SYNOPSIS): Add new "--tls-privkey" (-t)
                                          and "--tls-pubkey" (-T)
                                          options.
  (DESCRIPTION): Describe new dual-key design.
  (OPTIONS): Document new "--tls-privkey" (-t) and "--tls-pubkey" (-T)
             options.
  (OPTIONS/--dh-bits): No longer necessarily depends on OpenPGP key
                       size.
  (FILES): Add default locations for TLS public and private key files.
  (EXAMPLE): Use new --tls-pubkey and --tls-privkey options.
  (SECURITY): Alter wording slightly to reflect new dual-key design.
  (SEE ALSO/GnuTLS): Alter description to match new design.
  (SEE ALSO): Add reference to RFC 7250, and alter description of when
              RFC 6091 is used.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
2
# -*- mode: python; coding: utf-8 -*-
3
 
 
4
 
from __future__ import division
 
3
#
 
4
# Mandos Monitor - Control and monitor the Mandos server
 
5
#
 
6
# Copyright © 2008-2018 Teddy Hogeborn
 
7
# Copyright © 2008-2018 Björn Påhlsson
 
8
#
 
9
# This file is part of Mandos.
 
10
#
 
11
# Mandos is free software: you can redistribute it and/or modify it
 
12
# under the terms of the GNU General Public License as published by
 
13
# the Free Software Foundation, either version 3 of the License, or
 
14
# (at your option) any later version.
 
15
#
 
16
#     Mandos is distributed in the hope that it will be useful, but
 
17
#     WITHOUT ANY WARRANTY; without even the implied warranty of
 
18
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
19
#     GNU General Public License for more details.
 
20
#
 
21
# You should have received a copy of the GNU General Public License
 
22
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
 
23
#
 
24
# Contact the authors at <mandos@recompile.se>.
 
25
#
 
26
 
 
27
from __future__ import (division, absolute_import, print_function,
 
28
                        unicode_literals)
 
29
 
 
30
try:
 
31
    from future_builtins import *
 
32
except ImportError:
 
33
    pass
 
34
 
5
35
import sys
6
 
import dbus
7
 
from optparse import OptionParser
 
36
import argparse
8
37
import locale
9
38
import datetime
10
39
import re
11
 
 
12
 
locale.setlocale(locale.LC_ALL, u'')
 
40
import os
 
41
import collections
 
42
import json
 
43
 
 
44
import dbus
 
45
 
 
46
if sys.version_info.major == 2:
 
47
    str = unicode
 
48
 
 
49
locale.setlocale(locale.LC_ALL, "")
13
50
 
14
51
tablewords = {
15
 
    'name': u'Name',
16
 
    'enabled': u'Enabled',
17
 
    'timeout': u'Timeout',
18
 
    'last_checked_ok': u'Last Successful Check',
19
 
    'created': u'Created',
20
 
    'interval': u'Interval',
21
 
    'host': u'Host',
22
 
    'fingerprint': u'Fingerprint',
23
 
    'checker_running': u'Check Is Running',
24
 
    'last_enabled': u'Last Enabled',
25
 
    'checker': u'Checker',
26
 
    }
27
 
defaultkeywords = ('name', 'enabled', 'timeout', 'last_checked_ok',
28
 
                   'checker')
29
 
domain = 'se.bsnet.fukt'
30
 
busname = domain + '.Mandos'
31
 
server_path = '/'
32
 
server_interface = domain + '.Mandos'
33
 
client_interface = domain + '.Mandos.Client'
34
 
version = "1.0.14"
35
 
bus = dbus.SystemBus()
36
 
mandos_dbus_objc = bus.get_object(busname, server_path)
37
 
mandos_serv = dbus.Interface(mandos_dbus_objc,
38
 
                             dbus_interface = server_interface)
39
 
mandos_clients = mandos_serv.GetAllClientsWithProperties()
40
 
 
41
 
def timedelta_to_milliseconds(td):
42
 
    "Convert a datetime.timedelta object to milliseconds"
43
 
    return ((td.days * 24 * 60 * 60 * 1000)
44
 
            + (td.seconds * 1000)
45
 
            + (td.microseconds // 1000))
 
52
    "Name": "Name",
 
53
    "Enabled": "Enabled",
 
54
    "Timeout": "Timeout",
 
55
    "LastCheckedOK": "Last Successful Check",
 
56
    "LastApprovalRequest": "Last Approval Request",
 
57
    "Created": "Created",
 
58
    "Interval": "Interval",
 
59
    "Host": "Host",
 
60
    "Fingerprint": "Fingerprint",
 
61
    "KeyID": "Key ID",
 
62
    "CheckerRunning": "Check Is Running",
 
63
    "LastEnabled": "Last Enabled",
 
64
    "ApprovalPending": "Approval Is Pending",
 
65
    "ApprovedByDefault": "Approved By Default",
 
66
    "ApprovalDelay": "Approval Delay",
 
67
    "ApprovalDuration": "Approval Duration",
 
68
    "Checker": "Checker",
 
69
    "ExtendedTimeout": "Extended Timeout",
 
70
    "Expires": "Expires",
 
71
    "LastCheckerStatus": "Last Checker Status",
 
72
}
 
73
defaultkeywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
 
74
domain = "se.recompile"
 
75
busname = domain + ".Mandos"
 
76
server_path = "/"
 
77
server_interface = domain + ".Mandos"
 
78
client_interface = domain + ".Mandos.Client"
 
79
version = "1.7.20"
 
80
 
 
81
 
 
82
try:
 
83
    dbus.OBJECT_MANAGER_IFACE
 
84
except AttributeError:
 
85
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
 
86
 
46
87
 
47
88
def milliseconds_to_string(ms):
48
89
    td = datetime.timedelta(0, 0, 0, ms)
49
 
    return (u"%(days)s%(hours)02d:%(minutes)02d:%(seconds)02d"
50
 
            % { "days": "%dT" % td.days if td.days else "",
51
 
                "hours": td.seconds // 3600,
52
 
                "minutes": (td.seconds % 3600) // 60,
53
 
                "seconds": td.seconds % 60,
54
 
                })
 
90
    return ("{days}{hours:02}:{minutes:02}:{seconds:02}"
 
91
            .format(days="{}T".format(td.days) if td.days else "",
 
92
                    hours=td.seconds // 3600,
 
93
                    minutes=(td.seconds % 3600) // 60,
 
94
                    seconds=td.seconds % 60))
 
95
 
 
96
 
 
97
def rfc3339_duration_to_delta(duration):
 
98
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
 
99
 
 
100
    >>> rfc3339_duration_to_delta("P7D")
 
101
    datetime.timedelta(7)
 
102
    >>> rfc3339_duration_to_delta("PT60S")
 
103
    datetime.timedelta(0, 60)
 
104
    >>> rfc3339_duration_to_delta("PT60M")
 
105
    datetime.timedelta(0, 3600)
 
106
    >>> rfc3339_duration_to_delta("PT24H")
 
107
    datetime.timedelta(1)
 
108
    >>> rfc3339_duration_to_delta("P1W")
 
109
    datetime.timedelta(7)
 
110
    >>> rfc3339_duration_to_delta("PT5M30S")
 
111
    datetime.timedelta(0, 330)
 
112
    >>> rfc3339_duration_to_delta("P1DT3M20S")
 
113
    datetime.timedelta(1, 200)
 
114
    """
 
115
 
 
116
    # Parsing an RFC 3339 duration with regular expressions is not
 
117
    # possible - there would have to be multiple places for the same
 
118
    # values, like seconds.  The current code, while more esoteric, is
 
119
    # cleaner without depending on a parsing library.  If Python had a
 
120
    # built-in library for parsing we would use it, but we'd like to
 
121
    # avoid excessive use of external libraries.
 
122
 
 
123
    # New type for defining tokens, syntax, and semantics all-in-one
 
124
    Token = collections.namedtuple("Token", (
 
125
        "regexp",  # To match token; if "value" is not None, must have
 
126
                   # a "group" containing digits
 
127
        "value",   # datetime.timedelta or None
 
128
        "followers"))           # Tokens valid after this token
 
129
    # RFC 3339 "duration" tokens, syntax, and semantics; taken from
 
130
    # the "duration" ABNF definition in RFC 3339, Appendix A.
 
131
    token_end = Token(re.compile(r"$"), None, frozenset())
 
132
    token_second = Token(re.compile(r"(\d+)S"),
 
133
                         datetime.timedelta(seconds=1),
 
134
                         frozenset((token_end, )))
 
135
    token_minute = Token(re.compile(r"(\d+)M"),
 
136
                         datetime.timedelta(minutes=1),
 
137
                         frozenset((token_second, token_end)))
 
138
    token_hour = Token(re.compile(r"(\d+)H"),
 
139
                       datetime.timedelta(hours=1),
 
140
                       frozenset((token_minute, token_end)))
 
141
    token_time = Token(re.compile(r"T"),
 
142
                       None,
 
143
                       frozenset((token_hour, token_minute,
 
144
                                  token_second)))
 
145
    token_day = Token(re.compile(r"(\d+)D"),
 
146
                      datetime.timedelta(days=1),
 
147
                      frozenset((token_time, token_end)))
 
148
    token_month = Token(re.compile(r"(\d+)M"),
 
149
                        datetime.timedelta(weeks=4),
 
150
                        frozenset((token_day, token_end)))
 
151
    token_year = Token(re.compile(r"(\d+)Y"),
 
152
                       datetime.timedelta(weeks=52),
 
153
                       frozenset((token_month, token_end)))
 
154
    token_week = Token(re.compile(r"(\d+)W"),
 
155
                       datetime.timedelta(weeks=1),
 
156
                       frozenset((token_end, )))
 
157
    token_duration = Token(re.compile(r"P"), None,
 
158
                           frozenset((token_year, token_month,
 
159
                                      token_day, token_time,
 
160
                                      token_week)))
 
161
    # Define starting values:
 
162
    # Value so far
 
163
    value = datetime.timedelta()
 
164
    found_token = None
 
165
    # Following valid tokens
 
166
    followers = frozenset((token_duration, ))
 
167
    # String left to parse
 
168
    s = duration
 
169
    # Loop until end token is found
 
170
    while found_token is not token_end:
 
171
        # Search for any currently valid tokens
 
172
        for token in followers:
 
173
            match = token.regexp.match(s)
 
174
            if match is not None:
 
175
                # Token found
 
176
                if token.value is not None:
 
177
                    # Value found, parse digits
 
178
                    factor = int(match.group(1), 10)
 
179
                    # Add to value so far
 
180
                    value += factor * token.value
 
181
                # Strip token from string
 
182
                s = token.regexp.sub("", s, 1)
 
183
                # Go to found token
 
184
                found_token = token
 
185
                # Set valid next tokens
 
186
                followers = found_token.followers
 
187
                break
 
188
        else:
 
189
            # No currently valid tokens were found
 
190
            raise ValueError("Invalid RFC 3339 duration: {!r}"
 
191
                             .format(duration))
 
192
    # End token found
 
193
    return value
55
194
 
56
195
 
57
196
def string_to_delta(interval):
65
204
    datetime.timedelta(0, 3600)
66
205
    >>> string_to_delta('24h')
67
206
    datetime.timedelta(1)
68
 
    >>> string_to_delta(u'1w')
 
207
    >>> string_to_delta('1w')
69
208
    datetime.timedelta(7)
70
209
    >>> string_to_delta('5m 30s')
71
210
    datetime.timedelta(0, 330)
72
211
    """
73
 
    timevalue = datetime.timedelta(0)
74
 
    regexp = re.compile("\d+[dsmhw]")
75
 
    
76
 
    for s in regexp.findall(interval):
77
 
        try:
78
 
            suffix = unicode(s[-1])
79
 
            value = int(s[:-1])
80
 
            if suffix == u"d":
81
 
                delta = datetime.timedelta(value)
82
 
            elif suffix == u"s":
83
 
                delta = datetime.timedelta(0, value)
84
 
            elif suffix == u"m":
85
 
                delta = datetime.timedelta(0, 0, 0, 0, value)
86
 
            elif suffix == u"h":
87
 
                delta = datetime.timedelta(0, 0, 0, 0, 0, value)
88
 
            elif suffix == u"w":
89
 
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
90
 
            else:
91
 
                raise ValueError
92
 
        except (ValueError, IndexError):
93
 
            raise ValueError
94
 
        timevalue += delta
95
 
    return timevalue
96
 
 
97
 
def print_clients(clients):
 
212
 
 
213
    try:
 
214
        return rfc3339_duration_to_delta(interval)
 
215
    except ValueError:
 
216
        pass
 
217
 
 
218
    value = datetime.timedelta(0)
 
219
    regexp = re.compile(r"(\d+)([dsmhw]?)")
 
220
 
 
221
    for num, suffix in regexp.findall(interval):
 
222
        if suffix == "d":
 
223
            value += datetime.timedelta(int(num))
 
224
        elif suffix == "s":
 
225
            value += datetime.timedelta(0, int(num))
 
226
        elif suffix == "m":
 
227
            value += datetime.timedelta(0, 0, 0, 0, int(num))
 
228
        elif suffix == "h":
 
229
            value += datetime.timedelta(0, 0, 0, 0, 0, int(num))
 
230
        elif suffix == "w":
 
231
            value += datetime.timedelta(0, 0, 0, 0, 0, 0, int(num))
 
232
        elif suffix == "":
 
233
            value += datetime.timedelta(0, 0, 0, int(num))
 
234
    return value
 
235
 
 
236
 
 
237
def print_clients(clients, keywords):
98
238
    def valuetostring(value, keyword):
99
239
        if type(value) is dbus.Boolean:
100
 
            return u"Yes" if value else u"No"
101
 
        if keyword in (u"timeout", u"interval"):
 
240
            return "Yes" if value else "No"
 
241
        if keyword in ("Timeout", "Interval", "ApprovalDelay",
 
242
                       "ApprovalDuration", "ExtendedTimeout"):
102
243
            return milliseconds_to_string(value)
103
 
        return unicode(value)
104
 
    
 
244
        return str(value)
 
245
 
105
246
    # Create format string to print table rows
106
 
    format_string = u' '.join(u'%%-%ds' %
107
 
                              max(len(tablewords[key]),
108
 
                                  max(len(valuetostring(client[key], key))
109
 
                                      for client in
110
 
                                      clients))
111
 
                              for key in keywords)
 
247
    format_string = " ".join("{{{key}:{width}}}".format(
 
248
        width=max(len(tablewords[key]),
 
249
                  max(len(valuetostring(client[key], key))
 
250
                      for client in clients)),
 
251
        key=key)
 
252
                             for key in keywords)
112
253
    # Print header line
113
 
    print format_string % tuple(tablewords[key] for key in keywords)
 
254
    print(format_string.format(**tablewords))
114
255
    for client in clients:
115
 
        print format_string % tuple(valuetostring(client[key], key)
116
 
                                    for key in keywords)
117
 
 
118
 
parser = OptionParser(version = "%%prog %s" % version)
119
 
parser.add_option("-a", "--all", action="store_true",
120
 
                  help="Print all fields")
121
 
parser.add_option("-e", "--enable", action="store_true",
122
 
                  help="Enable client")
123
 
parser.add_option("-d", "--disable", action="store_true",
124
 
                  help="disable client")
125
 
parser.add_option("-b", "--bump-timeout", action="store_true",
126
 
                  help="Bump timeout for client")
127
 
parser.add_option("--start-checker", action="store_true",
128
 
                  help="Start checker for client")
129
 
parser.add_option("--stop-checker", action="store_true",
130
 
                  help="Stop checker for client")
131
 
parser.add_option("-V", "--is-valid", action="store_true",
132
 
                  help="Check if client is still valid")
133
 
parser.add_option("-r", "--remove", action="store_true",
134
 
                  help="Remove client")
135
 
parser.add_option("-c", "--checker", type="string",
136
 
                  help="Set checker command for client")
137
 
parser.add_option("-t", "--timeout", type="string",
138
 
                  help="Set timeout for client")
139
 
parser.add_option("-i", "--interval", type="string",
140
 
                  help="Set checker interval for client")
141
 
parser.add_option("-H", "--host", type="string",
142
 
                  help="Set host for client")
143
 
parser.add_option("-s", "--secret", type="string",
144
 
                  help="Set password blob (file) for client")
145
 
options, client_names = parser.parse_args()
146
 
 
147
 
# Compile list of clients to process
148
 
clients=[]
149
 
for name in client_names:
150
 
    for path, client in mandos_clients.iteritems():
151
 
        if client['name'] == name:
152
 
            client_objc = bus.get_object(busname, path)
153
 
            clients.append(client_objc)
154
 
            break
155
 
    else:
156
 
        print >> sys.stderr, "Client not found on server: %r" % name
157
 
        sys.exit(1)
158
 
 
159
 
if not clients and mandos_clients.values():
160
 
    keywords = defaultkeywords
161
 
    if options.all:
162
 
        keywords = ('name', 'enabled', 'timeout', 'last_checked_ok',
163
 
                    'created', 'interval', 'host', 'fingerprint',
164
 
                    'checker_running', 'last_enabled', 'checker')
165
 
    print_clients(mandos_clients.values())
166
 
 
167
 
# Process each client in the list by all selected options
168
 
for client in clients:
169
 
    if options.remove:
170
 
        mandos_serv.RemoveClient(client.__dbus_object_path__)
171
 
    if options.enable:
172
 
        client.Enable(dbus_interface=client_interface)
173
 
    if options.disable:
174
 
        client.Disable(dbus_interface=client_interface)
175
 
    if options.bump_timeout:
176
 
        client.CheckedOK(dbus_interface=client_interface)
177
 
    if options.start_checker:
178
 
        client.StartChecker(dbus_interface=client_interface)
179
 
    if options.stop_checker:
180
 
        client.StopChecker(dbus_interface=client_interface)
181
 
    if options.is_valid:
182
 
        sys.exit(0 if client.Get(client_interface,
183
 
                                 u"enabled",
184
 
                                 dbus_interface=dbus.PROPERTIES_IFACE)
185
 
                 else 1)
186
 
    if options.checker:
187
 
        client.Set(client_interface, u"checker", options.checker,
188
 
                   dbus_interface=dbus.PROPERTIES_IFACE)
189
 
    if options.host:
190
 
        client.Set(client_interface, u"host", options.host,
191
 
                   dbus_interface=dbus.PROPERTIES_IFACE)
192
 
    if options.interval:
193
 
        client.Set(client_interface, u"interval",
194
 
                   timedelta_to_milliseconds
195
 
                   (string_to_delta(options.interval)),
196
 
                   dbus_interface=dbus.PROPERTIES_IFACE)
197
 
    if options.timeout:
198
 
        client.Set(client_interface, u"timeout",
199
 
                   timedelta_to_milliseconds(string_to_delta
200
 
                                             (options.timeout)),
201
 
                   dbus_interface=dbus.PROPERTIES_IFACE)
202
 
    if options.secret:
203
 
        client.Set(client_interface, u"secret",
204
 
                   dbus.ByteArray(open(options.secret, u'rb').read()),
205
 
                   dbus_interface=dbus.PROPERTIES_IFACE)
 
256
        print(format_string
 
257
              .format(**{key: valuetostring(client[key], key)
 
258
                         for key in keywords}))
 
259
 
 
260
 
 
261
def has_actions(options):
 
262
    return any((options.enable,
 
263
                options.disable,
 
264
                options.bump_timeout,
 
265
                options.start_checker,
 
266
                options.stop_checker,
 
267
                options.is_enabled,
 
268
                options.remove,
 
269
                options.checker is not None,
 
270
                options.timeout is not None,
 
271
                options.extended_timeout is not None,
 
272
                options.interval is not None,
 
273
                options.approved_by_default is not None,
 
274
                options.approval_delay is not None,
 
275
                options.approval_duration is not None,
 
276
                options.host is not None,
 
277
                options.secret is not None,
 
278
                options.approve,
 
279
                options.deny))
 
280
 
 
281
 
 
282
def main():
 
283
    parser = argparse.ArgumentParser()
 
284
    parser.add_argument("--version", action="version",
 
285
                        version="%(prog)s {}".format(version),
 
286
                        help="show version number and exit")
 
287
    parser.add_argument("-a", "--all", action="store_true",
 
288
                        help="Select all clients")
 
289
    parser.add_argument("-v", "--verbose", action="store_true",
 
290
                        help="Print all fields")
 
291
    parser.add_argument("-j", "--dump-json", action="store_true",
 
292
                        help="Dump client data in JSON format")
 
293
    parser.add_argument("-e", "--enable", action="store_true",
 
294
                        help="Enable client")
 
295
    parser.add_argument("-d", "--disable", action="store_true",
 
296
                        help="disable client")
 
297
    parser.add_argument("-b", "--bump-timeout", action="store_true",
 
298
                        help="Bump timeout for client")
 
299
    parser.add_argument("--start-checker", action="store_true",
 
300
                        help="Start checker for client")
 
301
    parser.add_argument("--stop-checker", action="store_true",
 
302
                        help="Stop checker for client")
 
303
    parser.add_argument("-V", "--is-enabled", action="store_true",
 
304
                        help="Check if client is enabled")
 
305
    parser.add_argument("-r", "--remove", action="store_true",
 
306
                        help="Remove client")
 
307
    parser.add_argument("-c", "--checker",
 
308
                        help="Set checker command for client")
 
309
    parser.add_argument("-t", "--timeout",
 
310
                        help="Set timeout for client")
 
311
    parser.add_argument("--extended-timeout",
 
312
                        help="Set extended timeout for client")
 
313
    parser.add_argument("-i", "--interval",
 
314
                        help="Set checker interval for client")
 
315
    parser.add_argument("--approve-by-default", action="store_true",
 
316
                        default=None, dest="approved_by_default",
 
317
                        help="Set client to be approved by default")
 
318
    parser.add_argument("--deny-by-default", action="store_false",
 
319
                        dest="approved_by_default",
 
320
                        help="Set client to be denied by default")
 
321
    parser.add_argument("--approval-delay",
 
322
                        help="Set delay before client approve/deny")
 
323
    parser.add_argument("--approval-duration",
 
324
                        help="Set duration of one client approval")
 
325
    parser.add_argument("-H", "--host", help="Set host for client")
 
326
    parser.add_argument("-s", "--secret",
 
327
                        type=argparse.FileType(mode="rb"),
 
328
                        help="Set password blob (file) for client")
 
329
    parser.add_argument("-A", "--approve", action="store_true",
 
330
                        help="Approve any current client request")
 
331
    parser.add_argument("-D", "--deny", action="store_true",
 
332
                        help="Deny any current client request")
 
333
    parser.add_argument("--check", action="store_true",
 
334
                        help="Run self-test")
 
335
    parser.add_argument("client", nargs="*", help="Client name")
 
336
    options = parser.parse_args()
 
337
 
 
338
    if has_actions(options) and not (options.client or options.all):
 
339
        parser.error("Options require clients names or --all.")
 
340
    if options.verbose and has_actions(options):
 
341
        parser.error("--verbose can only be used alone.")
 
342
    if options.dump_json and (options.verbose
 
343
                              or has_actions(options)):
 
344
        parser.error("--dump-json can only be used alone.")
 
345
    if options.all and not has_actions(options):
 
346
        parser.error("--all requires an action.")
 
347
 
 
348
    if options.check:
 
349
        import doctest
 
350
        fail_count, test_count = doctest.testmod()
 
351
        sys.exit(os.EX_OK if fail_count == 0 else 1)
 
352
 
 
353
    try:
 
354
        bus = dbus.SystemBus()
 
355
        mandos_dbus_objc = bus.get_object(busname, server_path)
 
356
    except dbus.exceptions.DBusException:
 
357
        print("Could not connect to Mandos server", file=sys.stderr)
 
358
        sys.exit(1)
 
359
 
 
360
    mandos_serv = dbus.Interface(mandos_dbus_objc,
 
361
                                 dbus_interface=server_interface)
 
362
    mandos_serv_object_manager = dbus.Interface(
 
363
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
 
364
 
 
365
    # block stderr since dbus library prints to stderr
 
366
    null = os.open(os.path.devnull, os.O_RDWR)
 
367
    stderrcopy = os.dup(sys.stderr.fileno())
 
368
    os.dup2(null, sys.stderr.fileno())
 
369
    os.close(null)
 
370
    try:
 
371
        try:
 
372
            mandos_clients = {path: ifs_and_props[client_interface]
 
373
                              for path, ifs_and_props in
 
374
                              mandos_serv_object_manager
 
375
                              .GetManagedObjects().items()
 
376
                              if client_interface in ifs_and_props}
 
377
        finally:
 
378
            # restore stderr
 
379
            os.dup2(stderrcopy, sys.stderr.fileno())
 
380
            os.close(stderrcopy)
 
381
    except dbus.exceptions.DBusException as e:
 
382
        print("Access denied: "
 
383
              "Accessing mandos server through D-Bus: {}".format(e),
 
384
              file=sys.stderr)
 
385
        sys.exit(1)
 
386
 
 
387
    # Compile dict of (clients: properties) to process
 
388
    clients = {}
 
389
 
 
390
    if options.all or not options.client:
 
391
        clients = {bus.get_object(busname, path): properties
 
392
                   for path, properties in mandos_clients.items()}
 
393
    else:
 
394
        for name in options.client:
 
395
            for path, client in mandos_clients.items():
 
396
                if client["Name"] == name:
 
397
                    client_objc = bus.get_object(busname, path)
 
398
                    clients[client_objc] = client
 
399
                    break
 
400
            else:
 
401
                print("Client not found on server: {!r}"
 
402
                      .format(name), file=sys.stderr)
 
403
                sys.exit(1)
 
404
 
 
405
    if not has_actions(options) and clients:
 
406
        if options.verbose or options.dump_json:
 
407
            keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
 
408
                        "Created", "Interval", "Host", "KeyID",
 
409
                        "Fingerprint", "CheckerRunning",
 
410
                        "LastEnabled", "ApprovalPending",
 
411
                        "ApprovedByDefault", "LastApprovalRequest",
 
412
                        "ApprovalDelay", "ApprovalDuration",
 
413
                        "Checker", "ExtendedTimeout", "Expires",
 
414
                        "LastCheckerStatus")
 
415
        else:
 
416
            keywords = defaultkeywords
 
417
 
 
418
        if options.dump_json:
 
419
            json.dump({client["Name"]: {key:
 
420
                                        bool(client[key])
 
421
                                        if isinstance(client[key],
 
422
                                                      dbus.Boolean)
 
423
                                        else client[key]
 
424
                                        for key in keywords}
 
425
                       for client in clients.values()},
 
426
                      fp=sys.stdout, indent=4,
 
427
                      separators=(',', ': '))
 
428
            print()
 
429
        else:
 
430
            print_clients(clients.values(), keywords)
 
431
    else:
 
432
        # Process each client in the list by all selected options
 
433
        for client in clients:
 
434
 
 
435
            def set_client_prop(prop, value):
 
436
                """Set a Client D-Bus property"""
 
437
                client.Set(client_interface, prop, value,
 
438
                           dbus_interface=dbus.PROPERTIES_IFACE)
 
439
 
 
440
            def set_client_prop_ms(prop, value):
 
441
                """Set a Client D-Bus property, converted
 
442
                from a string to milliseconds."""
 
443
                set_client_prop(prop,
 
444
                                string_to_delta(value).total_seconds()
 
445
                                * 1000)
 
446
 
 
447
            if options.remove:
 
448
                mandos_serv.RemoveClient(client.__dbus_object_path__)
 
449
            if options.enable:
 
450
                set_client_prop("Enabled", dbus.Boolean(True))
 
451
            if options.disable:
 
452
                set_client_prop("Enabled", dbus.Boolean(False))
 
453
            if options.bump_timeout:
 
454
                set_client_prop("LastCheckedOK", "")
 
455
            if options.start_checker:
 
456
                set_client_prop("CheckerRunning", dbus.Boolean(True))
 
457
            if options.stop_checker:
 
458
                set_client_prop("CheckerRunning", dbus.Boolean(False))
 
459
            if options.is_enabled:
 
460
                if client.Get(client_interface, "Enabled",
 
461
                              dbus_interface=dbus.PROPERTIES_IFACE):
 
462
                    sys.exit(0)
 
463
                else:
 
464
                    sys.exit(1)
 
465
            if options.checker is not None:
 
466
                set_client_prop("Checker", options.checker)
 
467
            if options.host is not None:
 
468
                set_client_prop("Host", options.host)
 
469
            if options.interval is not None:
 
470
                set_client_prop_ms("Interval", options.interval)
 
471
            if options.approval_delay is not None:
 
472
                set_client_prop_ms("ApprovalDelay",
 
473
                                   options.approval_delay)
 
474
            if options.approval_duration is not None:
 
475
                set_client_prop_ms("ApprovalDuration",
 
476
                                   options.approval_duration)
 
477
            if options.timeout is not None:
 
478
                set_client_prop_ms("Timeout", options.timeout)
 
479
            if options.extended_timeout is not None:
 
480
                set_client_prop_ms("ExtendedTimeout",
 
481
                                   options.extended_timeout)
 
482
            if options.secret is not None:
 
483
                set_client_prop("Secret",
 
484
                                dbus.ByteArray(options.secret.read()))
 
485
            if options.approved_by_default is not None:
 
486
                set_client_prop("ApprovedByDefault",
 
487
                                dbus.Boolean(options
 
488
                                             .approved_by_default))
 
489
            if options.approve:
 
490
                client.Approve(dbus.Boolean(True),
 
491
                               dbus_interface=client_interface)
 
492
            elif options.deny:
 
493
                client.Approve(dbus.Boolean(False),
 
494
                               dbus_interface=client_interface)
 
495
 
 
496
 
 
497
if __name__ == "__main__":
 
498
    main()