Back to index

system-config-printer  1.3.9+20120706
authconn.py
Go to the documentation of this file.
00001 #!/usr/bin/python
00002 
00003 ## Copyright (C) 2007, 2008, 2009, 2010, 2011 Red Hat, Inc.
00004 ## Author: Tim Waugh <twaugh@redhat.com>
00005 
00006 ## This program is free software; you can redistribute it and/or modify
00007 ## it under the terms of the GNU General Public License as published by
00008 ## the Free Software Foundation; either version 2 of the License, or
00009 ## (at your option) any later version.
00010 
00011 ## This program is distributed in the hope that it will be useful,
00012 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
00013 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014 ## GNU General Public License for more details.
00015 
00016 ## You should have received a copy of the GNU General Public License
00017 ## along with this program; if not, write to the Free Software
00018 ## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
00019 
00020 import threading
00021 import cups
00022 import cupspk
00023 import gobject
00024 import gtk
00025 import os
00026 from errordialogs import *
00027 from debug import *
00028 from gettext import gettext as _
00029 N_ = lambda x: x
00030 
00031 cups.require("1.9.60")
00032 class AuthDialog(gtk.Dialog):
00033     AUTH_FIELD={'username': N_("Username:"),
00034                 'password': N_("Password:"),
00035                 'domain': N_("Domain:")}
00036 
00037     def __init__ (self, title=None, parent=None,
00038                   flags=gtk.DIALOG_MODAL | gtk.DIALOG_NO_SEPARATOR,
00039                   buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
00040                            gtk.STOCK_OK, gtk.RESPONSE_OK),
00041                   auth_info_required=['username', 'password'],
00042                   allow_remember=False):
00043         if title == None:
00044             title = _("Authentication")
00045         gtk.Dialog.__init__ (self, title, parent, flags, buttons)
00046         self.auth_info_required = auth_info_required
00047         self.set_default_response (gtk.RESPONSE_OK)
00048         self.set_border_width (6)
00049         self.set_resizable (False)
00050         hbox = gtk.HBox (False, 12)
00051         hbox.set_border_width (6)
00052         image = gtk.Image ()
00053         image.set_from_stock (gtk.STOCK_DIALOG_AUTHENTICATION,
00054                               gtk.ICON_SIZE_DIALOG)
00055         image.set_alignment (0.0, 0.0)
00056         hbox.pack_start (image, False, False, 0)
00057         vbox = gtk.VBox (False, 12)
00058         self.prompt_label = gtk.Label ()
00059         vbox.pack_start (self.prompt_label, False, False, 0)
00060 
00061         num_fields = len (auth_info_required)
00062         table = gtk.Table (num_fields, 2)
00063         table.set_row_spacings (6)
00064         table.set_col_spacings (6)
00065 
00066         self.field_entry = []
00067         for i in range (num_fields):
00068             field = auth_info_required[i]
00069             label = gtk.Label (_(self.AUTH_FIELD.get (field, field)))
00070             label.set_alignment (0, 0.5)
00071             table.attach (label, 0, 1, i, i + 1)
00072             entry = gtk.Entry ()
00073             entry.set_visibility (field != 'password')
00074             table.attach (entry, 1, 2, i, i + 1, 0, 0)
00075             self.field_entry.append (entry)
00076 
00077         self.field_entry[num_fields - 1].set_activates_default (True)
00078         vbox.pack_start (table, False, False, 0)
00079         hbox.pack_start (vbox, False, False, 0)
00080         self.vbox.pack_start (hbox)
00081 
00082         if allow_remember:
00083             cb = gtk.CheckButton (_("Remember password"))
00084             cb.set_active (False)
00085             vbox.pack_start (cb)
00086             self.remember_checkbox = cb
00087 
00088         self.vbox.show_all ()
00089 
00090     def set_prompt (self, prompt):
00091         self.prompt_label.set_markup ('<span weight="bold" size="larger">' +
00092                                       prompt + '</span>')
00093         self.prompt_label.set_use_markup (True)
00094         self.prompt_label.set_alignment (0, 0)
00095         self.prompt_label.set_line_wrap (True)
00096 
00097     def set_auth_info (self, auth_info):
00098         for i in range (len (self.field_entry)):
00099             self.field_entry[i].set_text (auth_info[i])
00100 
00101     def get_auth_info (self):
00102         return map (lambda x: x.get_text (), self.field_entry)
00103 
00104     def get_remember_password (self):
00105         try:
00106             return self.remember_checkbox.get_active ()
00107         except AttributeError:
00108             return False
00109 
00110     def field_grab_focus (self, field):
00111         i = self.auth_info_required.index (field)
00112         self.field_entry[i].grab_focus ()
00113 
00114 ###
00115 ### An auth-info cache.
00116 ###
00117 class _AuthInfoCache:
00118     def __init__ (self):
00119         self.creds = dict() # by (host,port)
00120 
00121     def cache_auth_info (self, data, host=None, port=None):
00122         if port == None:
00123             port = 631
00124 
00125         self.creds[(host,port)] = data
00126 
00127     def lookup_auth_info (self, host=None, port=None):
00128         if port == None:
00129             port = 631
00130 
00131         try:
00132             return self.creds[(host,port)]
00133         except KeyError:
00134             return None
00135 
00136     def remove_auth_info (self, host=None, port=None):
00137         if port == None:
00138             port = 631
00139 
00140         try:
00141             del self.creds[(host,port)]
00142         except KeyError:
00143             return None
00144 
00145 global_authinfocache = _AuthInfoCache ()
00146 
00147 class Connection:
00148     def __init__ (self, parent=None, try_as_root=True, lock=False,
00149                   host=None, port=None, encryption=None):
00150         if host != None:
00151             cups.setServer (host)
00152         if port != None:
00153             cups.setPort (port)
00154         if encryption != None:
00155             cups.setEncryption (encryption)
00156 
00157         self._use_password = ''
00158         self._parent = parent
00159         self._try_as_root = try_as_root
00160         self._use_user = cups.getUser ()
00161         self._server = cups.getServer ()
00162         self._port = cups.getPort()
00163         self._encryption = cups.getEncryption ()
00164         self._prompt_allowed = True
00165         self._operation_stack = []
00166         self._lock = lock
00167         self._gui_event = threading.Event ()
00168 
00169         self._connect ()
00170 
00171     def _begin_operation (self, operation):
00172         debugprint ("%s: Operation += %s" % (self, repr (operation)))
00173         self._operation_stack.append (operation)
00174 
00175     def _end_operation (self):
00176         debugprint ("%s: Operation ended" % self)
00177         self._operation_stack.pop ()
00178 
00179     def _get_prompt_allowed (self, ):
00180         return self._prompt_allowed
00181 
00182     def _set_prompt_allowed (self, allowed):
00183         self._prompt_allowed = allowed
00184 
00185     def _set_lock (self, whether):
00186         self._lock = whether
00187 
00188     def _connect (self, allow_pk=True):
00189         cups.setUser (self._use_user)
00190 
00191         self._use_pk = (allow_pk and
00192                         (self._server[0] == '/' or self._server == 'localhost')
00193                         and os.getuid () != 0)
00194         if self._use_pk:
00195             create_object = cupspk.Connection
00196         else:
00197             create_object = cups.Connection
00198 
00199         self._connection = create_object (host=self._server,
00200                                             port=self._port,
00201                                             encryption=self._encryption)
00202 
00203         if self._use_pk:
00204             self._connection.set_parent(self._parent)
00205 
00206         self._user = self._use_user
00207         debugprint ("Connected as user %s" % self._user)
00208         methodtype_lambda = type (self._connection.getPrinters)
00209         methodtype_real = type (self._connection.addPrinter)
00210         for fname in dir (self._connection):
00211             if fname[0] == '_':
00212                 continue
00213             fn = getattr (self._connection, fname)
00214             if not type (fn) in [methodtype_lambda, methodtype_real]:
00215                 continue
00216             setattr (self, fname, self._make_binding (fname, fn))
00217 
00218     def _make_binding (self, fname, fn):
00219         return lambda *args, **kwds: self._authloop (fname, fn, *args, **kwds)
00220 
00221     def _authloop (self, fname, fn, *args, **kwds):
00222         self._passes = 0
00223         c = self._connection
00224         retry = False
00225         while True:
00226             try:
00227                 if self._perform_authentication () == 0:
00228                     break
00229 
00230                 if c != self._connection:
00231                     # We have reconnected.
00232                     fn = getattr (self._connection, fname)
00233                     c = self._connection
00234 
00235                 cups.setUser (self._use_user)
00236 
00237                 result = fn.__call__ (*args, **kwds)
00238 
00239                 if fname == 'adminGetServerSettings':
00240                     # Special case for a rubbish bit of API.
00241                     if result == {}:
00242                         # Authentication failed, but we aren't told that.
00243                         raise cups.IPPError (cups.IPP_NOT_AUTHORIZED, '')
00244                 break
00245             except cups.IPPError, (e, m):
00246                 if self._use_pk and m == 'pkcancel':
00247                     raise cups.IPPError (0, _("Operation canceled"))
00248 
00249                 if not self._cancel and (e == cups.IPP_NOT_AUTHORIZED or
00250                                          e == cups.IPP_FORBIDDEN or
00251                                          e == cups.IPP_AUTHENTICATION_CANCELED):
00252                     self._failed (e == cups.IPP_FORBIDDEN)
00253                 elif not self._cancel and e == cups.IPP_SERVICE_UNAVAILABLE:
00254                     if self._lock:
00255                         self._gui_event.clear ()
00256                         gobject.timeout_add (1, self._ask_retry_server_error, m)
00257                         self._gui_event.wait ()
00258                     else:
00259                         self._ask_retry_server_error (m)
00260 
00261                     if self._retry_response == gtk.RESPONSE_OK:
00262                         debugprint ("retrying operation...")
00263                         retry = True
00264                         self._passes -= 1
00265                         self._has_failed = True
00266                     else:
00267                         self._cancel = True
00268                         raise
00269                 else:
00270                     if self._cancel and not self._cannot_auth:
00271                         raise cups.IPPError (0, _("Operation canceled"))
00272 
00273                     debugprint ("%s: %s" % (e, m))
00274                     raise
00275             except cups.HTTPError, (s,):
00276                 if not self._cancel:
00277                     self._failed (s == cups.HTTP_FORBIDDEN)
00278                 else:
00279                     raise
00280 
00281         return result
00282 
00283     def _ask_retry_server_error (self, message):
00284         if self._lock:
00285             gtk.gdk.threads_enter ()
00286 
00287         try:
00288             msg = _("CUPS server error (%s)") % self._operation_stack[0]
00289         except IndexError:
00290             msg = _("CUPS server error")
00291 
00292         d = gtk.MessageDialog (self._parent,
00293                                gtk.DIALOG_MODAL |
00294                                gtk.DIALOG_DESTROY_WITH_PARENT,
00295                                gtk.MESSAGE_ERROR,
00296                                gtk.BUTTONS_NONE,
00297                                msg)
00298                                
00299         d.format_secondary_text (_("There was an error during the "
00300                                    "CUPS operation: '%s'." % message))
00301         d.add_buttons (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
00302                        _("Retry"), gtk.RESPONSE_OK)
00303         d.set_default_response (gtk.RESPONSE_OK)
00304         if self._lock:
00305             d.connect ("response", self._on_retry_server_error_response)
00306             gtk.gdk.threads_leave ()
00307         else:
00308             self._retry_response = d.run ()
00309             d.destroy ()
00310 
00311     def _on_retry_server_error_response (self, dialog, response):
00312         self._retry_response = response
00313         dialog.destroy ()
00314         self._gui_event.set ()
00315 
00316     def _failed (self, forbidden=False):
00317         self._has_failed = True
00318         self._forbidden = forbidden
00319 
00320     def _password_callback (self, prompt):
00321         debugprint ("Got password callback")
00322         if self._cancel or self._auth_called:
00323             return ''
00324 
00325         self._auth_called = True
00326         self._prompt = prompt
00327         return self._use_password
00328 
00329     def _perform_authentication (self):
00330         self._passes += 1
00331 
00332         creds = global_authinfocache.lookup_auth_info (host=self._server, port=self._port)
00333         if creds != None:
00334             if (creds[0] != 'root' or self._try_as_root):
00335                 (self._use_user, self._use_password) = creds
00336             del creds
00337 
00338         debugprint ("Authentication pass: %d" % self._passes)
00339         if self._passes == 1:
00340             # Haven't yet tried the operation.  Set the password
00341             # callback and return > 0 so we try it for the first time.
00342             self._has_failed = False
00343             self._forbidden = False
00344             self._auth_called = False
00345             self._cancel = False
00346             self._cannot_auth = False
00347             self._dialog_shown = False
00348             cups.setPasswordCB (self._password_callback)
00349             debugprint ("Authentication: password callback set")
00350             return 1
00351 
00352         debugprint ("Forbidden: %s" % self._forbidden)
00353         if not self._has_failed:
00354             # Tried the operation and it worked.  Return 0 to signal to
00355             # break out of the loop.
00356             debugprint ("Authentication: Operation successful")
00357             return 0
00358 
00359         # Reset failure flag.
00360         self._has_failed = False
00361 
00362         if self._passes >= 2:
00363             # Tried the operation without a password and it failed.
00364             if (self._try_as_root and
00365                 self._user != 'root' and
00366                 (self._server[0] == '/' or self._forbidden)):
00367                 # This is a UNIX domain socket connection so we should
00368                 # not have needed a password (or it is not a UDS but
00369                 # we got an HTTP_FORBIDDEN response), and so the
00370                 # operation must not be something that the current
00371                 # user is authorised to do.  They need to try as root,
00372                 # and supply the password.  However, to get the right
00373                 # prompt, we need to try as root but with no password
00374                 # first.
00375                 debugprint ("Authentication: Try as root")
00376                 self._use_user = 'root'
00377                 self._auth_called = False
00378                 try:
00379                     self._connect (allow_pk=False)
00380                 except RuntimeError:
00381                     raise cups.IPPError (cups.IPP_SERVICE_UNAVAILABLE,
00382                                          'server-error-service-unavailable')
00383 
00384                 return 1
00385 
00386         if not self._prompt_allowed:
00387             debugprint ("Authentication: prompting not allowed")
00388             self._cancel = True
00389             return 1
00390 
00391         if not self._auth_called:
00392             # We aren't even getting a chance to supply credentials.
00393             debugprint ("Authentication: giving up")
00394             self._cancel = True
00395             self._cannot_auth = True
00396             return 1
00397 
00398         # Reset the flag indicating whether we were given an auth callback.
00399         self._auth_called = False
00400 
00401         # If we're previously prompted, explain why we're prompting again.
00402         if self._dialog_shown:
00403             if self._lock:
00404                 self._gui_event.clear ()
00405                 gobject.timeout_add (1, self._show_not_authorized_dialog)
00406                 self._gui_event.wait ()
00407             else:
00408                 self._show_not_authorized_dialog ()
00409 
00410         if self._lock:
00411             self._gui_event.clear ()
00412             gobject.timeout_add (1, self._perform_authentication_with_dialog)
00413             self._gui_event.wait ()
00414         else:
00415             self._perform_authentication_with_dialog ()
00416 
00417         if self._cancel:
00418             debugprint ("cancelled")
00419             return -1
00420 
00421         cups.setUser (self._use_user)
00422         debugprint ("Authentication: Reconnect")
00423         try:
00424             self._connect (allow_pk=False)
00425         except RuntimeError:
00426             raise cups.IPPError (cups.IPP_SERVICE_UNAVAILABLE,
00427                                  'server-error-service-unavailable')
00428 
00429         return 1
00430 
00431     def _show_not_authorized_dialog (self):
00432         if self._lock:
00433             gtk.gdk.threads_enter ()
00434         d = gtk.MessageDialog (self._parent,
00435                                gtk.DIALOG_MODAL |
00436                                gtk.DIALOG_DESTROY_WITH_PARENT,
00437                                gtk.MESSAGE_ERROR,
00438                                gtk.BUTTONS_CLOSE)
00439         d.set_title (_("Not authorized"))
00440         d.set_markup ('<span weight="bold" size="larger">' +
00441                       _("Not authorized") + '</span>\n\n' +
00442                       _("The password may be incorrect."))
00443         if self._lock:
00444             d.connect ("response", self._on_not_authorized_dialog_response)
00445             d.show_all ()
00446             d.show_now ()
00447             gtk.gdk.threads_leave ()
00448         else:
00449             d.run ()
00450             d.destroy ()
00451 
00452     def _on_not_authorized_dialog_response (self, dialog, response):
00453         self._gui_event.set ()
00454         dialog.destroy ()
00455 
00456     def _perform_authentication_with_dialog (self):
00457         if self._lock:
00458             gtk.gdk.threads_enter ()
00459 
00460         # Prompt.
00461         if len (self._operation_stack) > 0:
00462             try:
00463                 title = _("Authentication (%s)") % self._operation_stack[0]
00464             except IndexError:
00465                 title = _("Authentication")
00466 
00467             d = AuthDialog (title=title,
00468                             parent=self._parent)
00469         else:
00470             d = AuthDialog (parent=self._parent)
00471 
00472         d.set_prompt (self._prompt)
00473         d.set_auth_info ([self._use_user, ''])
00474         d.field_grab_focus ('password')
00475         d.set_keep_above (True)
00476         d.show_all ()
00477         d.show_now ()
00478         self._dialog_shown = True
00479         if self._lock:
00480             d.connect ("response", self._on_authentication_response)
00481             gtk.gdk.threads_leave ()
00482         else:
00483             response = d.run ()
00484             self._on_authentication_response (d, response)
00485 
00486     def _on_authentication_response (self, dialog, response):
00487         (user, self._use_password) = dialog.get_auth_info ()
00488         if user != '':
00489             self._use_user = user
00490         global_authinfocache.cache_auth_info ((self._use_user,
00491                                                self._use_password),
00492                                               host=self._server,
00493                                               port=self._port)
00494         dialog.destroy ()
00495 
00496         if (response == gtk.RESPONSE_CANCEL or
00497             response == gtk.RESPONSE_DELETE_EVENT):
00498             self._cancel = True
00499 
00500         if self._lock:
00501             self._gui_event.set ()
00502 
00503 if __name__ == '__main__':
00504     # Test it out.
00505     gtk.gdk.threads_init ()
00506     from timedops import TimedOperation
00507     set_debugging (True)
00508     c = TimedOperation (Connection, args=(None,)).run ()
00509     debugprint ("Connected")
00510     c._set_lock (True)
00511     print TimedOperation (c.getFile,
00512                           args=('/admin/conf/cupsd.conf',
00513                                 '/dev/stdout')).run ()