/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-monitor

merge
new approve/deny functionallity in mandos-monitor

Show diffs side-by-side

added added

removed removed

Lines of Context:
4
4
from __future__ import division, absolute_import, with_statement
5
5
 
6
6
import sys
 
7
import os
7
8
import signal
8
9
 
 
10
import datetime
 
11
 
9
12
import urwid.curses_display
10
13
import urwid
11
14
 
16
19
 
17
20
import UserList
18
21
 
 
22
import locale
 
23
 
 
24
locale.setlocale(locale.LC_ALL, u'')
 
25
 
 
26
import logging
 
27
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
 
28
 
19
29
# Some useful constants
20
30
domain = 'se.bsnet.fukt'
21
31
server_interface = domain + '.Mandos'
22
32
client_interface = domain + '.Mandos.Client'
23
 
version = "1.0.14"
 
33
version = "1.0.15"
24
34
 
25
35
# Always run in monochrome mode
26
36
urwid.curses_display.curses.has_colors = lambda : False
35
45
    properties and calls a hook function when any of them are
36
46
    changed.
37
47
    """
38
 
    def __init__(self, proxy_object=None, properties=None, *args,
39
 
                 **kwargs):
40
 
        # Type conversion mapping
41
 
        self.type_map = {
42
 
            dbus.ObjectPath: unicode,
43
 
            dbus.ByteArray: str,
44
 
            dbus.Signature: unicode,
45
 
            dbus.Byte: chr,
46
 
            dbus.Int16: int,
47
 
            dbus.UInt16: int,
48
 
            dbus.Int32: int,
49
 
            dbus.UInt32: int,
50
 
            dbus.Int64: int,
51
 
            dbus.UInt64: int,
52
 
            dbus.Dictionary: dict,
53
 
            dbus.Array: list,
54
 
            dbus.String: unicode,
55
 
            dbus.Boolean: bool,
56
 
            dbus.Double: float,
57
 
            dbus.Struct: tuple,
58
 
            }
 
48
    def __init__(self, proxy_object=None, *args, **kwargs):
59
49
        self.proxy = proxy_object # Mandos Client proxy object
60
50
        
61
 
        if properties is None:
62
 
            self.properties = dict()
63
 
        else:
64
 
            self.properties = dict(self.convert_property(prop, val)
65
 
                                   for prop, val in
66
 
                                   properties.iteritems())
67
 
        self.proxy.connect_to_signal("PropertyChanged",
 
51
        self.properties = dict()
 
52
        self.proxy.connect_to_signal(u"PropertyChanged",
68
53
                                     self.property_changed,
69
54
                                     client_interface,
70
55
                                     byte_arrays=True)
71
56
        
72
 
        if properties is None:
73
 
            self.properties.update(
74
 
                self.convert_property(prop, val)
75
 
                for prop, val in
76
 
                self.proxy.GetAll(client_interface,
77
 
                                  dbus_interface =
78
 
                                  dbus.PROPERTIES_IFACE).iteritems())
79
 
        super(MandosClientPropertyCache, self).__init__(
80
 
            proxy_object=proxy_object,
81
 
            properties=properties, *args, **kwargs)
 
57
        self.properties.update(
 
58
            self.proxy.GetAll(client_interface,
 
59
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
60
 
 
61
        #XXX This break good super behaviour!
 
62
#        super(MandosClientPropertyCache, self).__init__(
 
63
#            *args, **kwargs)
82
64
    
83
 
    def convert_property(self, property, value):
84
 
        """This converts the arguments from a D-Bus signal, which are
85
 
        D-Bus types, into normal Python types, using a conversion
86
 
        function from "self.type_map".
87
 
        """
88
 
        property_name = unicode(property) # Always a dbus.String
89
 
        if isinstance(value, dbus.UTF8String):
90
 
            # Should not happen, but prepare for it anyway
91
 
            value = dbus.String(str(value).decode("utf-8"))
92
 
        try:
93
 
            convfunc = self.type_map[type(value)]
94
 
        except KeyError:
95
 
            # Unknown type, return unmodified
96
 
            return property_name, value
97
 
        return property_name, convfunc(value)
98
65
    def property_changed(self, property=None, value=None):
99
66
        """This is called whenever we get a PropertyChanged signal
100
67
        It updates the changed property in the "properties" dict.
101
68
        """
102
 
        # Convert name and value
103
 
        property_name, cvalue = self.convert_property(property, value)
104
69
        # Update properties dict with new value
105
 
        self.properties[property_name] = cvalue
 
70
        self.properties[property] = value
106
71
 
107
72
 
108
73
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
110
75
    """
111
76
    
112
77
    def __init__(self, server_proxy_object=None, update_hook=None,
113
 
                 delete_hook=None, *args, **kwargs):
 
78
                 delete_hook=None, logger=None, *args, **kwargs):
114
79
        # Called on update
115
80
        self.update_hook = update_hook
116
81
        # Called on delete
117
82
        self.delete_hook = delete_hook
118
83
        # Mandos Server proxy object
119
84
        self.server_proxy_object = server_proxy_object
 
85
        # Logger
 
86
        self.logger = logger
120
87
        
121
88
        # The widget shown normally
122
 
        self._text_widget = urwid.Text("")
 
89
        self._text_widget = urwid.Text(u"")
123
90
        # The widget shown when we have focus
124
 
        self._focus_text_widget = urwid.Text("")
 
91
        self._focus_text_widget = urwid.Text(u"")
125
92
        super(MandosClientWidget, self).__init__(
126
93
            update_hook=update_hook, delete_hook=delete_hook,
127
94
            *args, **kwargs)
128
95
        self.update()
129
96
        self.opened = False
 
97
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
98
                                     self.checker_completed,
 
99
                                     client_interface,
 
100
                                     byte_arrays=True)
 
101
        self.proxy.connect_to_signal(u"CheckerStarted",
 
102
                                     self.checker_started,
 
103
                                     client_interface,
 
104
                                     byte_arrays=True)
 
105
        self.proxy.connect_to_signal(u"GotSecret",
 
106
                                     self.got_secret,
 
107
                                     client_interface,
 
108
                                     byte_arrays=True)
 
109
        self.proxy.connect_to_signal(u"NeedApproval",
 
110
                                     self.need_approval,
 
111
                                     client_interface,
 
112
                                     byte_arrays=True)
 
113
        self.proxy.connect_to_signal(u"Rejected",
 
114
                                     self.rejected,
 
115
                                     client_interface,
 
116
                                     byte_arrays=True)
 
117
    
 
118
    def checker_completed(self, exitstatus, condition, command):
 
119
        if exitstatus == 0:
 
120
            #self.logger(u'Checker for client %s (command "%s")'
 
121
            #            u' was successful'
 
122
            #            % (self.properties[u"name"], command))
 
123
            return
 
124
        if os.WIFEXITED(condition):
 
125
            self.logger(u'Checker for client %s (command "%s")'
 
126
                        u' failed with exit code %s'
 
127
                        % (self.properties[u"name"], command,
 
128
                           os.WEXITSTATUS(condition)))
 
129
            return
 
130
        if os.WIFSIGNALED(condition):
 
131
            self.logger(u'Checker for client %s (command "%s")'
 
132
                        u' was killed by signal %s'
 
133
                        % (self.properties[u"name"], command,
 
134
                           os.WTERMSIG(condition)))
 
135
            return
 
136
        if os.WCOREDUMP(condition):
 
137
            self.logger(u'Checker for client %s (command "%s")'
 
138
                        u' dumped core'
 
139
                        % (self.properties[u"name"], command))
 
140
        self.logger(u'Checker for client %s completed mysteriously')
 
141
    
 
142
    def checker_started(self, command):
 
143
        #self.logger(u'Client %s started checker "%s"'
 
144
        #            % (self.properties[u"name"], unicode(command)))
 
145
        pass
 
146
    
 
147
    def got_secret(self):
 
148
        self.logger(u'Client %s received its secret'
 
149
                    % self.properties[u"name"])
 
150
    
 
151
    def need_approval(self, timeout, default):
 
152
        if not default:
 
153
            message = u'Client %s needs approval within %s seconds'
 
154
        else:
 
155
            message = u'Client %s will get its secret in %s seconds'
 
156
        self.logger(message
 
157
                    % (self.properties[u"name"], timeout/1000))
 
158
    
 
159
    def rejected(self, reason):
 
160
        self.logger(u'Client %s was rejected; reason: %s'
 
161
                    % (self.properties[u"name"], reason))
130
162
    
131
163
    def selectable(self):
132
164
        """Make this a "selectable" widget.
154
186
                          u"bold-underline-blink":
155
187
                              u"bold-underline-blink-standout",
156
188
                          }
157
 
        
 
189
 
158
190
        # Rebuild focus and non-focus widgets using current properties
159
 
        self._text = (u'name="%(name)s", enabled=%(enabled)s'
160
 
                      % self.properties)
 
191
 
 
192
        # Base part of a client. Name!
 
193
        self._text = (u'%(name)s: '
 
194
                      % {u"name": self.properties[u"name"]})
 
195
 
 
196
        if self.properties[u"approved_pending"]:
 
197
            if self.properties[u"approved_by_default"]:
 
198
                self._text += u"Connection established to client. (d)eny?"
 
199
            else:
 
200
                self._text += u"Seeks approval to send secret. (a)pprove?"
 
201
        else:
 
202
            self._text += (u'%(enabled)s'
 
203
                           % {u"enabled":
 
204
                               (u"enabled"
 
205
                                if self.properties[u"enabled"]
 
206
                                else u"DISABLED")})
161
207
        if not urwid.supports_unicode():
162
208
            self._text = self._text.encode("ascii", "replace")
163
 
        textlist = [(u"normal", u"BLÄRGH: "), (u"bold", self._text)]
 
209
        textlist = [(u"normal", self._text)]
164
210
        self._text_widget.set_text(textlist)
165
211
        self._focus_text_widget.set_text([(with_standout[text[0]],
166
212
                                           text[1])
187
233
    def keypress(self, (maxcol,), key):
188
234
        """Handle keys.
189
235
        This overrides the method from urwid.FlowWidget"""
190
 
        if key == u"e" or key == u"+":
191
 
            self.proxy.Enable()
192
 
        elif key == u"d" or key == u"-":
193
 
            self.proxy.Disable()
194
 
        elif key == u"r" or key == u"_":
 
236
        if key == u"+":
 
237
            self.proxy.Enable(dbus_interface = client_interface)
 
238
        elif key == u"-":
 
239
            self.proxy.Disable(dbus_interface = client_interface)
 
240
        elif key == u"a":
 
241
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
 
242
                               dbus_interface = client_interface)
 
243
        elif key == u"d":
 
244
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
 
245
                                  dbus_interface = client_interface)
 
246
        elif key == u"r" or key == u"_" or key == u"ctrl k":
195
247
            self.server_proxy_object.RemoveClient(self.proxy
196
248
                                                  .object_path)
197
249
        elif key == u"s":
198
 
            self.proxy.StartChecker()
199
 
        elif key == u"c":
200
 
            self.proxy.StopChecker()
 
250
            self.proxy.StartChecker(dbus_interface = client_interface)
201
251
        elif key == u"S":
202
 
            self.proxy.CheckedOK()
 
252
            self.proxy.StopChecker(dbus_interface = client_interface)
 
253
        elif key == u"C":
 
254
            self.proxy.CheckedOK(dbus_interface = client_interface)
203
255
        # xxx
204
256
#         elif key == u"p" or key == "=":
205
257
#             self.proxy.pause()
207
259
#             self.proxy.unpause()
208
260
#         elif key == u"RET":
209
261
#             self.open()
 
262
#        elif key == u"+":
 
263
#            self.proxy.Approve(True)
 
264
#        elif key == u"-":
 
265
#            self.proxy.Approve(False)
210
266
        else:
211
267
            return key
212
268
    
222
278
            self.update()
223
279
 
224
280
 
 
281
class ConstrainedListBox(urwid.ListBox):
 
282
    """Like a normal urwid.ListBox, but will consume all "up" or
 
283
    "down" key presses, thus not allowing any containing widgets to
 
284
    use them as an excuse to shift focus away from this widget.
 
285
    """
 
286
    def keypress(self, (maxcol, maxrow), key):
 
287
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
288
        if ret in (u"up", u"down"):
 
289
            return
 
290
        return ret
 
291
 
 
292
 
225
293
class UserInterface(object):
226
294
    """This is the entire user interface - the whole screen
227
295
    with boxes, lists of client widgets, etc.
228
296
    """
229
 
    def __init__(self):
230
 
        DBusGMainLoop(set_as_default=True )
 
297
    def __init__(self, max_log_length=1000):
 
298
        DBusGMainLoop(set_as_default=True)
231
299
        
232
300
        self.screen = urwid.curses_display.Screen()
233
301
        
251
319
                                          u"standout")),
252
320
                ))
253
321
        
 
322
        if urwid.supports_unicode():
 
323
            self.divider = u"─" # \u2500
 
324
            #self.divider = u"━" # \u2501
 
325
        else:
 
326
            #self.divider = u"-" # \u002d
 
327
            self.divider = u"_" # \u005f
 
328
        
254
329
        self.screen.start()
255
330
        
256
331
        self.size = self.screen.get_cols_rows()
257
332
        
258
333
        self.clients = urwid.SimpleListWalker([])
259
334
        self.clients_dict = {}
260
 
        self.topwidget = urwid.LineBox(urwid.ListBox(self.clients))
261
 
        #self.topwidget = urwid.ListBox(clients)
 
335
        
 
336
        # We will add Text widgets to this list
 
337
        self.log = []
 
338
        self.max_log_length = max_log_length
 
339
        
 
340
        # We keep a reference to the log widget so we can remove it
 
341
        # from the ListWalker without it getting destroyed
 
342
        self.logbox = ConstrainedListBox(self.log)
 
343
        
 
344
        # This keeps track of whether self.uilist currently has
 
345
        # self.logbox in it or not
 
346
        self.log_visible = True
 
347
        self.log_wrap = u"any"
 
348
        
 
349
        self.rebuild()
 
350
        self.log_message_raw((u"bold",
 
351
                              u"Mandos Monitor version " + version))
 
352
        self.log_message_raw((u"bold",
 
353
                              u"q: Quit  ?: Help"))
262
354
        
263
355
        self.busname = domain + '.Mandos'
264
356
        self.main_loop = gobject.MainLoop()
275
367
            mandos_clients = dbus.Dictionary()
276
368
        
277
369
        (self.mandos_serv
278
 
         .connect_to_signal("ClientRemoved",
 
370
         .connect_to_signal(u"ClientRemoved",
279
371
                            self.find_and_remove_client,
280
372
                            dbus_interface=server_interface,
281
373
                            byte_arrays=True))
282
374
        (self.mandos_serv
283
 
         .connect_to_signal("ClientAdded",
 
375
         .connect_to_signal(u"ClientAdded",
284
376
                            self.add_new_client,
285
377
                            dbus_interface=server_interface,
286
378
                            byte_arrays=True))
287
 
        for path, client in (mandos_clients.iteritems()):
 
379
        (self.mandos_serv
 
380
         .connect_to_signal(u"ClientNotFound",
 
381
                            self.client_not_found,
 
382
                            dbus_interface=server_interface,
 
383
                            byte_arrays=True))
 
384
        for path, client in mandos_clients.iteritems():
288
385
            client_proxy_object = self.bus.get_object(self.busname,
289
386
                                                      path)
290
387
            self.add_client(MandosClientWidget(server_proxy_object
295
392
                                               update_hook
296
393
                                               =self.refresh,
297
394
                                               delete_hook
298
 
                                               =self.remove_client),
 
395
                                               =self.remove_client,
 
396
                                               logger
 
397
                                               =self.log_message),
299
398
                            path=path)
300
399
    
 
400
    def client_not_found(self, fingerprint, address):
 
401
        self.log_message((u"Client with address %s and fingerprint %s"
 
402
                          u" could not be found" % (address,
 
403
                                                    fingerprint)))
 
404
    
 
405
    def rebuild(self):
 
406
        """This rebuilds the User Interface.
 
407
        Call this when the widget layout needs to change"""
 
408
        self.uilist = []
 
409
        #self.uilist.append(urwid.ListBox(self.clients))
 
410
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
411
                                       #header=urwid.Divider(),
 
412
                                       header=None,
 
413
                                       footer=urwid.Divider(div_char=self.divider)))
 
414
        if self.log_visible:
 
415
            self.uilist.append(self.logbox)
 
416
            pass
 
417
        self.topwidget = urwid.Pile(self.uilist)
 
418
    
 
419
    def log_message(self, message):
 
420
        timestamp = datetime.datetime.now().isoformat()
 
421
        self.log_message_raw(timestamp + u": " + message)
 
422
    
 
423
    def log_message_raw(self, markup):
 
424
        """Add a log message to the log buffer."""
 
425
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
426
        if (self.max_log_length
 
427
            and len(self.log) > self.max_log_length):
 
428
            del self.log[0:len(self.log)-self.max_log_length-1]
 
429
        self.logbox.set_focus(len(self.logbox.body.contents),
 
430
                              coming_from=u"above")
 
431
        self.refresh()
 
432
    
 
433
    def toggle_log_display(self):
 
434
        """Toggle visibility of the log buffer."""
 
435
        self.log_visible = not self.log_visible
 
436
        self.rebuild()
 
437
        self.log_message(u"Log visibility changed to: "
 
438
                         + unicode(self.log_visible))
 
439
    
 
440
    def change_log_display(self):
 
441
        """Change type of log display.
 
442
        Currently, this toggles wrapping of text lines."""
 
443
        if self.log_wrap == u"clip":
 
444
            self.log_wrap = u"any"
 
445
        else:
 
446
            self.log_wrap = u"clip"
 
447
        for textwidget in self.log:
 
448
            textwidget.set_wrap_mode(self.log_wrap)
 
449
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
450
    
301
451
    def find_and_remove_client(self, path, name):
302
452
        """Find an client from its object path and remove it.
303
453
        
310
460
            return
311
461
        self.remove_client(client, path)
312
462
    
313
 
    def add_new_client(self, path, properties):
 
463
    def add_new_client(self, path):
314
464
        client_proxy_object = self.bus.get_object(self.busname, path)
315
465
        self.add_client(MandosClientWidget(server_proxy_object
316
466
                                           =self.mandos_serv,
317
467
                                           proxy_object
318
468
                                           =client_proxy_object,
319
 
                                           properties=properties,
320
469
                                           update_hook
321
470
                                           =self.refresh,
322
471
                                           delete_hook
323
 
                                           =self.remove_client),
 
472
                                           =self.remove_client,
 
473
                                           logger
 
474
                                           =self.log_message),
324
475
                        path=path)
325
476
    
326
477
    def add_client(self, client, path=None):
336
487
        if path is None:
337
488
            path = client.proxy.object_path
338
489
        del self.clients_dict[path]
 
490
        if not self.clients_dict:
 
491
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
492
            # is completely emptied, we need to recreate it.
 
493
            self.clients = urwid.SimpleListWalker([])
 
494
            self.rebuild()
339
495
        self.refresh()
340
496
    
341
497
    def refresh(self):
360
516
    
361
517
    def process_input(self, source, condition):
362
518
        keys = self.screen.get_input()
363
 
        translations = { u"j": u"down",
364
 
                         u"k": u"up",
 
519
        translations = { u"ctrl n": u"down",      # Emacs
 
520
                         u"ctrl p": u"up",        # Emacs
 
521
                         u"ctrl v": u"page down", # Emacs
 
522
                         u"meta v": u"page up",   # Emacs
 
523
                         u" ": u"page down",      # less
 
524
                         u"f": u"page down",      # less
 
525
                         u"b": u"page up",        # less
 
526
                         u"j": u"down",           # vi
 
527
                         u"k": u"up",             # vi
365
528
                         }
366
529
        for key in keys:
367
530
            try:
375
538
            elif key == u"window resize":
376
539
                self.size = self.screen.get_cols_rows()
377
540
                self.refresh()
378
 
            elif key == " ":
379
 
                self.refresh()
 
541
            elif key == u"\f":  # Ctrl-L
 
542
                self.refresh()
 
543
            elif key == u"l" or key == u"D":
 
544
                self.toggle_log_display()
 
545
                self.refresh()
 
546
            elif key == u"w" or key == u"i":
 
547
                self.change_log_display()
 
548
                self.refresh()
 
549
            elif key == u"?" or key == u"f1" or key == u"esc":
 
550
                if not self.log_visible:
 
551
                    self.log_visible = True
 
552
                    self.rebuild()
 
553
                self.log_message_raw((u"bold",
 
554
                                      u"  ".
 
555
                                      join((u"q: Quit",
 
556
                                            u"?: Help",
 
557
                                            u"l: Log window toggle",
 
558
                                            u"TAB: Switch window",
 
559
                                            u"w: Wrap (log)"))))
 
560
                self.log_message_raw((u"bold",
 
561
                                      u"  "
 
562
                                      .join((u"Clients:",
 
563
                                             u"e: Enable",
 
564
                                             u"d: Disable",
 
565
                                             u"r: Remove",
 
566
                                             u"s: Start new checker",
 
567
                                             u"S: Stop checker",
 
568
                                             u"C: Checker OK",
 
569
                                             u"A: Approve",
 
570
                                             u"D: Deny"))))
 
571
                self.refresh()
 
572
            elif key == u"tab":
 
573
                if self.topwidget.get_focus() is self.logbox:
 
574
                    self.topwidget.set_focus(0)
 
575
                else:
 
576
                    self.topwidget.set_focus(self.logbox)
 
577
                self.refresh()
 
578
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
579
            #      or key == u">"):
 
580
            #    pass            # xxx end-of-buffer
 
581
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
582
            #      or key == u"<"):
 
583
            #    pass            # xxx beginning-of-buffer
 
584
            #elif key == u"ctrl e" or key == u"$":
 
585
            #    pass            # xxx move-end-of-line
 
586
            #elif key == u"ctrl a" or key == u"^":
 
587
            #    pass            # xxx move-beginning-of-line
 
588
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
589
            #    pass            # xxx left
 
590
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
591
            #    pass            # xxx right
 
592
            #elif key == u"a":
 
593
            #    pass            # scroll up log
 
594
            #elif key == u"z":
 
595
            #    pass            # scroll down log
380
596
            elif self.topwidget.selectable():
381
597
                self.topwidget.keypress(self.size, key)
382
598
                self.refresh()
385
601
ui = UserInterface()
386
602
try:
387
603
    ui.run()
388
 
except:
 
604
except Exception, e:
 
605
    ui.log_message(unicode(e))
389
606
    ui.screen.stop()
390
607
    raise