Refactored authentication handling

- created helper routine that will do authentication;

 - routine tries each method in turn, first successful
   one terminates it: makes things easier to read
   and handle;

 - renamed plainauth() inside offlineimap/imapserver.py
   to loginauth(): the function does IMAP LOGIN authentication
   and there is PLAIN SASL method, so previous name was
   a bit misleading;

 - slightly improved error reporting: all exceptions during
   authentication will be reported at the end of the run;

 - now loginauth() is never called if LOGINDISABLED is advertized
   by the server; it used to be invoked unconditionally when
   CRAM-MD5 fails, but we should respect server's opinion on
   how to handle its users.

Signed-off-by: Eygene Ryabinkin <rea@codelabs.ru>
This commit is contained in:
Eygene Ryabinkin 2013-08-06 01:10:10 +04:00
parent 1184b6c1a3
commit 7d313f49dc

View File

@ -128,8 +128,9 @@ class IMAPServer:
self.ui.debug('imap', 'md5handler: returning %s' % retval) self.ui.debug('imap', 'md5handler: returning %s' % retval)
return retval return retval
def plainauth(self, imapobj): def loginauth(self, imapobj):
self.ui.debug('imap', 'Attempting plain authentication') """ Basic authentication via LOGIN command """
self.ui.debug('imap', 'Attempting IMAP LOGIN authentication')
imapobj.login(self.username, self.getpassword()) imapobj.login(self.username, self.getpassword())
def gssauth(self, response): def gssauth(self, response):
@ -159,6 +160,107 @@ class IMAPServer:
response = '' response = ''
return base64.b64decode(response) return base64.b64decode(response)
def _authn_helper(self, imapobj):
"""
Authentication machinery for self.acquireconnection().
Raises OfflineImapError() of type ERROR.REPO when
there are either fatal problems or no authentications
succeeded.
If any authentication method succeeds, routine should exit:
warnings for failed methods are to be produced in the
respective except blocks.
"""
# Stack stores pairs of (method name, exception)
exc_stack = []
tried_to_authn = False
# Try GSSAPI and continue if it fails
if 'AUTH=GSSAPI' in imapobj.capabilities and have_gss:
self.connectionlock.acquire()
self.ui.debug('imap', 'Attempting GSSAPI authentication')
tried_to_authn = True
try:
imapobj.authenticate('GSSAPI', self.gssauth)
except imapobj.error as e:
self.gssapi = False
self.ui.warn('GSSAPI authentication failed: %s' % e)
exc_stack.append(('GSSAPI', e))
else:
self.gssapi = True
kerberos.authGSSClientClean(self.gss_vc)
self.gss_vc = None
self.gss_step = self.GSS_STATE_STEP
#if we do self.password = None then the next attempt cannot try...
#self.password = None
return
finally:
self.connectionlock.release()
# Fire up TLS if we can and asked to: gonna to authenticate
# via plaintext or hashed schemes, so it is best to have
# channel that is protected from eavesdropping.
if 'STARTTLS' in imapobj.capabilities and not self.usessl:
self.ui.debug('imap', 'Using STARTTLS connection')
try:
imapobj.starttls()
except imapobj.error as e:
raise OfflineImapError("Failed to start "
"TLS connection: %s" % str(e),
OfflineImapError.ERROR.REPO)
if 'AUTH=CRAM-MD5' in imapobj.capabilities:
tried_to_authn = True
self.ui.debug('imap', 'Attempting '
'CRAM-MD5 authentication')
try:
imapobj.authenticate('CRAM-MD5', self.md5handler)
return
except imapobj.error as e:
self.ui.warn('CRAM-MD5 authentication failed: %s' % e)
exc_stack.append(('CRAM-MD5', e))
# Last resort: use LOGIN command,
# unless LOGINDISABLED is advertized (RFC 2595)
if 'LOGINDISABLED' in imapobj.capabilities:
e = OfflineImapError("IMAP LOGIN is "
"disabled by server. Need to use SSL?",
OfflineImapError.ERROR.REPO)
exc_stack.append(('IMAP LOGIN', e))
else:
tried_to_authn = True
self.ui.debug('imap', 'Attempting '
'IMAP LOGIN authentication')
try:
self.loginauth(imapobj)
return
except imapobj.error as e:
self.ui.warn('IMAP LOGIN authentication failed: %s' % e)
exc_stack.append(('IMAP LOGIN', e))
if len(exc_stack):
msg = "\n\t".join(map(
lambda x: ": ".join((x[0], str(x[1]))),
exc_stack
))
raise OfflineImapError("All authentication types "
"failed:\n\t%s" % msg, OfflineImapError.ERROR.REPO)
if not tried_to_authn:
methods = ", ".join(map(
lambda x: x[5:], filter(lambda x: x[0:5] == "AUTH=",
imapobj.capabilities)
))
raise OfflineImapError("No supported "
"authentication mechanisms found; "
"server advertises %s" % methods,
OfflineImapError.ERROR.REPO)
def acquireconnection(self): def acquireconnection(self):
"""Fetches a connection from the pool, making sure to create a new one """Fetches a connection from the pool, making sure to create a new one
if needed, to obey the maximum connection limits, etc. if needed, to obey the maximum connection limits, etc.
@ -223,54 +325,11 @@ class IMAPServer:
if not self.tunnel: if not self.tunnel:
try: try:
# Try GSSAPI and continue if it fails self._authn_helper(imapobj)
if 'AUTH=GSSAPI' in imapobj.capabilities and have_gss:
self.connectionlock.acquire()
self.ui.debug('imap',
'Attempting GSSAPI authentication')
try:
imapobj.authenticate('GSSAPI', self.gssauth)
except imapobj.error as val:
self.gssapi = False
self.ui.debug('imap',
'GSSAPI Authentication failed')
else:
self.gssapi = True
kerberos.authGSSClientClean(self.gss_vc)
self.gss_vc = None
self.gss_step = self.GSS_STATE_STEP
#if we do self.password = None then the next attempt cannot try...
#self.password = None
self.connectionlock.release()
if not self.gssapi:
if 'STARTTLS' in imapobj.capabilities and not\
self.usessl:
self.ui.debug('imap',
'Using STARTTLS connection')
imapobj.starttls()
if 'AUTH=CRAM-MD5' in imapobj.capabilities:
self.ui.debug('imap',
'Attempting CRAM-MD5 authentication')
try:
imapobj.authenticate('CRAM-MD5',
self.md5handler)
except imapobj.error as val:
self.plainauth(imapobj)
else:
# Use plaintext login, unless
# LOGINDISABLED (RFC2595)
if 'LOGINDISABLED' in imapobj.capabilities:
raise OfflineImapError("Plaintext login "
"disabled by server. Need to use SSL?",
OfflineImapError.ERROR.REPO)
self.plainauth(imapobj)
# Would bail by here if there was a failure.
success = 1
self.goodpassword = self.password self.goodpassword = self.password
except imapobj.error as val: success = 1
self.passworderror = str(val) except OfflineImapError as e:
self.passworderror = str(e)
raise raise
# update capabilities after login, e.g. gmail serves different ones # update capabilities after login, e.g. gmail serves different ones