Back to index

moin  1.9.0~rc2
__init__.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - modular authentication handling
00004 
00005     Each authentication method is an object instance containing
00006     four methods:
00007       * login(request, user_obj, **kw)
00008       * logout(request, user_obj, **kw)
00009       * request(request, user_obj, **kw)
00010       * login_hint(request)
00011 
00012     The kw arguments that are passed in are currently:
00013        attended: boolean indicating whether a user (attended=True) or
00014                  a machine is requesting login, multistage auth is not
00015                  currently possible for machine logins [login only]
00016        username: the value of the 'username' form field (or None)
00017                  [login only]
00018        password: the value of the 'password' form field (or None)
00019                  [login only]
00020        cookie: a Cookie.SimpleCookie instance containing the cookie
00021                that the browser sent
00022        multistage: boolean indicating multistage login continuation
00023                    [may not be present, login only]
00024        openid_identifier: the OpenID identifier we got from the form
00025                           (or None) [login only]
00026 
00027     login_hint() should return a HTML text that is displayed to the user right
00028     below the login form, it should tell the user what to do in case of a
00029     forgotten password and how to create an account (if applicable.)
00030 
00031     More may be added.
00032 
00033     The request method is called for each request except login/logout.
00034 
00035     The 'request' and 'logout' methods must return a tuple (user_obj, continue)
00036     where 'user_obj' can be
00037       * None, to throw away any previous user_obj from previous auth methods
00038       * the passed in user_obj for no changes
00039       * a newly created MoinMoin.user.User instance
00040     and 'continue' is a boolean to indicate whether the next authentication
00041     method should be tried.
00042 
00043     The 'login' method must return an instance of MoinMoin.auth.LoginReturn
00044     which contains the members
00045       * user_obj
00046       * continue_flag
00047       * multistage
00048       * message
00049       * redirect_to
00050 
00051     There are some helpful subclasses derived from this class for the most
00052     common cases, namely ContinueLogin(), CancelLogin(), MultistageFormLogin()
00053     and MultistageRedirectLogin().
00054 
00055     The user_obj and continue_flag members have the same semantics as for the
00056     request and logout methods.
00057 
00058     The messages that are returned by the various auth methods will be
00059     displayed to the user, since they will all be displayed usually auth
00060     methods will use the message feature only along with returning False for
00061     the continue flag.
00062 
00063     Note, however, that when no username is entered or the username is not
00064     found in the database, it may be appropriate to return with a message
00065     and the continue flag set to true (ContinueLogin) because a subsequent auth
00066     plugin might work even without the username, say the openid plugin for
00067     example.
00068 
00069     The multistage member must evaluate to false or be callable. If it is
00070     callable, this indicates that the authentication method requires a second
00071     login stage. In that case, the multistage item will be called with the
00072     request as the only parameter. It should return an instance of
00073     MoinMoin.widget.html.FORM and the generic code will append some required
00074     hidden fields to it. It is also permissible to return some valid HTML,
00075     but that feature has very limited use since it breaks the authentication
00076     method chain.
00077 
00078     Note that because multistage login does not depend on anonymous session
00079     support, it is possible that users jump directly into the second stage
00080     by giving the appropriate parameters to the login action. Hence, auth
00081     methods should take care to recheck everything and not assume the user
00082     has gone through all previous stages.
00083 
00084     If the multistage login requires querying an external site that involves
00085     a redirect, the redirect_to member may be set instead of the multistage
00086     member. If this is set it must be a URL that user should be redirected to.
00087     Since the user must be able to come back to the authentication, any
00088     "%return" in the URL is replaced with the url-encoded form of the URL
00089     to the next authentication stage, any "%return_form" is replaced with
00090     the url-plus-encoded form (spaces encoded as +) of the same URL.
00091 
00092     After the user has submitted the required form or has been redirected back
00093     from the external site, execution of the auth login methods resumes with
00094     the auth item that requested the multistage login and its login method is
00095     called with the 'multistage' keyword parameter set to True.
00096 
00097     Each authentication method instance must also contain the members
00098      * login_inputs: a list of required inputs, currently supported are
00099                       - 'username': username entry field
00100                       - 'password': password entry field
00101                       - 'openid_identifier': OpenID entry field
00102                       - 'special_no_input': manual login is required
00103                             but no form fields need to be filled in
00104                             (for example openid with forced provider)
00105                             in this case the theme may provide a short-
00106                             cut omitting the login form
00107      * logout_possible: boolean indicating whether this auth methods
00108                         supports logging out
00109      * name: name of the auth method, must be the same as given as the
00110              user object's auth_method keyword parameter.
00111 
00112     To simplify creating new authentication methods you can inherit from
00113     MoinMoin.auth.BaseAuth that does nothing for all three methods, but
00114     allows you to override only some methods.
00115 
00116     cfg.auth is a list of authentication object instances whose methods
00117     are called in the order they are listed. The session method is called
00118     for every request, when logging in or out these are called before the
00119     session method.
00120 
00121     When creating a new MoinMoin.user.User object, you can give a keyword
00122     argument "auth_attribs" to User.__init__ containing a list of user
00123     attributes that are determined and fixed by this auth method and may
00124     not be changed by the user in their preferences.
00125     You also have to give the keyword argument "auth_method" containing the
00126     name of the authentication method.
00127 
00128     @copyright: 2005-2006 Bastian Blank, Florian Festi,
00129                           MoinMoin:AlexanderSchremmer, Nick Phillips,
00130                           MoinMoin:FrankieChow, MoinMoin:NirSoffer,
00131                 2005-2009 MoinMoin:ThomasWaldmann,
00132                 2007      MoinMoin:JohannesBerg
00133 
00134     @license: GNU GPL, see COPYING for details.
00135 """
00136 
00137 from MoinMoin import log
00138 logging = log.getLogger(__name__)
00139 
00140 from MoinMoin import user, wikiutil
00141 
00142 
00143 def get_multistage_continuation_url(request, auth_name, extra_fields={}):
00144     """get_continuation_url - return a multistage continuation URL
00145 
00146        This function returns a URL that when loaded continues a multistage
00147        authentication at the auth method requesting it (parameter auth_name.)
00148        Additional fields are added to the URL from the extra_fields dict.
00149 
00150        @param request: the Moin request
00151        @param auth_name: name of the auth method requesting the continuation
00152        @param extra_fields: extra GET fields to add to the URL
00153     """
00154     # logically, this belongs to request, but semantically it should
00155     # live in auth so people do auth.get_multistage_continuation_url()
00156     fields = {'action': 'login',
00157               'login': '1',
00158               'stage': auth_name}
00159     fields.update(extra_fields)
00160     if request.page:
00161         logging.debug("request.page.url: " + request.page.url(request, querystr=fields))
00162         return request.page.url(request, querystr=fields)
00163     else:
00164         logging.debug("request.abs_href: " + request.abs_href(**fields))
00165         return request.abs_href(**fields)
00166 
00167 class LoginReturn(object):
00168     """ LoginReturn - base class for auth method login() return value"""
00169     def __init__(self, user_obj, continue_flag, message=None, multistage=None,
00170                  redirect_to=None):
00171         self.user_obj = user_obj
00172         self.continue_flag = continue_flag
00173         self.message = message
00174         self.multistage = multistage
00175         self.redirect_to = redirect_to
00176 
00177 class ContinueLogin(LoginReturn):
00178     """ ContinueLogin - helper for auth method login that just continues """
00179     def __init__(self, user_obj, message=None):
00180         LoginReturn.__init__(self, user_obj, True, message=message)
00181 
00182 class CancelLogin(LoginReturn):
00183     """ CancelLogin - cancel login showing a message """
00184     def __init__(self, message):
00185         LoginReturn.__init__(self, None, False, message=message)
00186 
00187 class MultistageFormLogin(LoginReturn):
00188     """ MultistageFormLogin - require user to fill in another form """
00189     def __init__(self, multistage):
00190         LoginReturn.__init__(self, None, False, multistage=multistage)
00191 
00192 class MultistageRedirectLogin(LoginReturn):
00193     """ MultistageRedirectLogin - redirect user to another site before continuing login """
00194     def __init__(self, url):
00195         LoginReturn.__init__(self, None, False, redirect_to=url)
00196 
00197 
00198 class BaseAuth:
00199     name = None
00200     login_inputs = []
00201     logout_possible = False
00202     def __init__(self):
00203         pass
00204     def login(self, request, user_obj, **kw):
00205         return ContinueLogin(user_obj)
00206     def request(self, request, user_obj, **kw):
00207         return user_obj, True
00208     def logout(self, request, user_obj, **kw):
00209         if self.name and user_obj and user_obj.auth_method == self.name:
00210             logging.debug("%s: logout - invalidating user %r" % (self.name, user_obj.name))
00211             user_obj.valid = False
00212         return user_obj, True
00213     def login_hint(self, request):
00214         return None
00215 
00216 class MoinAuth(BaseAuth):
00217     """ handle login from moin login form """
00218     def __init__(self):
00219         BaseAuth.__init__(self)
00220 
00221     login_inputs = ['username', 'password']
00222     name = 'moin'
00223     logout_possible = True
00224 
00225     def login(self, request, user_obj, **kw):
00226         username = kw.get('username')
00227         password = kw.get('password')
00228 
00229         # simply continue if something else already logged in successfully
00230         if user_obj and user_obj.valid:
00231             return ContinueLogin(user_obj)
00232 
00233         if not username and not password:
00234             return ContinueLogin(user_obj)
00235 
00236         _ = request.getText
00237 
00238         logging.debug("%s: performing login action" % self.name)
00239 
00240         if username and not password:
00241             return ContinueLogin(user_obj, _('Missing password. Please enter user name and password.'))
00242 
00243         u = user.User(request, name=username, password=password, auth_method=self.name)
00244         if u.valid:
00245             logging.debug("%s: successfully authenticated user %r (valid)" % (self.name, u.name))
00246             return ContinueLogin(u)
00247         else:
00248             logging.debug("%s: could not authenticate user %r (not valid)" % (self.name, username))
00249             return ContinueLogin(user_obj, _("Invalid username or password."))
00250 
00251     def login_hint(self, request):
00252         _ = request.getText
00253         #if request.cfg.openidrp_registration_url:
00254         #    userprefslink = request.cfg.openidrp_registration_url
00255         #else:
00256         userprefslink = request.page.url(request, querystr={'action': 'newaccount'})
00257         sendmypasswordlink = request.page.url(request, querystr={'action': 'recoverpass'})
00258 
00259         msg = ''
00260         #if request.cfg.openidrp_allow_registration:
00261         msg = _('If you do not have an account, <a href="%(userprefslink)s">you can create one now</a>. ') % {
00262               'userprefslink': userprefslink}
00263         msg += _('<a href="%(sendmypasswordlink)s">Forgot your password?</a>') % {
00264                'sendmypasswordlink': sendmypasswordlink}
00265         return msg
00266 
00267         #return _('If you do not have an account, <a href="%(userprefslink)s">you can create one now</a>. '
00268         #         '<a href="%(sendmypasswordlink)s">Forgot your password?</a>') % {
00269         #       'userprefslink': userprefslink,
00270         #       'sendmypasswordlink': sendmypasswordlink}
00271 
00272 
00273 class GivenAuth(BaseAuth):
00274     """ reuse a given authentication, e.g. http basic auth (or any other auth)
00275         done by the web server, that sets REMOTE_USER environment variable.
00276         This is the default behaviour.
00277         You can also specify to read another environment variable (env_var).
00278         Alternatively you can directly give a fixed user name (user_name)
00279         that will be considered as authenticated.
00280     """
00281     name = 'given' # was 'http' in 1.8.x and before
00282 
00283     def __init__(self,
00284                  env_var=None,  # environment variable we want to read (default: REMOTE_USER)
00285                  user_name=None,  # can be used to just give a specific user name to log in
00286                  autocreate=False,  # create/update the user profile for the auth. user
00287                  strip_maildomain=False,  # joe@example.org -> joe
00288                  strip_windomain=False,  # DOMAIN\joe -> joe
00289                  titlecase=False,  # joe doe -> Joe Doe
00290                  remove_blanks=False,  # Joe Doe -> JoeDoe
00291                  coding=None,  # for decoding REMOTE_USER correctly (default: auto)
00292                 ):
00293         self.env_var = env_var
00294         self.user_name = user_name
00295         self.autocreate = autocreate
00296         self.strip_maildomain = strip_maildomain
00297         self.strip_windomain = strip_windomain
00298         self.titlecase = titlecase
00299         self.remove_blanks = remove_blanks
00300         self.coding = coding
00301         BaseAuth.__init__(self)
00302 
00303     def decode_username(self, name):
00304         """ decode the name we got from the environment var to unicode """
00305         if isinstance(name, str):
00306             if self.coding:
00307                 name = name.decode(self.coding)
00308             else:
00309                 # XXX we have no idea about REMOTE_USER encoding, please help if
00310                 # you know how to do that cleanly
00311                 name = wikiutil.decodeUnknownInput(name)
00312         return name
00313 
00314     def transform_username(self, name):
00315         """ transform the name we got (unicode in, unicode out)
00316 
00317             Note: if you need something more special, you could create your own
00318                   auth class, inherit from this class and overwrite this function.
00319         """
00320         assert isinstance(name, unicode)
00321         if self.strip_maildomain:
00322             # split off mail domain, e.g. "user@example.org" -> "user"
00323             name = name.split(u'@')[0]
00324 
00325         if self.strip_windomain:
00326             # split off window domain, e.g. "DOMAIN\user" -> "user"
00327             name = name.split(u'\\')[-1]
00328 
00329         if self.titlecase:
00330             # this "normalizes" the login name, e.g. meier, Meier, MEIER -> Meier
00331             name = name.title()
00332 
00333         if self.remove_blanks:
00334             # remove blanks e.g. "Joe Doe" -> "JoeDoe"
00335             name = u''.join(name.split())
00336 
00337         return name
00338 
00339     def request(self, request, user_obj, **kw):
00340         u = None
00341         _ = request.getText
00342         # always revalidate auth
00343         if user_obj and user_obj.auth_method == self.name:
00344             user_obj = None
00345         # something else authenticated before us
00346         if user_obj:
00347             logging.debug("already authenticated, doing nothing")
00348             return user_obj, True
00349 
00350         if self.user_name is not None:
00351             auth_username = self.user_name
00352         elif self.env_var is None:
00353             auth_username = request.remote_user
00354         else:
00355             auth_username = request.environ.get(self.env_var)
00356 
00357         logging.debug("auth_username = %r" % auth_username)
00358         if auth_username:
00359             auth_username = self.decode_username(auth_username)
00360             auth_username = self.transform_username(auth_username)
00361             logging.debug("auth_username (after decode/transform) = %r" % auth_username)
00362             u = user.User(request, auth_username=auth_username,
00363                           auth_method=self.name, auth_attribs=('name', 'password'))
00364 
00365         logging.debug("u: %r" % u)
00366         if u and self.autocreate:
00367             logging.debug("autocreating user")
00368             u.create_or_update()
00369         if u and u.valid:
00370             logging.debug("returning valid user %r" % u)
00371             return u, True # True to get other methods called, too
00372         else:
00373             logging.debug("returning %r" % user_obj)
00374             return user_obj, True
00375 
00376 
00377 def handle_login(request, userobj=None, username=None, password=None,
00378                  attended=True, openid_identifier=None, stage=None):
00379     """
00380     Process a 'login' request by going through the configured authentication
00381     methods in turn. The passable keyword arguments are explained in more
00382     detail at the top of this file.
00383     """
00384     params = {
00385         'username': username,
00386         'password': password,
00387         'attended': attended,
00388         'openid_identifier': openid_identifier,
00389         'multistage': (stage and True) or None
00390     }
00391     for authmethod in request.cfg.auth:
00392         if stage and authmethod.name != stage:
00393             continue
00394         ret = authmethod.login(request, userobj, **params)
00395 
00396         userobj = ret.user_obj
00397         cont = ret.continue_flag
00398         if stage:
00399             stage = None
00400             del params['multistage']
00401 
00402         if ret.multistage:
00403             request._login_multistage = ret.multistage
00404             request._login_multistage_name = authmethod.name
00405             return userobj
00406 
00407         if ret.redirect_to:
00408             nextstage = auth.get_multistage_continuation_url(request, authmethod.name)
00409             url = ret.redirect_to
00410             url = url.replace('%return_form', quote_plus(nextstage))
00411             url = url.replace('%return', quote(nextstage))
00412             abort(redirect(url))
00413         msg = ret.message
00414         if msg and not msg in request._login_messages:
00415             request._login_messages.append(msg)
00416 
00417         if not cont:
00418             break
00419 
00420     return userobj
00421 
00422 def handle_logout(request, userobj):
00423     """ Logout the passed user from every configured authentication method. """
00424     if userobj is None:
00425         # not logged in
00426         return userobj
00427 
00428     if userobj.auth_method == 'setuid':
00429         # we have no authmethod object for setuid
00430         userobj = request._setuid_real_user
00431         del request._setuid_real_user
00432         return userobj
00433 
00434     for authmethod in request.cfg.auth:
00435         userobj, cont = authmethod.logout(request, userobj, cookie=request.cookies)
00436         if not cont:
00437             break
00438     return userobj
00439 
00440 def handle_request(request, userobj):
00441     """ Handle the per-request callbacks of the configured authentication methods. """
00442     for authmethod in request.cfg.auth:
00443         userobj, cont = authmethod.request(request, userobj, cookie=request.cookies)
00444         if not cont:
00445             break
00446     return userobj
00447 
00448 def setup_setuid(request, userobj):
00449     """ Check for setuid conditions in the session and setup an user
00450     object accordingly. Returns a tuple of the new user objects.
00451 
00452     @param request: a moin request object
00453     @param userobj: a moin user object
00454     @rtype: boolean
00455     @return: (new_user, user) or (user, None)
00456     """
00457     old_user = None
00458     if 'setuid' in request.session and userobj and userobj.isSuperUser():
00459         old_user = userobj
00460         uid = request.session['setuid']
00461         userobj = user.User(request, uid, auth_method='setuid')
00462         userobj.valid = True
00463     logging.debug("setup_suid returns %r, %r" % (userobj, old_user))
00464     return (userobj, old_user)
00465 
00466 def setup_from_session(request, session):
00467     userobj = None
00468     if 'user.id' in session:
00469         auth_userid = session['user.id']
00470         auth_method = session['user.auth_method']
00471         auth_attrs = session['user.auth_attribs']
00472         logging.debug("got from session: %r %r" % (auth_userid, auth_method))
00473         logging.debug("current auth methods: %r" % request.cfg.auth_methods)
00474         if auth_method and auth_method in request.cfg.auth_methods:
00475             userobj = user.User(request, id=auth_userid,
00476                                 auth_method=auth_method,
00477                                 auth_attribs=auth_attrs)
00478     logging.debug("session started for user %r", userobj)
00479     return userobj
00480