changeset 2025:d919b7b7b3e9

auth framework: login() methods return an object now This avoids overloading the returned tuple and simplifies code since they can now just instantiate ContinueLogin(), CancelLogin() etc.
author Johannes Berg <johannes AT sipsolutions DOT net>
date Tue, 24 Apr 2007 12:04:19 +0200
parents b73858101d66
children 5aa49f81db45
files MoinMoin/auth/__init__.py MoinMoin/auth/interwiki.py MoinMoin/auth/ldap_login.py MoinMoin/auth/log.py MoinMoin/auth/mysql_group.py MoinMoin/auth/smb_mount.py MoinMoin/request/__init__.py
diffstat 7 files changed, 136 insertions(+), 52 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/auth/__init__.py	Mon Apr 23 23:28:03 2007 +0200
+++ b/MoinMoin/auth/__init__.py	Tue Apr 24 12:04:19 2007 +0200
@@ -28,21 +28,30 @@
     and 'continue' is a boolean to indicate whether the next authentication
     method should be tried.
 
-    The 'login' method must return a tuple
-        (user_obj, continue, multistage, message).
+    The 'login' method must return an instance of MoinMoin.auth.LoginReturn
+    which contains the members
+      * user_obj
+      * continue_flag
+      * multistage
+      * message
+      * redirect_to
 
-    The user_obj and continue values have the same semantics as for the request
-    and logout methods.
+    There are some helpful subclasses derived from this class for the most
+    common cases, namely ContinueLogin(), CancelLogin(), MultistageFormLogin()
+    and MultistageRedirectLogin().
+
+    The user_obj and continue_flag members have the same semantics as for the
+    request and logout methods.
 
     The messages that are returned by the various auth methods will be
     displayed to the user, since they will all be displayed usually auth
     methods will use the message feature only along with returning False for
     the continue flag.
 
-    The multistage item in the tuple must evaluate to false or be callable.
-    If it is callable, this indicates that the authentication method requires
-    a second login stage. In that case, the multistage item will be called
-    with the request as the only parameter. It should return an instance of
+    The multistage member must evaluate to false or be callable. If it is
+    callable, this indicates that the authentication method requires a second
+    login stage. In that case, the multistage item will be called with the
+    request as the only parameter. It should return an instance of
     MoinMoin.widget.html.FORM and the generic code will append some required
     hidden fields to it. It is also permissible to return some valid HTML,
     but that feature has very limited use since it breaks the authentication
@@ -54,10 +63,18 @@
     methods should take care to recheck everything and not assume the user
     has gone through all previous stages.
 
-    After the user has submitted the required form, execution of the auth
-    login methods resumes with the auth item that requested the multistage
-    login and its login method is called with the 'multistage' keyword
-    parameter set to True.
+    If the multistage login requires querying an external site that involves
+    a redirect, the redirect_to member may be set instead of the multistage
+    member. If this is set it must be a URL that user should be redirected to.
+    Since the user must be able to come back to the authentication, any
+    "%return" in the URL is replaced with the url-encoded form of the URL
+    to the next authentication stage, any "%return_form" is replaced with
+    the url-plus-encoded form (spaces encoded as +) of the same URL.
+
+    After the user has submitted the required form or has been redirected back
+    from the external site, execution of the auth login methods resumes with
+    the auth item that requested the multistage login and its login method is
+    called with the 'multistage' keyword parameter set to True.
 
     Each authentication method instance must also contain the members
      * login_inputs: a list of required inputs, currently supported are
@@ -93,7 +110,60 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-from MoinMoin import user
+from MoinMoin import user, wikiutil
+
+
+def get_multistage_continuation_url(request, auth_name, extra_fields={}):
+    """get_continuation_url - return a multistage continuation URL
+
+       This function returns a URL that when loaded continues a multistage
+       authentication at the auth method requesting it (parameter auth_name.)
+       Additional fields are added to the URL from the extra_fields dict.
+
+       @param request: the Moin request
+       @param auth_name: name of the auth method requesting the continuation
+       @param extra_fields: extra GET fields to add to the URL
+    """
+    # logically, this belongs to request, but semantically it should
+    # live in auth so people do auth.get_multistage_continuation_url()
+    fields = {'action': 'login',
+              'login': '1',
+              'stage': auth_name}
+    fields.update(extra_fields)
+    qstr = wikiutil.makeQueryString(fields)
+    return ''.join([request.getBaseURL(), '?', qstr])
+
+
+class LoginReturn(object):
+    """ LoginReturn - base class for auth method login() return value"""
+    def __init__(self, user_obj, continue_flag, message=None, multistage=None,
+                 redirect_to=None):
+        self.user_obj = user_obj
+        self.continue_flag = continue_flag
+        self.message = message
+        self.multistage = multistage
+        self.redirect_to = redirect_to
+
+class ContinueLogin(LoginReturn):
+    """ ContinueLogin - helper for auth method login that just continues """
+    def __init__(self, user_obj, message=None):
+        LoginReturn.__init__(self, user_obj, True, message=message)
+
+class CancelLogin(LoginReturn):
+    """ CancelLogin - cancel login showing a message """
+    def __init__(self, message):
+        LoginReturn.__init__(self, None, False, message=message)
+
+class MultistageFormLogin(LoginReturn):
+    """ MultistageFormLogin - require user to fill in another form """
+    def __init__(self, multistage):
+        LoginReturn.__init__(self, None, False, multistage=multistage)
+
+class MultistageRedirectLogin(LoginReturn):
+    """ MultistageRedirectLogin - redirect user to another site before continuing login """
+    def __init__(self, url):
+        LoginReturn.__init__(self, None, False, redirect_to=url)
+
 
 class BaseAuth:
     name = None
@@ -102,7 +172,7 @@
     def __init__(self):
         pass
     def login(self, request, user_obj, **kw):
-        return user_obj, True, None, None
+        return ContinueLogin(user_obj)
     def request(self, request, user_obj, **kw):
         return user_obj, True
     def logout(self, request, user_obj, **kw):
@@ -126,10 +196,10 @@
 
         # simply continue if something else already logged in successfully
         if user_obj and user_obj.valid:
-            return user_obj, True, None, None
+            return ContinueLogin(user_obj)
 
         if not username and not password:
-            return user_obj, True, None, None
+            return ContinueLogin(user_obj)
 
         _ = request.getText
 
@@ -138,12 +208,12 @@
         if verbose: request.log("moin_login performing login action")
 
         if username and not password:
-            return user_obj, True, None, _('Missing password. Please enter user name and password.')
+            return ContinueLogin(user_obj, _('Missing password. Please enter user name and password.'))
 
         u = user.User(request, name=username, password=password, auth_method=self.name)
         if u.valid:
             if verbose: request.log("moin_login got valid user...")
-            return u, True, None, None
+            return ContinueLogin(u)
         else:
             if verbose: request.log("moin_login not valid, previous valid=%d." % user_obj.valid)
-            return user_obj, True, None, _("Invalid username or password.")
+            return ContinueLogin(user_obj, _("Invalid username or password."))
--- a/MoinMoin/auth/interwiki.py	Mon Apr 23 23:28:03 2007 +0200
+++ b/MoinMoin/auth/interwiki.py	Tue Apr 24 12:04:19 2007 +0200
@@ -11,7 +11,7 @@
 
 import xmlrpclib
 from MoinMoin import auth, wikiutil, user
-from MoinMoin.auth import BaseAuth
+from MoinMoin.auth import BaseAuth, ContinueLogin, CancelLogin
 
 class InterwikiAuth(BaseAuth):
     name = 'interwiki'
@@ -27,7 +27,7 @@
         password = kw.get('password')
 
         if not username or not password:
-            return user_obj, True, None, None
+            return ContinueLogin(user_obj)
 
         if verbose: request.log("interwiki auth: trying to auth %r" % username)
         username = username.replace(' ', ':', 1) # Hack because ':' is not allowed in name field
@@ -35,13 +35,13 @@
 
         if verbose: request.log("interwiki auth: resolve wiki returned: %r %r %r %r" % (wikitag, wikiurl, name, err))
         if err or wikitag not in self.trusted_wikis:
-            return user_obj, True, None, None
+            return ContinueLogin(user_obj)
 
         homewiki = xmlrpclib.Server(wikiurl + "?action=xmlrpc2")
         account_data = homewiki.getUser(name, password)
         if isinstance(account_data, str):
             if verbose: request.log("interwiki auth: %r wiki said: %s" % (wikitag, account_data))
-            return user_obj, True, None, account_data
+            return ContinueLogin(None, account_data)
 
         # TODO: check remote auth_attribs
         u = user.User(request, name=name, auth_method=self.name, auth_attribs=('name', 'aliasname', 'password', 'email', ))
@@ -51,4 +51,4 @@
         u.valid = True
         u.create_or_update(True)
         if verbose: request.log("interwiki: successful auth for %r" % name)
-        return u, True, None, None
+        return ContinueLogin(u)
--- a/MoinMoin/auth/ldap_login.py	Mon Apr 23 23:28:03 2007 +0200
+++ b/MoinMoin/auth/ldap_login.py	Tue Apr 24 12:04:19 2007 +0200
@@ -16,7 +16,7 @@
 import ldap
 
 from MoinMoin import user
-from MoinMoin.auth import BaseAuth
+from MoinMoin.auth import BaseAuth, CancelLogin, ContinueLogin
 
 class LDAPAuth(BaseAuth):
     """ get authentication data from form, authenticate against LDAP (or Active Directory),
@@ -32,6 +32,7 @@
     def login(self, request, user_obj, **kw):
         username = kw.get('username')
         password = kw.get('password')
+        _ = request.getText
 
         cfg = request.cfg
         verbose = cfg.ldap_verbose
@@ -39,7 +40,7 @@
         # we require non-empty password as ldap bind does a anon (not password
         # protected) bind if the password is empty and SUCCEEDS!
         if not password:
-            return user_obj, True, None, None
+            return ContinueLogin(user_obj, _('Missing password. Please enter user name and password.'))
 
         try:
             try:
@@ -93,7 +94,7 @@
                         request.log("LDAP: Search found more than one (%d) matches for %s." % (result_length, filterstr))
                     if result_length == 0:
                         if verbose: request.log("LDAP: Search found no matches for %s." % (filterstr, ))
-                    return None, False, None # if ldap returns unusable results, we veto the user and don't let him in
+                    return CancelLogin(_("Invalid username or password."))
 
                 dn, ldap_dict = lusers[0]
                 if verbose: request.log("LDAP: DN found is %s, trying to bind with pw" % dn)
@@ -124,16 +125,16 @@
 
             except ldap.INVALID_CREDENTIALS, err:
                 request.log("LDAP: invalid credentials (wrong password?) for dn %s (username: %s)" % (dn, username))
-                return None, False, None, None # if ldap says no, we veto the user and don't let him in
+                return CancelLogin(_("Invalid username or password."))
 
             if u:
                 u.create_or_update(True)
-            return u, True, None, None
+            return ContinueLogin(u)
 
         except:
             import traceback
             info = sys.exc_info()
             request.log("LDAP: caught an exception, traceback follows...")
             request.log(''.join(traceback.format_exception(*info)))
-            return None, False, None, None # something went completely wrong, in doubt we veto the login
+            return CancelLogin(None)
 
--- a/MoinMoin/auth/log.py	Mon Apr 23 23:28:03 2007 +0200
+++ b/MoinMoin/auth/log.py	Tue Apr 24 12:04:19 2007 +0200
@@ -9,7 +9,7 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-from MoinMoin.auth import BaseAuth
+from MoinMoin.auth import BaseAuth, ContinueLogin
 
 class AuthLog(BaseAuth):
     """ just log the call, do nothing else """
@@ -20,7 +20,7 @@
 
     def login(self, request, user_obj, **kw):
         self.log(request, 'login', user_obj, kw)
-        return user_obj, True, None, None
+        return ContinueLogin(user_obj)
 
     def request(self, request, user_obj, **kw):
         self.log(request, 'session', user_obj, kw)
--- a/MoinMoin/auth/mysql_group.py	Mon Apr 23 23:28:03 2007 +0200
+++ b/MoinMoin/auth/mysql_group.py	Tue Apr 24 12:04:19 2007 +0200
@@ -8,7 +8,7 @@
 """
 
 import MySQLdb
-from MoinMoin.auth import BaseAuth
+from MoinMoin.auth import BaseAuth, CancelLogin, ContinueLogin
 
 class MysqlGroupAuth(BaseAuth):
     """ Authorize via MySQL group DB.
@@ -37,7 +37,7 @@
             # No other method succeeded, so we cannot authorize
             # but maybe some following auth methods can still "fix" that.
             if verbose: request.log("auth.mysql_group did not get valid user from previous auth method")
-            return user_obj, True, None, None
+            return ContinueLogin(user_obj)
 
         # Got a valid user object - we can do stuff!
         if verbose:
@@ -56,7 +56,7 @@
             info = sys.exc_info()
             request.log("auth.mysql_group: authorization failed due to exception connecting to DB, traceback follows...")
             request.log(''.join(traceback.format_exception(*info)))
-            return None, False, None, _('Failed to connect to database.')
+            return CancelLogin(_('Failed to connect to database.'))
 
         c = m.cursor()
         c.execute(self.mysql_group_query, user_obj.auth_username)
@@ -64,13 +64,13 @@
         if results:
             # Checked out OK
             if verbose: request.log("auth.mysql_group got %d results -- authorized!" % len(results))
-            return user_obj, True, None, None # we make continuing possible, e.g. for smbmount
+            return ContinueLogin(user_obj)
         else:
             if verbose: request.log("auth.mysql_group did not get match from DB -- not authorized")
-            return None, False, None, None
+            return CancelLogin(_("Invalid username or password."))
 
     # XXX do we really want this? could it be enough to check when they log in?
     # of course then when you change the DB people who are logged in can still do stuff...
     def request(self, request, user_obj, **kw):
-        u, cont, multi, msg = self.login(request, user_obj, **kw)
-        return u, cont
+        retval = self.login(request, user_obj, **kw)
+        return retval.user_obj, retval.continue_flag
--- a/MoinMoin/auth/smb_mount.py	Mon Apr 23 23:28:03 2007 +0200
+++ b/MoinMoin/auth/smb_mount.py	Tue Apr 24 12:04:19 2007 +0200
@@ -11,7 +11,7 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-from MoinMoin.auth import BaseAuth
+from MoinMoin.auth import BaseAuth, CancelLogin, ContinueLogin
 
 class SMBMount(BaseAuth):
     """ auth plugin for (un)mounting an smb share """
@@ -75,7 +75,7 @@
         password = kw.get('password')
         if user_obj and user_obj.valid:
             do_smb(request, username, password, True)
-        return user_obj, True, None, None
+        return ContinueLogin(user_obj)
 
     def logout(self, request, user_obj, **kw):
         if user_obj and not user_obj.valid:
--- a/MoinMoin/request/__init__.py	Mon Apr 23 23:28:03 2007 +0200
+++ b/MoinMoin/request/__init__.py	Tue Apr 24 12:04:19 2007 +0200
@@ -20,6 +20,8 @@
 from MoinMoin import config, wikiutil, user, caching, error
 from MoinMoin.config import multiconfig
 from MoinMoin.util import IsWin9x
+from MoinMoin import auth
+from urllib import quote, quote_plus
 
 # umask setting --------------------------------------------------------
 def set_umask(new_mask=0777^config.umask):
@@ -139,6 +141,8 @@
 
         self._finishers = []
 
+        self._auth_redirected = False
+
         # Decode values collected by sub classes
         self.path_info = self.decodePagename(self.path_info)
 
@@ -617,26 +621,35 @@
             del self.session['setuid']
             return user_obj
 
-        for auth in self.cfg.auth:
+        for authmethod in self.cfg.auth:
             if logout:
-                user_obj, cont = auth.logout(self, user_obj, **extra)
+                user_obj, cont = authmethod.logout(self, user_obj, **extra)
             elif login:
-                if stage and auth.name != stage:
+                if stage and authmethod.name != stage:
                     continue
-                user_obj, cont, multistage, msg = auth.login(self,
-                                                             user_obj,
-                                                             **extra)
+                ret = authmethod.login(self, user_obj, **extra)
+                user_obj = ret.user_obj
+                cont = ret.continue_flag
                 if stage:
                     stage = None
                     del extra['multistage']
-                if multistage:
-                    self._login_multistage = multistage
-                    self._login_multistage_name = auth.name
+                if ret.multistage:
+                    self._login_multistage = ret.multistage
+                    self._login_multistage_name = authmethod.name
                     return user_obj
+                if ret.redirect_to:
+                    nextstage = auth.get_multistage_continuation_url(self, authmethod.name)
+                    url = ret.redirect_to
+                    url = url.replace('%return_form', quote_plus(nextstage))
+                    url = url.replace('%return', quote(nextstage))
+                    self._auth_redirected = True
+                    self.http_redirect(url)
+                    return user_obj
+                msg = ret.message
                 if msg and not msg in login_msgs:
                     login_msgs.append(msg)
             else:
-                user_obj, cont = auth.request(self, user_obj, **extra)
+                user_obj, cont = authmethod.request(self, user_obj, **extra)
             if not cont:
                 break
 
@@ -1096,7 +1109,7 @@
 
     def run(self):
         # Exit now if __init__ failed or request is forbidden
-        if self.failed or self.forbidden:
+        if self.failed or self.forbidden or self._auth_redirected:
             # Don't sleep() here, it binds too much of our resources!
             return self.finish()