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