Package repoze :: Package who :: Module middleware
[hide private]

Source Code for Module repoze.who.middleware

  1  import logging 
  2  from StringIO import StringIO 
  3  import sys 
  4   
  5  from repoze.who.interfaces import IIdentifier 
  6  from repoze.who.interfaces import IAuthenticator 
  7  from repoze.who.interfaces import IChallenger 
  8  from repoze.who.interfaces import IMetadataProvider 
  9   
 10  _STARTED = '-- repoze.who request started (%s) --' 
 11  _ENDED = '-- repoze.who request ended (%s) --' 
 12   
13 -class PluggableAuthenticationMiddleware(object):
14 - def __init__(self, app, 15 identifiers, 16 authenticators, 17 challengers, 18 mdproviders, 19 classifier, 20 challenge_decider, 21 log_stream = None, 22 log_level = logging.INFO, 23 remote_user_key = 'REMOTE_USER', 24 ):
25 iregistry, nregistry = make_registries(identifiers, authenticators, 26 challengers, mdproviders) 27 self.registry = iregistry 28 self.name_registry = nregistry 29 self.app = app 30 self.classifier = classifier 31 self.challenge_decider = challenge_decider 32 self.remote_user_key = remote_user_key 33 self.logger = None 34 if isinstance(log_stream, logging.Logger): 35 self.logger = log_stream 36 elif log_stream: 37 handler = logging.StreamHandler(log_stream) 38 fmt = '%(asctime)s %(message)s' 39 formatter = logging.Formatter(fmt) 40 handler.setFormatter(formatter) 41 self.logger = logging.Logger('repoze.who') 42 self.logger.addHandler(handler) 43 self.logger.setLevel(log_level)
44
45 - def __call__(self, environ, start_response):
46 if self.remote_user_key in environ: 47 # act as a pass through if REMOTE_USER (or whatever) is 48 # already set 49 return self.app(environ, start_response) 50 51 path_info = environ.get('PATH_INFO', None) 52 53 environ['repoze.who.plugins'] = self.name_registry 54 environ['repoze.who.logger'] = self.logger 55 environ['repoze.who.application'] = self.app 56 57 logger = self.logger 58 logger and logger.info(_STARTED % path_info) 59 classification = self.classifier(environ) 60 logger and logger.info('request classification: %s' % classification) 61 userid = None 62 identity = None 63 identifier = None 64 65 ids = self.identify(environ, classification) 66 67 # ids will be list of tuples: [ (IIdentifier, identity) ] 68 if ids: 69 auth_ids = self.authenticate(environ, classification, ids) 70 71 # auth_ids will be a list of five-tuples in the form 72 # ( (auth_rank, id_rank), authenticator, identifier, identity, 73 # userid ) 74 # 75 # When sorted, its first element will represent the "best" 76 # identity for this request. 77 78 if auth_ids: 79 auth_ids.sort() 80 best = auth_ids[0] 81 rank, authenticator, identifier, identity, userid = best 82 identity = Identity(identity) # dont show contents at print 83 84 # allow IMetadataProvider plugins to scribble on the identity 85 self.add_metadata(environ, classification, identity) 86 87 # add the identity to the environment; a downstream 88 # application can mutate it to do an 'identity reset' 89 # as necessary, e.g. identity['login'] = 'foo', 90 # identity['password'] = 'bar' 91 environ['repoze.who.identity'] = identity 92 # set the REMOTE_USER 93 environ[self.remote_user_key] = userid 94 95 else: 96 logger and logger.info('no identities found, not authenticating') 97 98 # allow identifier plugins to replace the downstream 99 # application (to do redirection and unauthorized themselves 100 # mostly) 101 app = environ.pop('repoze.who.application') 102 if app is not self.app: 103 logger and logger.info( 104 'static downstream application replaced with %s' % app) 105 106 wrapper = StartResponseWrapper(start_response) 107 app_iter = app(environ, wrapper.wrap_start_response) 108 109 # The challenge decider almost(?) always needs information from the 110 # response. The WSGI spec (PEP 333) states that a WSGI application 111 # must call start_response by the iterable's first iteration. If 112 # start_response hasn't been called, we'll wrap it in a way that 113 # triggers that call. 114 if not wrapper.called: 115 app_iter = wrap_generator(app_iter) 116 117 if self.challenge_decider(environ, wrapper.status, wrapper.headers): 118 logger and logger.info('challenge required') 119 120 challenge_app = self.challenge( 121 environ, 122 classification, 123 wrapper.status, 124 wrapper.headers, 125 identifier, 126 identity 127 ) 128 if challenge_app is not None: 129 logger and logger.info('executing challenge app') 130 if app_iter: 131 list(app_iter) # unwind the original app iterator 132 # replace the downstream app with the challenge app 133 app_iter = challenge_app(environ, start_response) 134 else: 135 logger and logger.info('configuration error: no challengers') 136 raise RuntimeError('no challengers found') 137 else: 138 logger and logger.info('no challenge required') 139 remember_headers = [] 140 if identifier: 141 remember_headers = identifier.remember(environ, identity) 142 if remember_headers: 143 logger and logger.info('remembering via headers from %s: %s' 144 % (identifier, remember_headers)) 145 wrapper.finish_response(remember_headers) 146 147 logger and logger.info(_ENDED % path_info) 148 return app_iter
149
150 - def identify(self, environ, classification):
151 logger = self.logger 152 candidates = self.registry.get(IIdentifier, ()) 153 logger and self.logger.info('identifier plugins registered %s' % 154 (candidates,)) 155 plugins = match_classification(IIdentifier, candidates, classification) 156 logger and self.logger.info( 157 'identifier plugins matched for ' 158 'classification "%s": %s' % (classification, plugins)) 159 160 results = [] 161 for plugin in plugins: 162 identity = plugin.identify(environ) 163 if identity is not None: 164 logger and logger.debug( 165 'identity returned from %s: %s' % (plugin, identity)) 166 results.append((plugin, identity)) 167 else: 168 logger and logger.debug( 169 'no identity returned from %s (%s)' % (plugin, identity)) 170 171 logger and logger.debug('identities found: %s' % (results,)) 172 return results
173
174 - def add_metadata(self, environ, classification, identity):
175 candidates = self.registry.get(IMetadataProvider, ()) 176 plugins = match_classification(IMetadataProvider, candidates, 177 classification) 178 for plugin in plugins: 179 plugin.add_metadata(environ, identity)
180
181 - def authenticate(self, environ, classification, identities):
182 logger = self.logger 183 candidates = self.registry.get(IAuthenticator, []) 184 logger and self.logger.info('authenticator plugins registered %s' % 185 candidates) 186 plugins = match_classification(IAuthenticator, candidates, 187 classification) 188 logger and self.logger.info( 189 'authenticator plugins matched for ' 190 'classification "%s": %s' % (classification, plugins)) 191 192 # 'preauthenticated' identities are considered best-ranking 193 identities, results, id_rank_start =self._filter_preauthenticated( 194 identities) 195 196 auth_rank = 0 197 198 for plugin in plugins: 199 identifier_rank = id_rank_start 200 for identifier, identity in identities: 201 userid = plugin.authenticate(environ, identity) 202 if userid is not None: 203 logger and logger.debug( 204 'userid returned from %s: "%s"' % (plugin, userid)) 205 206 # stamp the identity with the userid 207 identity['repoze.who.userid'] = userid 208 rank = (auth_rank, identifier_rank) 209 results.append( 210 (rank, plugin, identifier, identity, userid) 211 ) 212 else: 213 logger and logger.debug( 214 'no userid returned from %s: (%s)' % ( 215 plugin, userid)) 216 identifier_rank += 1 217 auth_rank += 1 218 219 logger and logger.debug('identities authenticated: %s' % (results,)) 220 return results
221
222 - def _filter_preauthenticated(self, identities):
223 logger = self.logger 224 results = [] 225 new_identities = identities[:] 226 227 identifier_rank = 0 228 for thing in identities: 229 identifier, identity = thing 230 userid = identity.get('repoze.who.userid') 231 if userid is not None: 232 # the identifier plugin has already authenticated this 233 # user (domain auth, auth ticket, etc) 234 logger and logger.info( 235 'userid preauthenticated by %s: "%s" ' 236 '(repoze.who.userid set)' % (identifier, userid) 237 ) 238 rank = (0, identifier_rank) 239 results.append( 240 (rank, None, identifier, identity, userid) 241 ) 242 identifier_rank += 1 243 new_identities.remove(thing) 244 return new_identities, results, identifier_rank
245
246 - def challenge(self, environ, classification, status, app_headers, 247 identifier, identity):
248 # happens on egress 249 logger = self.logger 250 251 forget_headers = [] 252 253 if identifier: 254 forget_headers = identifier.forget(environ, identity) 255 if forget_headers is None: 256 forget_headers = [] 257 else: 258 logger and logger.info('forgetting via headers from %s: %s' 259 % (identifier, forget_headers)) 260 261 candidates = self.registry.get(IChallenger, ()) 262 logger and logger.info('challengers registered: %s' % candidates) 263 plugins = match_classification(IChallenger, 264 candidates, classification) 265 logger and logger.info('challengers matched for ' 266 'classification "%s": %s' % (classification, 267 plugins)) 268 for plugin in plugins: 269 app = plugin.challenge(environ, status, app_headers, 270 forget_headers) 271 if app is not None: 272 # new WSGI application 273 logger and logger.info( 274 'challenger plugin %s "challenge" returned an app' % ( 275 plugin)) 276 return app 277 278 # signifies no challenge 279 logger and logger.info('no challenge app returned') 280 return None
281
282 -def wrap_generator(result):
283 """\ 284 This function returns a generator that behaves exactly the same as the 285 original. It's only difference is it pulls the first iteration off and 286 caches it to trigger any immediate side effects (in a WSGI world, this 287 ensures start_response is called). 288 """ 289 # Neat trick to pull the first iteration only. We need to do this outside 290 # of the generator function to ensure it is called. 291 for iter in result: 292 first = iter 293 break 294 295 # Wrapper yields the first iteration, then passes result's iterations 296 # directly up. 297 def wrapper(): 298 yield first 299 for iter in result: 300 # We'll let result's StopIteration bubble up directly. 301 yield iter
302 return wrapper() 303
304 -def match_classification(iface, plugins, classification):
305 result = [] 306 for plugin in plugins: 307 308 plugin_classifications = getattr(plugin, 'classifications', {}) 309 iface_classifications = plugin_classifications.get(iface) 310 if not iface_classifications: # good for any 311 result.append(plugin) 312 continue 313 if classification in iface_classifications: 314 result.append(plugin) 315 316 return result
317
318 -class StartResponseWrapper(object):
319 - def __init__(self, start_response):
320 self.start_response = start_response 321 self.status = None 322 self.headers = [] 323 self.exc_info = None 324 self.buffer = StringIO() 325 # A WSGI app may delay calling start_response until the first iteration 326 # of its generator. We track this so we know whether or not we need to 327 # trigger an iteration before examining the response. 328 self.called = False
329
330 - def wrap_start_response(self, status, headers, exc_info=None):
331 self.headers = headers 332 self.status = status 333 self.exc_info = exc_info 334 # The response has been initiated, so we have a valid code. 335 self.called = True 336 return self.buffer.write
337
338 - def finish_response(self, extra_headers):
339 if not extra_headers: 340 extra_headers = [] 341 headers = self.headers + extra_headers 342 write = self.start_response(self.status, headers, self.exc_info) 343 if write: 344 self.buffer.seek(0) 345 value = self.buffer.getvalue() 346 if value: 347 write(value) 348 if hasattr(write, 'close'): 349 write.close()
350
351 -def make_test_middleware(app, global_conf):
352 """ Functionally equivalent to 353 354 [plugin:form] 355 use = repoze.who.plugins.form.FormPlugin 356 rememberer_name = cookie 357 login_form_qs=__do_login 358 359 [plugin:cookie] 360 use = repoze.who.plugins.cookie:InsecureCookiePlugin 361 cookie_name = oatmeal 362 363 [plugin:basicauth] 364 use = repoze.who.plugins.basicauth.BasicAuthPlugin 365 realm = repoze.who 366 367 [plugin:htpasswd] 368 use = repoze.who.plugins.htpasswd.HTPasswdPlugin 369 filename = <...> 370 check_fn = repoze.who.plugins.htpasswd:crypt_check 371 372 [general] 373 request_classifier = repoze.who.classifiers:default_request_classifier 374 challenge_decider = repoze.who.classifiers:default_challenge_decider 375 376 [identifiers] 377 plugins = form:browser cookie basicauth 378 379 [authenticators] 380 plugins = htpasswd 381 382 [challengers] 383 plugins = form:browser basicauth 384 """ 385 # be able to test without a config file 386 from repoze.who.plugins.basicauth import BasicAuthPlugin 387 from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin 388 from repoze.who.plugins.cookie import InsecureCookiePlugin 389 from repoze.who.plugins.form import FormPlugin 390 from repoze.who.plugins.htpasswd import HTPasswdPlugin 391 io = StringIO() 392 salt = 'aa' 393 for name, password in [ ('admin', 'admin'), ('chris', 'chris') ]: 394 io.write('%s:%s\n' % (name, password)) 395 io.seek(0) 396 def cleartext_check(password, hashed): 397 return password == hashed #pragma NO COVERAGE
398 htpasswd = HTPasswdPlugin(io, cleartext_check) 399 basicauth = BasicAuthPlugin('repoze.who') 400 auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt') 401 cookie = InsecureCookiePlugin('oatmeal') 402 form = FormPlugin('__do_login', rememberer_name='auth_tkt') 403 form.classifications = { IIdentifier:['browser'], 404 IChallenger:['browser'] } # only for browser 405 identifiers = [('form', form),('auth_tkt',auth_tkt),('basicauth',basicauth)] 406 authenticators = [('htpasswd', htpasswd)] 407 challengers = [('form',form), ('basicauth',basicauth)] 408 mdproviders = [] 409 from repoze.who.classifiers import default_request_classifier 410 from repoze.who.classifiers import default_challenge_decider 411 log_stream = None 412 import os 413 if os.environ.get('WHO_LOG'): 414 log_stream = sys.stdout 415 middleware = PluggableAuthenticationMiddleware( 416 app, 417 identifiers, 418 authenticators, 419 challengers, 420 mdproviders, 421 default_request_classifier, 422 default_challenge_decider, 423 log_stream = log_stream, 424 log_level = logging.DEBUG 425 ) 426 return middleware 427
428 -def verify(plugin, iface):
429 from zope.interface.verify import verifyObject 430 verifyObject(iface, plugin, tentative=True)
431
432 -def make_registries(identifiers, authenticators, challengers, mdproviders):
433 from zope.interface.verify import BrokenImplementation 434 interface_registry = {} 435 name_registry = {} 436 437 for supplied, iface in [ (identifiers, IIdentifier), 438 (authenticators, IAuthenticator), 439 (challengers, IChallenger), 440 (mdproviders, IMetadataProvider)]: 441 442 for name, value in supplied: 443 try: 444 verify(value, iface) 445 except BrokenImplementation, why: 446 why = str(why) 447 raise ValueError(str(name) + ': ' + why) 448 L = interface_registry.setdefault(iface, []) 449 L.append(value) 450 name_registry[name] = value 451 452 return interface_registry, name_registry
453
454 -class Identity(dict):
455 """ dict subclass that prevents its members from being rendered 456 during print """
457 - def __repr__(self):
458 return '<repoze.who identity (hidden, dict-like) at %s>' % id(self)
459 __str__ = __repr__
460