1 """GNUmed authentication widgets.
2
3 This module contains widgets and GUI
4 functions for authenticating users.
5 """
6
7 __version__ = "$Revision: 1.45 $"
8 __author__ = "karsten.hilbert@gmx.net, H.Herb, H.Berger, R.Terry"
9 __license__ = "GPL (details at http://www.gnu.org)"
10
11
12
13 import sys, os.path, logging, re as regex
14
15
16
17 import wx
18
19
20
21 if __name__ == '__main__':
22 sys.path.insert(0, '../../')
23 from Gnumed.pycommon import gmLoginInfo, gmPG2, gmBackendListener, gmTools, gmCfg2, gmI18N
24 from Gnumed.business import gmSurgery
25 from Gnumed.wxpython import gmGuiHelpers, gmExceptionHandlingWidgets
26
27
28 _log = logging.getLogger('gm.ui')
29 _log.info(__version__)
30 _cfg = gmCfg2.gmCfgData()
31
32 try:
33 _('dummy-no-need-to-translate-but-make-epydoc-happy')
34 except NameError:
35 _ = lambda x:x
36
37
38 msg_generic = _("""
39 GNUmed database version mismatch.
40
41 This database version cannot be used with this client:
42
43 client version: %s
44 database version detected: %s
45 database version needed: %s
46
47 Currently connected to database:
48
49 host: %s
50 database: %s
51 user: %s
52 """)
53
54 msg_time_skew_fail = _("""\
55 The server and client clocks are off
56 by more than %s minutes !
57
58 You must fix the time settings before
59 you can use this database with this
60 client.
61
62 You may have to contact your
63 administrator for help.""")
64
65 msg_time_skew_warn = _("""\
66 The server and client clocks are off
67 by more than %s minutes !
68
69 You should fix the time settings.
70 Otherwise clinical data may appear to
71 have been entered at the wrong time.
72
73 You may have to contact your
74 administrator for help.""")
75
76 msg_insanity = _("""
77 There is a serious problem with the database settings:
78
79 %s
80
81 You may have to contact your administrator for help.""")
82
83 msg_fail = _("""
84 You must connect to a different database in order
85 to use the GNUmed client. You may have to contact
86 your administrator for help.""")
87
88 msg_override = _("""
89 The client will, however, continue to start up because
90 you are running a development/test version of GNUmed.
91
92 There may be schema related errors. Please report and/or
93 fix them. Do not rely on this database to work properly
94 in all cases !""")
95
96
97
98
100 """Display the login dialog and try to log into the backend.
101
102 - up to max_attempts times
103 - returns True/False
104 """
105
106 expected_hash = gmPG2.known_schema_hashes[expected_version]
107 client_version = _cfg.get(option = u'client_version')
108 global current_db_name
109 current_db_name = u'gnumed_%s' % expected_version
110
111 attempt = 0
112
113 dlg = cLoginDialog(None, -1, client_version = client_version)
114 dlg.Centre(wx.BOTH)
115
116 while attempt < max_attempts:
117
118 _log.debug('login attempt %s of %s', (attempt+1), max_attempts)
119
120 connected = False
121
122 dlg.ShowModal()
123 login = dlg.panel.GetLoginInfo()
124 if login is None:
125 _log.info("user cancelled login dialog")
126 break
127
128
129 dsn = gmPG2.make_psycopg2_dsn (
130 database = login.database,
131 host = login.host,
132 port = login.port,
133 user = login.user,
134 password = login.password
135 )
136 try:
137 conn = gmPG2.get_raw_connection(dsn = dsn, verbose = True, readonly = True)
138 connected = True
139
140 except gmPG2.cAuthenticationError, e:
141 attempt += 1
142 _log.error(u"login attempt failed: %s", e)
143 if attempt < max_attempts:
144 if (u'host=127.0.0.1' in (u'%s' % e)) or (u'host=' not in (u'%s' % e)):
145 msg = _(
146 'Unable to connect to database:\n\n'
147 '%s\n\n'
148 "Are you sure you have got a local database installed ?\n"
149 '\n'
150 "Please retry with proper credentials or cancel.\n"
151 '\n'
152 'You may also need to check the PostgreSQL client\n'
153 'authentication configuration in pg_hba.conf. For\n'
154 'details see:\n'
155 '\n'
156 'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL'
157 )
158 else:
159 msg = _(
160 "Unable to connect to database:\n\n"
161 "%s\n\n"
162 "Please retry with proper credentials or cancel.\n"
163 "\n"
164 'You may also need to check the PostgreSQL client\n'
165 'authentication configuration in pg_hba.conf. For\n'
166 'details see:\n'
167 '\n'
168 'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL'
169 )
170 msg = msg % e
171 msg = regex.sub(r'password=[^\s]+', u'password=%s' % gmTools.u_replacement_character, msg)
172 gmGuiHelpers.gm_show_error (
173 msg,
174 _('Connecting to backend')
175 )
176 del e
177 continue
178
179 except gmPG2.dbapi.OperationalError, e:
180 _log.error(u"login attempt failed: %s", e)
181 msg = _(
182 "Unable to connect to database:\n\n"
183 "%s\n\n"
184 "Please retry another backend / user / password combination !\n"
185 ) % gmPG2.extract_msg_from_pg_exception(e)
186 msg = regex.sub(r'password=[^\s]+', u'password=%s' % gmTools.u_replacement_character, msg)
187 gmGuiHelpers.gm_show_error (
188 msg,
189 _('Connecting to backend')
190 )
191 del e
192 continue
193
194
195 gmPG2.set_default_login(login = login)
196 gmPG2.set_default_client_encoding(encoding = dlg.panel.backend_profile.encoding)
197
198 compatible = gmPG2.database_schema_compatible(version = expected_version)
199 if compatible or not require_version:
200 dlg.panel.save_state()
201
202 if not compatible:
203 connected_db_version = gmPG2.get_schema_version()
204 msg = msg_generic % (
205 client_version,
206 connected_db_version,
207 expected_version,
208 gmTools.coalesce(login.host, '<localhost>'),
209 login.database,
210 login.user
211 )
212 if require_version:
213 gmGuiHelpers.gm_show_error(msg + msg_fail, _('Verifying database version'))
214 connected = False
215 continue
216 gmGuiHelpers.gm_show_info(msg + msg_override, _('Verifying database version'))
217
218
219 max_skew = 1
220 if _cfg.get(option = 'debug'):
221 max_skew = 10
222 if not gmPG2.sanity_check_time_skew(tolerance = (max_skew * 60)):
223 if _cfg.get(option = 'debug'):
224 gmGuiHelpers.gm_show_warning(msg_time_skew_warn % max_skew, _('Verifying database settings'))
225 else:
226 gmGuiHelpers.gm_show_error(msg_time_skew_fail % max_skew, _('Verifying database settings'))
227 connected = False
228 continue
229
230 sanity_level, message = gmPG2.sanity_check_database_settings()
231 if sanity_level != 0:
232 gmGuiHelpers.gm_show_error((msg_insanity % message), _('Verifying database settings'))
233 if sanity_level == 2:
234 connected = False
235 continue
236
237 gmExceptionHandlingWidgets.set_is_public_database(login.public_db)
238 gmExceptionHandlingWidgets.set_helpdesk(login.helpdesk)
239
240 listener = gmBackendListener.gmBackendListener(conn = conn)
241 break
242
243 dlg.Destroy()
244
245 return connected
246
248 if procedure is None:
249 procedure = _('<restricted procedure>')
250
251
252 if dbo_password is None:
253 pwd_gm_dbo = wx.GetPasswordFromUser (
254 message = _("""
255 [%s]
256
257 This is a restricted procedure. We need the
258 password for the GNUmed database owner.
259
260 Please enter the password for <gm-dbo>:""") % procedure,
261 caption = procedure
262 )
263 if pwd_gm_dbo == '':
264 return None
265 else:
266 pwd_gm_dbo = dbo_password
267
268
269 login = gmPG2.get_default_login()
270 dsn = gmPG2.make_psycopg2_dsn(database=login.database, host=login.host, port=login.port, user='gm-dbo', password=pwd_gm_dbo)
271 try:
272 conn = gmPG2.get_connection(dsn=dsn, readonly=False, verbose=True, pooled=False)
273 except:
274 _log.exception('cannot connect')
275 gmGuiHelpers.gm_show_error (
276 aMessage = _('Cannot connect as the GNUmed database owner <gm-dbo>.'),
277 aTitle = procedure
278 )
279 return None
280
281 return conn
282
285
287 """cLoginDialog - window holding cLoginPanel"""
288
289 - def __init__(self, parent, id, title=_("Welcome to the"), client_version=u'*** unknown ***'):
296
298 """GUI panel class that interactively gets Postgres login parameters.
299
300 It features combo boxes which "remember" any number of
301 previously entered settings.
302 """
303 - def __init__(self, parent, id,
304 pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.TAB_TRAVERSAL,
305 isDialog = 0, client_version = u'*** unknown ***'):
306 """Create login panel.
307
308 isDialog: if this panel is the main panel of a dialog, the panel will
309 resize the dialog automatically to display everything neatly
310 if isDialog is set to True
311 """
312 wx.Panel.__init__(self, parent, id, pos, size, style)
313 self.parent = parent
314
315
316
317 self.cancelled = True
318
319
320 self.isDialog = isDialog
321
322 self.topsizer = wx.BoxSizer(wx.VERTICAL)
323
324
325 paths = gmTools.gmPaths(app_name = u'gnumed', wx = wx)
326 bitmap = os.path.join(paths.system_app_data_dir, 'bitmaps', 'gnumedlogo.png')
327 try:
328 png = wx.Image(bitmap, wx.BITMAP_TYPE_PNG).ConvertToBitmap()
329 bmp = wx.StaticBitmap(self, -1, png, wx.Point(10, 10), wx.Size(png.GetWidth(), png.GetHeight()))
330 self.topsizer.Add (
331 bmp,
332 proportion = 0,
333 flag = wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL,
334 border = 10
335 )
336 except:
337 self.topsizer.Add (
338 wx.StaticText (
339 self,
340 -1,
341 label = _("Cannot find image") + bitmap,
342 style = wx.ALIGN_CENTRE
343 ),
344 proportion = 0,
345 flag = wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL,
346 border = 10
347 )
348
349 paramsbox_caption = _('"%s" (version %s)') % (gmSurgery.gmCurrentPractice().active_workplace, client_version)
350
351
352 self.paramsbox = wx.StaticBox( self, -1, paramsbox_caption, style = wx.ALIGN_CENTRE_HORIZONTAL)
353 self.paramsboxsizer = wx.StaticBoxSizer( self.paramsbox, wx.VERTICAL )
354 self.paramsbox.SetForegroundColour(wx.Colour(35, 35, 142))
355 self.paramsbox.SetFont(wx.Font(
356 pointSize = 12,
357 family = wx.SWISS,
358 style = wx.NORMAL,
359 weight = wx.BOLD,
360 underline = False
361 ))
362 self.pboxgrid = wx.FlexGridSizer(5, 2, 5, 5)
363 self.pboxgrid.AddGrowableCol(1)
364
365
366 label = wx.StaticText( self, -1, _('Log into'), wx.DefaultPosition, wx.DefaultSize, 0)
367 label.SetForegroundColour(wx.Colour(35, 35, 142))
368 self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
369 self.__backend_profiles = self.__get_backend_profiles()
370 self._CBOX_profile = wx.ComboBox (
371 self,
372 -1,
373 self.__backend_profiles.keys()[0],
374 wx.DefaultPosition,
375 size = wx.Size(150,-1),
376 choices = self.__backend_profiles.keys(),
377 style = wx.CB_READONLY
378 )
379 self.pboxgrid.Add (self._CBOX_profile, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
380
381
382 label = wx.StaticText( self, -1, _("Username"), wx.DefaultPosition, wx.DefaultSize, 0 )
383 label.SetForegroundColour(wx.Colour(35, 35, 142))
384 self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
385 self.__previously_used_accounts = self.__get_previously_used_accounts()
386 self._CBOX_user = wx.ComboBox (
387 self,
388 -1,
389 self.__previously_used_accounts[0],
390 wx.DefaultPosition,
391 wx.Size(150,-1),
392 self.__previously_used_accounts,
393 wx.CB_DROPDOWN
394 )
395 self.pboxgrid.Add( self._CBOX_user, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
396
397
398 label = wx.StaticText( self, -1, _("Password"), wx.DefaultPosition, wx.DefaultSize, 0 )
399 label.SetForegroundColour(wx.Colour(35, 35, 142))
400 self.pboxgrid.Add( label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
401 self.pwdentry = wx.TextCtrl( self, 1, '', wx.DefaultPosition, wx.Size(80,-1), wx.TE_PASSWORD )
402
403 self.pwdentry.SetFocus()
404 self.pboxgrid.Add( self.pwdentry, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
405
406
407 label = wx.StaticText(self, -1, _('Options'), wx.DefaultPosition, wx.DefaultSize, 0)
408 label.SetForegroundColour(wx.Colour(35, 35, 142))
409 self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
410 self._CHBOX_debug = wx.CheckBox(self, -1, _('&Debug mode'))
411 self._CHBOX_debug.SetToolTipString(_('Check this to run GNUmed client in debugging mode.'))
412 self.pboxgrid.Add(self._CHBOX_debug, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
413
414
415 label = wx.StaticText(self, -1, '', wx.DefaultPosition, wx.DefaultSize, 0)
416 label.SetForegroundColour(wx.Colour(35, 35, 142))
417 self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
418 self._CHBOX_slave = wx.CheckBox(self, -1, _('Enable &remote control'))
419 self._CHBOX_slave.SetToolTipString(_('Check this to run GNUmed client in slave mode for remote control.'))
420 self.pboxgrid.Add(self._CHBOX_slave, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
421
422
423
424
425
426
427
428
429
430
431
432
433
434 self.button_gridsizer = wx.GridSizer(1,3,0,0)
435
436
437
438 ID_BUTTON_LOGIN = wx.NewId()
439 button_login_ok = wx.Button(self, ID_BUTTON_LOGIN, _("&Ok"), wx.DefaultPosition, wx.DefaultSize, 0 )
440 button_login_ok.SetToolTip(wx.ToolTip(_("Proceed with login.")) )
441 button_login_ok.SetDefault()
442
443
444
445
446 ID_BUTTON_CANCEL = wx.NewId()
447 button_cancel = wx.Button(self, ID_BUTTON_CANCEL, _("&Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 )
448 button_cancel.SetToolTip(wx.ToolTip(_("Cancel Login.")) )
449
450
451
452 ID_BUTTON_HELP = wx.NewId()
453 button_help = wx.Button(self, ID_BUTTON_HELP, _("&Help"), wx.DefaultPosition, wx.DefaultSize, 0 )
454 button_help.SetToolTip(wx.ToolTip(_("Help for login screen")))
455
456
457
458 self.button_gridsizer.Add (button_help,0,wx.EXPAND|wx.ALL,5)
459 self.button_gridsizer.Add (button_login_ok,0,wx.EXPAND|wx.ALL,5)
460 self.button_gridsizer.Add (button_cancel,0,wx.EXPAND|wx.ALL,5)
461
462 self.paramsboxsizer.Add(self.pboxgrid, 1, wx.GROW|wx.ALL, 10)
463 self.topsizer.Add(self.paramsboxsizer, 1, wx.GROW|wx.ALL, 10)
464 self.topsizer.Add( self.button_gridsizer, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
465
466 self.__load_state()
467
468 self.SetAutoLayout(True)
469 self.SetSizer( self.topsizer)
470 self.topsizer.Fit( self )
471 if self.isDialog:
472 self.topsizer.SetSizeHints(parent)
473
474 wx.EVT_BUTTON(self, ID_BUTTON_HELP, self.OnHelp)
475 wx.EVT_BUTTON(self, ID_BUTTON_LOGIN, self.__on_login_button_pressed)
476 wx.EVT_BUTTON(self, ID_BUTTON_CANCEL, self.OnCancel)
477
478
479
480
482
483 accounts = gmTools.coalesce (
484 _cfg.get (
485 group = u'backend',
486 option = u'logins',
487 source_order = [
488 (u'explicit', u'extend'),
489 (u'user', u'extend'),
490 (u'workbase', u'extend')
491 ]
492 ),
493 ['any-doc']
494 )
495
496
497 return accounts
498
500 """Get server profiles from the configuration files.
501
502 1) from system-wide file
503 2) from user file
504
505 Profiles in the user file which have the same name
506 as a profile in the system file will override the
507 system file.
508 """
509
510 src_order = [
511 (u'explicit', u'extend'),
512 (u'system', u'extend'),
513 (u'user', u'extend'),
514 (u'workbase', u'extend')
515 ]
516
517 profile_names = gmTools.coalesce (
518 _cfg.get(group = u'backend', option = u'profiles', source_order = src_order),
519 []
520 )
521
522
523 src_order = [
524 (u'explicit', u'return'),
525 (u'workbase', u'return'),
526 (u'user', u'return'),
527 (u'system', u'return')
528 ]
529
530 profiles = {}
531
532 for profile_name in profile_names:
533
534
535 profile = cBackendProfile()
536 profile_section = 'profile %s' % profile_name
537
538 profile.name = profile_name
539 profile.host = gmTools.coalesce(_cfg.get(profile_section, u'host', src_order), u'').strip()
540 port = gmTools.coalesce(_cfg.get(profile_section, u'port', src_order), 5432)
541 try:
542 profile.port = int(port)
543 if profile.port < 1024:
544 raise ValueError('refusing to use priviledged port (< 1024)')
545 except ValueError:
546 _log.warning('invalid port definition: [%s], skipping profile [%s]', port, profile_name)
547 continue
548 profile.database = gmTools.coalesce(_cfg.get(profile_section, u'database', src_order), u'').strip()
549 if profile.database == u'':
550 _log.warning('database name not specified, skipping profile [%s]', profile_name)
551 continue
552 profile.encoding = gmTools.coalesce(_cfg.get(profile_section, u'encoding', src_order), u'UTF8')
553 profile.public_db = bool(_cfg.get(profile_section, u'public/open access', src_order))
554 profile.helpdesk = _cfg.get(profile_section, u'help desk', src_order)
555
556 label = u'%s (%s@%s)' % (profile_name, profile.database, profile.host)
557 profiles[label] = profile
558
559
560
561 if not (_cfg.get(option = 'debug') or current_db_name.endswith('_devel')):
562 profiles2remove = []
563 for label in profiles:
564 if profiles[label].database != current_db_name:
565 profiles2remove.append(label)
566 for label in profiles2remove:
567 del profiles[label]
568
569 if len(profiles) == 0:
570 host = u'salaam.homeunix.com'
571 label = u'public GNUmed database (%s@%s)' % (current_db_name, host)
572 profiles[label] = cBackendProfile()
573 profiles[label].name = label
574 profiles[label].host = host
575 profiles[label].port = 5432
576 profiles[label].database = current_db_name
577 profiles[label].encoding = u'UTF8'
578 profiles[label].public_db = True
579 profiles[label].helpdesk = u'http://wiki.gnumed.de'
580
581 return profiles
582
584
585 src_order = [
586 (u'explicit', u'return'),
587 (u'user', u'return'),
588 ]
589
590 self._CBOX_user.SetValue (
591 gmTools.coalesce (
592 _cfg.get(u'preferences', u'login', src_order),
593 self.__previously_used_accounts[0]
594 )
595 )
596
597 last_used_profile_label = _cfg.get(u'preferences', u'profile', src_order)
598 if last_used_profile_label in self.__backend_profiles.keys():
599 self._CBOX_profile.SetValue(last_used_profile_label)
600 else:
601 self._CBOX_profile.SetValue(self.__backend_profiles.keys()[0])
602
603 self._CHBOX_debug.SetValue(_cfg.get(option = 'debug'))
604 self._CHBOX_slave.SetValue(_cfg.get(option = 'slave'))
605
624
625
626
628 """convenience function for compatibility with gmLoginInfo.LoginInfo"""
629 if not self.cancelled:
630
631
632 profile = self.__backend_profiles[self._CBOX_profile.GetValue().encode('utf8').strip()]
633 _log.debug(u'backend profile "%s" selected', profile.name)
634 _log.debug(u' details: <%s> on %s@%s:%s (%s, %s)',
635 self._CBOX_user.GetValue(),
636 profile.database,
637 profile.host,
638 profile.port,
639 profile.encoding,
640 gmTools.bool2subst(profile.public_db, u'public', u'private')
641 )
642 _log.debug(u' helpdesk: "%s"', profile.helpdesk)
643 login = gmLoginInfo.LoginInfo (
644 user = self._CBOX_user.GetValue(),
645 password = self.pwdentry.GetValue(),
646 host = profile.host,
647 database = profile.database,
648 port = profile.port
649 )
650 login.public_db = profile.public_db
651 login.helpdesk = profile.helpdesk
652 return login
653
654 return None
655
656
657
659 praxis = gmSurgery.gmCurrentPractice()
660 wx.MessageBox(_(
661 """GNUmed main login screen
662
663 USER:
664 name of the GNUmed user
665 PASSWORD
666 password for this user
667
668 button OK:
669 proceed with login
670 button OPTIONS:
671 set advanced options
672 button CANCEL:
673 abort login and quit GNUmed client
674 button HELP:
675 this help screen
676
677 For assistance on using GNUmed please contact:
678 %s""") % praxis.helpdesk)
679
680
708
710 self.cancelled = True
711 self.parent.Close()
712
713
714
715
716 if __name__ == "__main__":
717
718 if len(sys.argv) < 2:
719 sys.exit()
720
721 if sys.argv[1] != 'test':
722 sys.exit()
723
724
725 sys.exit()
726
727 from Gnumed.pycommon import gmI18N
728
729 logging.basicConfig(level = logging.DEBUG)
730
731 gmI18N.activate_locale()
732 gmI18N.install_domain(domain='gnumed')
733
734
736 app = wx.PyWidgetTester(size = (300,400))
737
738
739
740 dlg = cLoginDialog(None, -1)
741 dlg.ShowModal()
742
743 lp = dlg.panel.GetLoginInfo()
744 if lp is None:
745 wx.MessageBox(_("Dialog was cancelled by user"))
746 else:
747 wx.MessageBox(_("You tried to log in as [%s] with password [%s].\nHost:%s, DB: %s, Port: %s") % (lp.GetUser(),lp.GetPassword(),lp.GetHost(),lp.GetDatabase(),lp.GetPort()))
748 dlg.Destroy()
749
750
751
752