1# -*- coding: iso-8859-1 -*-
2"""
3    MoinMoin - modular authentication handling
4
5    Each authentication method is an object instance containing
6    four methods:
7      * login(request, user_obj, **kw)
8      * logout(request, user_obj, **kw)
9      * request(request, user_obj, **kw)
10      * login_hint(request)
11
12    The kw arguments that are passed in are currently:
13       attended: boolean indicating whether a user (attended=True) or
14                 a machine is requesting login, multistage auth is not
15                 currently possible for machine logins [login only]
16       username: the value of the 'username' form field (or None)
17                 [login only]
18       password: the value of the 'password' form field (or None)
19                 [login only]
20       cookie: a Cookie.SimpleCookie instance containing the cookie
21               that the browser sent
22       multistage: boolean indicating multistage login continuation
23                   [may not be present, login only]
24       openid_identifier: the OpenID identifier we got from the form
25                          (or None) [login only]
26
27    login_hint() should return a HTML text that is displayed to the user right
28    below the login form, it should tell the user what to do in case of a
29    forgotten password and how to create an account (if applicable.)
30
31    More may be added.
32
33    The request method is called for each request except login/logout.
34
35    The 'request' and 'logout' methods must return a tuple (user_obj, continue)
36    where 'user_obj' can be
37      * None, to throw away any previous user_obj from previous auth methods
38      * the passed in user_obj for no changes
39      * a newly created MoinMoin.user.User instance
40    and 'continue' is a boolean to indicate whether the next authentication
41    method should be tried.
42
43    The 'login' method must return an instance of MoinMoin.auth.LoginReturn
44    which contains the members
45      * user_obj
46      * continue_flag
47      * multistage
48      * message
49      * redirect_to
50
51    There are some helpful subclasses derived from this class for the most
52    common cases, namely ContinueLogin(), CancelLogin(), MultistageFormLogin()
53    and MultistageRedirectLogin().
54
55    The user_obj and continue_flag members have the same semantics as for the
56    request and logout methods.
57
58    The messages that are returned by the various auth methods will be
59    displayed to the user, since they will all be displayed usually auth
60    methods will use the message feature only along with returning False for
61    the continue flag.
62
63    Note, however, that when no username is entered or the username is not
64    found in the database, it may be appropriate to return with a message
65    and the continue flag set to true (ContinueLogin) because a subsequent auth
66    plugin might work even without the username, say the openid plugin for
67    example.
68
69    The multistage member must evaluate to false or be callable. If it is
70    callable, this indicates that the authentication method requires a second
71    login stage. In that case, the multistage item will be called with the
72    request as the only parameter. It should return an instance of
73    MoinMoin.widget.html.FORM and the generic code will append some required
74    hidden fields to it. It is also permissible to return some valid HTML,
75    but that feature has very limited use since it breaks the authentication
76    method chain.
77
78    Note that because multistage login does not depend on anonymous session
79    support, it is possible that users jump directly into the second stage
80    by giving the appropriate parameters to the login action. Hence, auth
81    methods should take care to recheck everything and not assume the user
82    has gone through all previous stages.
83
84    If the multistage login requires querying an external site that involves
85    a redirect, the redirect_to member may be set instead of the multistage
86    member. If this is set it must be a URL that user should be redirected to.
87    Since the user must be able to come back to the authentication, any
88    "%return" in the URL is replaced with the url-encoded form of the URL
89    to the next authentication stage, any "%return_form" is replaced with
90    the url-plus-encoded form (spaces encoded as +) of the same URL.
91
92    After the user has submitted the required form or has been redirected back
93    from the external site, execution of the auth login methods resumes with
94    the auth item that requested the multistage login and its login method is
95    called with the 'multistage' keyword parameter set to True.
96
97    Each authentication method instance must also contain the members
98     * login_inputs: a list of required inputs, currently supported are
99                      - 'username': username entry field
100                      - 'password': password entry field
101                      - 'openid_identifier': OpenID entry field
102                      - 'special_no_input': manual login is required
103                            but no form fields need to be filled in
104                            (for example openid with forced provider)
105                            in this case the theme may provide a short-
106                            cut omitting the login form
107     * logout_possible: boolean indicating whether this auth methods
108                        supports logging out
109     * name: name of the auth method, must be the same as given as the
110             user object's auth_method keyword parameter.
111
112    To simplify creating new authentication methods you can inherit from
113    MoinMoin.auth.BaseAuth that does nothing for all three methods, but
114    allows you to override only some methods.
115
116    cfg.auth is a list of authentication object instances whose methods
117    are called in the order they are listed. The session method is called
118    for every request, when logging in or out these are called before the
119    session method.
120
121    When creating a new MoinMoin.user.User object, you can give a keyword
122    argument "auth_attribs" to User.__init__ containing a list of user
123    attributes that are determined and fixed by this auth method and may
124    not be changed by the user in their preferences.
125    You also have to give the keyword argument "auth_method" containing the
126    name of the authentication method.
127
128    @copyright: 2005-2006 Bastian Blank, Florian Festi,
129                          MoinMoin:AlexanderSchremmer, Nick Phillips,
130                          MoinMoin:FrankieChow, MoinMoin:NirSoffer,
131                2005-2009 MoinMoin:ThomasWaldmann,
132                2007      MoinMoin:JohannesBerg
133
134    @license: GNU GPL, see COPYING for details.
135"""
136
137from MoinMoin import log
138logging = log.getLogger(__name__)
139
140from werkzeug.utils import redirect
141from werkzeug.exceptions import abort
142from werkzeug.urls import url_quote, url_quote_plus
143
144from MoinMoin import user, wikiutil
145from MoinMoin.web.utils import check_surge_protect
146from MoinMoin.util.abuse import log_attempt
147
148
149def get_multistage_continuation_url(request, auth_name, extra_fields={}):
150    """get_continuation_url - return a multistage continuation URL
151
152       This function returns a URL that when loaded continues a multistage
153       authentication at the auth method requesting it (parameter auth_name.)
154       Additional fields are added to the URL from the extra_fields dict.
155
156       @param request: the Moin request
157       @param auth_name: name of the auth method requesting the continuation
158       @param extra_fields: extra GET fields to add to the URL
159    """
160    # logically, this belongs to request, but semantically it should
161    # live in auth so people do auth.get_multistage_continuation_url()
162    fields = {'action': 'login',
163              'login': '1',
164              'stage': auth_name}
165    fields.update(extra_fields)
166    if request.page:
167        logging.debug("request.page.url: " + request.page.url(request, querystr=fields))
168        return request.page.url(request, querystr=fields)
169    else:
170        logging.debug("request.abs_href: " + request.abs_href(**fields))
171        return request.abs_href(**fields)
172
173class LoginReturn(object):
174    """ LoginReturn - base class for auth method login() return value"""
175    def __init__(self, user_obj, continue_flag, message=None, multistage=None,
176                 redirect_to=None):
177        self.user_obj = user_obj
178        self.continue_flag = continue_flag
179        self.message = message
180        self.multistage = multistage
181        self.redirect_to = redirect_to
182
183class ContinueLogin(LoginReturn):
184    """ ContinueLogin - helper for auth method login that just continues """
185    def __init__(self, user_obj, message=None):
186        LoginReturn.__init__(self, user_obj, True, message=message)
187
188class CancelLogin(LoginReturn):
189    """ CancelLogin - cancel login showing a message """
190    def __init__(self, message):
191        LoginReturn.__init__(self, None, False, message=message)
192
193class MultistageFormLogin(LoginReturn):
194    """ MultistageFormLogin - require user to fill in another form """
195    def __init__(self, multistage):
196        LoginReturn.__init__(self, None, False, multistage=multistage)
197
198class MultistageRedirectLogin(LoginReturn):
199    """ MultistageRedirectLogin - redirect user to another site before continuing login """
200    def __init__(self, url):
201        LoginReturn.__init__(self, None, False, redirect_to=url)
202
203
204class BaseAuth:
205    name = None
206    login_inputs = []
207    logout_possible = False
208    def __init__(self):
209        pass
210    def login(self, request, user_obj, **kw):
211        return ContinueLogin(user_obj)
212    def request(self, request, user_obj, **kw):
213        return user_obj, True
214    def logout(self, request, user_obj, **kw):
215        if self.name and user_obj and user_obj.auth_method == self.name:
216            logging.debug("%s: logout - invalidating user %r" % (self.name, user_obj.name))
217            user_obj.valid = False
218        return user_obj, True
219    def login_hint(self, request):
220        return None
221
222class MoinAuth(BaseAuth):
223    """ handle login from moin login form """
224    def __init__(self):
225        BaseAuth.__init__(self)
226
227    login_inputs = ['username', 'password']
228    name = 'moin'
229    logout_possible = True
230
231    def login(self, request, user_obj, **kw):
232        username = kw.get('username')
233        password = kw.get('password')
234
235        # simply continue if something else already logged in successfully
236        if user_obj and user_obj.valid:
237            return ContinueLogin(user_obj)
238
239        if not username and not password:
240            return ContinueLogin(user_obj)
241
242        _ = request.getText
243
244        logging.debug("%s: performing login action" % self.name)
245
246        if username and not password:
247            return ContinueLogin(user_obj, _('Missing password. Please enter user name and password.'))
248        if not username and password:
249            return ContinueLogin(user_obj, _('Missing user name. Please enter user name and password.'))
250
251        check_surge_protect(request, action='auth-ip')
252        check_surge_protect(request, action='auth-name', username=username)
253
254        u = user.User(request, name=username, password=password, auth_method=self.name)
255        if u.valid:
256            try:
257                verification = u.account_verification
258            except:
259                verification = False
260            if request.cfg.require_email_verification and verification:
261                logging.debug("%s: could not authenticate user %r (not verified yet)" % (self.name, username))
262                return ContinueLogin(user_obj, _("User account not verified yet."))
263            logging.debug("%s: successfully authenticated user %r (valid)" % (self.name, u.name))
264            log_attempt("auth/login (moin)", True, request, username)
265            return ContinueLogin(u)
266        else:
267            logging.debug("%s: could not authenticate user %r (not valid)" % (self.name, username))
268            log_attempt("auth/login (moin)", False, request, username)
269            return ContinueLogin(user_obj, _("Invalid username or password."))
270
271    def login_hint(self, request):
272        _ = request.getText
273        #if request.cfg.openidrp_registration_url:
274        #    userprefslink = request.cfg.openidrp_registration_url
275        #else:
276        userprefslink = request.page.url(request, querystr={'action': 'newaccount'})
277        sendmypasswordlink = request.page.url(request, querystr={'action': 'recoverpass'})
278
279        msg = ''
280        #if request.cfg.openidrp_allow_registration:
281        if 'newaccount' not in request.cfg.actions_superuser:
282            msg += _('If you do not have an account, <a href="%(userprefslink)s">you can create one now</a>. ') % {
283                     'userprefslink': userprefslink}
284        msg += _('<a href="%(sendmypasswordlink)s">Forgot your password?</a>') % {
285               'sendmypasswordlink': sendmypasswordlink}
286        return msg
287
288        #return _('If you do not have an account, <a href="%(userprefslink)s">you can create one now</a>. '
289        #         '<a href="%(sendmypasswordlink)s">Forgot your password?</a>') % {
290        #       'userprefslink': userprefslink,
291        #       'sendmypasswordlink': sendmypasswordlink}
292
293
294class GivenAuth(BaseAuth):
295    """ reuse a given authentication, e.g. http basic auth (or any other auth)
296        done by the web server, that sets REMOTE_USER environment variable.
297        This is the default behaviour.
298        You can also specify to read another environment variable (env_var).
299        Alternatively you can directly give a fixed user name (user_name)
300        that will be considered as authenticated.
301    """
302    name = 'given' # was 'http' in 1.8.x and before
303
304    def __init__(self,
305                 env_var=None,  # environment variable we want to read (default: REMOTE_USER)
306                 user_name=None,  # can be used to just give a specific user name to log in
307                 autocreate=False,  # create/update the user profile for the auth. user
308                 strip_maildomain=False,  # joe@example.org -> joe
309                 strip_windomain=False,  # DOMAIN\joe -> joe
310                 titlecase=False,  # joe doe -> Joe Doe
311                 remove_blanks=False,  # Joe Doe -> JoeDoe
312                 coding=None,  # for decoding REMOTE_USER correctly (default: auto)
313                ):
314        self.env_var = env_var
315        self.user_name = user_name
316        self.autocreate = autocreate
317        self.strip_maildomain = strip_maildomain
318        self.strip_windomain = strip_windomain
319        self.titlecase = titlecase
320        self.remove_blanks = remove_blanks
321        self.coding = coding
322        BaseAuth.__init__(self)
323
324    def decode_username(self, name):
325        """ decode the name we got from the environment var to unicode """
326        if isinstance(name, str):
327            if self.coding:
328                name = name.decode(self.coding)
329            else:
330                # XXX we have no idea about REMOTE_USER encoding, please help if
331                # you know how to do that cleanly
332                name = wikiutil.decodeUnknownInput(name)
333        return name
334
335    def transform_username(self, name):
336        """ transform the name we got (unicode in, unicode out)
337
338            Note: if you need something more special, you could create your own
339                  auth class, inherit from this class and overwrite this function.
340        """
341        assert isinstance(name, unicode)
342        if self.strip_maildomain:
343            # split off mail domain, e.g. "user@example.org" -> "user"
344            name = name.split(u'@')[0]
345
346        if self.strip_windomain:
347            # split off window domain, e.g. "DOMAIN\user" -> "user"
348            name = name.split(u'\\')[-1]
349
350        if self.titlecase:
351            # this "normalizes" the login name, e.g. meier, Meier, MEIER -> Meier
352            name = name.title()
353
354        if self.remove_blanks:
355            # remove blanks e.g. "Joe Doe" -> "JoeDoe"
356            name = u''.join(name.split())
357
358        return name
359
360    def request(self, request, user_obj, **kw):
361        u = None
362        _ = request.getText
363        # always revalidate auth
364        if user_obj and user_obj.auth_method == self.name:
365            user_obj = None
366        # something else authenticated before us
367        if user_obj:
368            logging.debug("already authenticated, doing nothing")
369            return user_obj, True
370
371        if self.user_name is not None:
372            auth_username = self.user_name
373        elif self.env_var is None:
374            auth_username = request.remote_user
375        else:
376            auth_username = request.environ.get(self.env_var)
377
378        logging.debug("auth_username = %r" % auth_username)
379        if auth_username:
380            auth_username = self.decode_username(auth_username)
381            auth_username = self.transform_username(auth_username)
382            logging.debug("auth_username (after decode/transform) = %r" % auth_username)
383            u = user.User(request, auth_username=auth_username,
384                          auth_method=self.name, auth_attribs=('name', 'password'))
385
386        logging.debug("u: %r" % u)
387        if u and self.autocreate:
388            logging.debug("autocreating user")
389            u.create_or_update()
390        if u and u.valid:
391            logging.debug("returning valid user %r" % u)
392            log_attempt("auth/request (given)", True, request, auth_username)
393            return u, True # True to get other methods called, too
394        else:
395            logging.debug("returning %r" % user_obj)
396            if u and not u.valid:
397                log_attempt("auth/request (given)", False, request, auth_username)
398            return user_obj, True
399
400
401def handle_login(request, userobj=None, username=None, password=None,
402                 attended=True, openid_identifier=None, stage=None):
403    """
404    Process a 'login' request by going through the configured authentication
405    methods in turn. The passable keyword arguments are explained in more
406    detail at the top of this file.
407    """
408    params = {
409        'username': username,
410        'password': password,
411        'attended': attended,
412        'openid_identifier': openid_identifier,
413        'multistage': (stage and True) or None
414    }
415    for authmethod in request.cfg.auth:
416        if stage and authmethod.name != stage:
417            continue
418        ret = authmethod.login(request, userobj, **params)
419
420        userobj = ret.user_obj
421        cont = ret.continue_flag
422        if stage:
423            stage = None
424            del params['multistage']
425
426        if ret.multistage:
427            request._login_multistage = ret.multistage
428            request._login_multistage_name = authmethod.name
429            return userobj
430
431        if ret.redirect_to:
432            nextstage = get_multistage_continuation_url(request, authmethod.name)
433            url = ret.redirect_to
434            url = url.replace('%return_form', url_quote_plus(nextstage))
435            url = url.replace('%return', url_quote(nextstage))
436            abort(redirect(url))
437        msg = ret.message
438        if msg and not msg in request._login_messages:
439            request._login_messages.append(msg)
440
441        if not cont:
442            break
443
444    return userobj
445
446def handle_logout(request, userobj):
447    """ Logout the passed user from every configured authentication method. """
448    if userobj is None:
449        # not logged in
450        return userobj
451
452    if userobj.auth_method == 'setuid':
453        # we have no authmethod object for setuid
454        userobj = request._setuid_real_user
455        del request._setuid_real_user
456        return userobj
457
458    for authmethod in request.cfg.auth:
459        userobj, cont = authmethod.logout(request, userobj, cookie=request.cookies)
460        if not cont:
461            break
462    return userobj
463
464def handle_request(request, userobj):
465    """ Handle the per-request callbacks of the configured authentication methods. """
466    for authmethod in request.cfg.auth:
467        userobj, cont = authmethod.request(request, userobj, cookie=request.cookies)
468        if not cont:
469            break
470    return userobj
471
472def setup_setuid(request, userobj):
473    """ Check for setuid conditions in the session and setup an user
474    object accordingly. Returns a tuple of the new user objects.
475
476    @param request: a moin request object
477    @param userobj: a moin user object
478    @rtype: boolean
479    @return: (new_user, user) or (user, None)
480    """
481    old_user = None
482    if 'setuid' in request.session and userobj and userobj.isSuperUser():
483        old_user = userobj
484        uid = request.session['setuid']
485        userobj = user.User(request, uid, auth_method='setuid')
486        userobj.valid = True
487        log_attempt("auth/login (setuid from %r)" % old_user.name, True, request, userobj.name)
488    logging.debug("setup_suid returns %r, %r" % (userobj, old_user))
489    return (userobj, old_user)
490
491def setup_from_session(request, session):
492    userobj = None
493    if 'user.id' in session:
494        auth_userid = session['user.id']
495        auth_method = session['user.auth_method']
496        auth_attrs = session['user.auth_attribs']
497        logging.debug("got from session: %r %r" % (auth_userid, auth_method))
498        logging.debug("current auth methods: %r" % request.cfg.auth_methods)
499        if auth_method and auth_method in request.cfg.auth_methods:
500            userobj = user.User(request, id=auth_userid,
501                                auth_method=auth_method,
502                                auth_attribs=auth_attrs)
503    logging.debug("session started for user %r", userobj)
504    return userobj
505
506