Tres Seaver
2016-05-31 455778d138ea623d224c9206e5001fd2a1fd7e1c
commit | author | age
7dfea7 1 import logging
CM 2 import sys
a400b0 3
993216 4 from repoze.who.api import APIFactory
cb5426 5 from repoze.who.interfaces import IChallenger
adef05 6 from repoze.who._compat import StringIO
7dfea7 7
88e646 8 _STARTED = '-- repoze.who request started (%s) --'
CM 9 _ENDED = '-- repoze.who request ended (%s) --'
7dfea7 10
d85ba6 11 class PluggableAuthenticationMiddleware(object):
993216 12     def __init__(self,
TS 13                  app,
c51195 14                  identifiers,
CM 15                  authenticators,
16                  challengers,
f1582f 17                  mdproviders,
cbc983 18                  request_classifier = None,
TS 19                  challenge_decider = None,
db4cf5 20                  log_stream = None,
CM 21                  log_level = logging.INFO,
22                  remote_user_key = 'REMOTE_USER',
cbc983 23                  classifier = None
c51195 24                  ):
cbc983 25         if challenge_decider is None:
TS 26             raise ValueError('challenge_decider is required')
27         if request_classifier is not None and classifier is not None:
28             raise ValueError(
29                     'Only one of request_classifier and classifier is allowed')
30         if request_classifier is None:
31             if classifier is None:
32                 raise ValueError(
9b56fd 33                         'Either request_classifier or classifier is required')
cbc983 34             request_classifier = classifier
d85ba6 35         self.app = app
993216 36         logger = self.logger = None
a84601 37         if isinstance(log_stream, logging.Logger):
993216 38             logger = self.logger = log_stream
a84601 39         elif log_stream:
7dfea7 40             handler = logging.StreamHandler(log_stream)
CM 41             fmt = '%(asctime)s %(message)s'
42             formatter = logging.Formatter(fmt)
43             handler.setFormatter(formatter)
993216 44             logger = self.logger = logging.Logger('repoze.who')
TS 45             logger.addHandler(handler)
46             logger.setLevel(log_level)
47         self.remote_user_key = remote_user_key
48
49         self.api_factory = APIFactory(identifiers,
50                                       authenticators,
51                                       challengers, 
52                                       mdproviders,
53                                       request_classifier,
54                                       challenge_decider,
b482a1 55                                       remote_user_key,
TS 56                                       logger
57                                      )
993216 58
d85ba6 59
CM 60     def __call__(self, environ, start_response):
db4cf5 61         if self.remote_user_key in environ:
CM 62             # act as a pass through if REMOTE_USER (or whatever) is
63             # already set
c51195 64             return self.app(environ, start_response)
CM 65
993216 66         api = self.api_factory(environ)
88e646 67
993216 68         environ['repoze.who.plugins'] = api.name_registry # BBB?
cb5426 69         environ['repoze.who.logger'] = self.logger
a400b0 70         environ['repoze.who.application'] = self.app
c51195 71
7dfea7 72         logger = self.logger
993216 73         path_info = environ.get('PATH_INFO', None)
88e646 74         logger and logger.info(_STARTED % path_info)
455778 75         api.authenticate()  # identity saved in environ
c51195 76
a400b0 77         # allow identifier plugins to replace the downstream
CM 78         # application (to do redirection and unauthorized themselves
79         # mostly)
80         app = environ.pop('repoze.who.application')
81         if  app is not self.app:
82             logger and logger.info(
83                 'static downstream application replaced with %s' % app)
84
c51195 85         wrapper = StartResponseWrapper(start_response)
a400b0 86         app_iter = app(environ, wrapper.wrap_start_response)
7dfea7 87
08b2ae 88         # The challenge decider almost(?) always needs information from the
CM 89         # response.  The WSGI spec (PEP 333) states that a WSGI application
90         # must call start_response by the iterable's first iteration.  If
91         # start_response hasn't been called, we'll wrap it in a way that
92         # triggers that call.
93         if not wrapper.called:
94             app_iter = wrap_generator(app_iter)
95
993216 96         if api.challenge_decider(environ, wrapper.status, wrapper.headers):
e99c8b 97             logger and logger.info('challenge required')
493726 98             close = getattr(app_iter, 'close', _no_op)
7dfea7 99
b482a1 100             challenge_app = api.challenge(wrapper.status, wrapper.headers)
c51195 101             if challenge_app is not None:
e99c8b 102                 logger and logger.info('executing challenge app')
c51195 103                 if app_iter:
CM 104                     list(app_iter) # unwind the original app iterator
b01f44 105                 # PEP 333 requires that we call the original iterator's
TS 106                 # 'close' method, if it exists, before releasing it.
107                 close()
c51195 108                 # replace the downstream app with the challenge app
CM 109                 app_iter = challenge_app(environ, start_response)
110             else:
e99c8b 111                 logger and logger.info('configuration error: no challengers')
493726 112                 close()
c51195 113                 raise RuntimeError('no challengers found')
7dfea7 114         else:
97cfa2 115             logger and logger.info('no challenge required')
455778 116             remember_headers = api.remember()
e99c8b 117             wrapper.finish_response(remember_headers)
7dfea7 118
88e646 119         logger and logger.info(_ENDED % path_info)
c51195 120         return app_iter
7dfea7 121
493726 122 def _no_op():
TS 123     pass
124
08b2ae 125 def wrap_generator(result):
CM 126     """\
127     This function returns a generator that behaves exactly the same as the
128     original.  It's only difference is it pulls the first iteration off and
129     caches it to trigger any immediate side effects (in a WSGI world, this
130     ensures start_response is called).
131     """
b01f44 132     # PEP 333 requires that we call the original iterator's
TS 133     # 'close' method, if it exists, before releasing it.
134     close = getattr(result, 'close', lambda: None)
08b2ae 135     # Neat trick to pull the first iteration only. We need to do this outside
CM 136     # of the generator function to ensure it is called.
19d219 137     first = marker = []
08b2ae 138     for iter in result:
CM 139         first = iter
140         break
141
142     # Wrapper yields the first iteration, then passes result's iterations
143     # directly up.
144     def wrapper():
19d219 145         if first is not marker:
TS 146             yield first
08b2ae 147         for iter in result:
CM 148             # We'll let result's StopIteration bubble up directly.
149             yield iter
b01f44 150         close()
08b2ae 151     return wrapper()
CM 152
c51195 153 class StartResponseWrapper(object):
CM 154     def __init__(self, start_response):
155         self.start_response = start_response
740830 156         self.status = None
c51195 157         self.headers = []
740830 158         self.exc_info = None
c51195 159         self.buffer = StringIO()
08b2ae 160         # A WSGI app may delay calling start_response until the first iteration
CM 161         # of its generator.  We track this so we know whether or not we need to
162         # trigger an iteration before examining the response.
163         self.called = False
c51195 164
CM 165     def wrap_start_response(self, status, headers, exc_info=None):
166         self.headers = headers
167         self.status = status
740830 168         self.exc_info = exc_info
08b2ae 169         # The response has been initiated, so we have a valid code.
CM 170         self.called = True
c51195 171         return self.buffer.write
CM 172
e99c8b 173     def finish_response(self, extra_headers):
CM 174         if not extra_headers:
175             extra_headers = []
176         headers = self.headers + extra_headers
740830 177         write = self.start_response(self.status, headers, self.exc_info)
c51195 178         if write:
CM 179             self.buffer.seek(0)
180             value = self.buffer.getvalue()
181             if value:
182                 write(value)
183             if hasattr(write, 'close'):
184                 write.close()
7dfea7 185
d85ba6 186 def make_test_middleware(app, global_conf):
80a263 187     """ Functionally equivalent to
CM 188
d7f613 189     [plugin:redirector]
TS 190     use = repoze.who.plugins.redirector.RedirectorPlugin
191     login_url = /login.html
80a263 192
d7f613 193     [plugin:auth_tkt]
TS 194     use = repoze.who.plugins.auth_tkt:AuthTktCookiePlugin
195     secret = SEEKRIT
80a263 196     cookie_name = oatmeal
CM 197
198     [plugin:basicauth]
cb5426 199     use = repoze.who.plugins.basicauth.BasicAuthPlugin
CM 200     realm = repoze.who
80a263 201
CM 202     [plugin:htpasswd]
cb5426 203     use = repoze.who.plugins.htpasswd.HTPasswdPlugin
80a263 204     filename = <...>
cb5426 205     check_fn = repoze.who.plugins.htpasswd:crypt_check
80a263 206
CM 207     [general]
cb5426 208     request_classifier = repoze.who.classifiers:default_request_classifier
CM 209     challenge_decider = repoze.who.classifiers:default_challenge_decider
80a263 210
CM 211     [identifiers]
d7f613 212     plugins = authtkt basicauth
80a263 213
CM 214     [authenticators]
d7f613 215     plugins = authtkt htpasswd
80a263 216
CM 217     [challengers]
d7f613 218     plugins = redirector:browser basicauth
80a263 219     """
7dfea7 220     # be able to test without a config file
cb5426 221     from repoze.who.plugins.basicauth import BasicAuthPlugin
CM 222     from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin
d7f613 223     from repoze.who.plugins.redirector import RedirectorPlugin
cb5426 224     from repoze.who.plugins.htpasswd import HTPasswdPlugin
7dfea7 225     io = StringIO()
c51195 226     for name, password in [ ('admin', 'admin'), ('chris', 'chris') ]:
53e216 227         io.write('%s:%s\n' % (name, password))
c51195 228     io.seek(0)
53e216 229     def cleartext_check(password, hashed):
d32c12 230         return password == hashed #pragma NO COVERAGE
53e216 231     htpasswd = HTPasswdPlugin(io, cleartext_check)
cb5426 232     basicauth = BasicAuthPlugin('repoze.who')
40a968 233     auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt')
d7f613 234     redirector = RedirectorPlugin('/login.html')
TS 235     redirector.classifications = {IChallenger: ['browser']} # only for browser
236     identifiers = [('auth_tkt', auth_tkt),
237                    ('basicauth', basicauth),
238                   ]
c51195 239     authenticators = [('htpasswd', htpasswd)]
d7f613 240     challengers = [('redirector', redirector),
TS 241                    ('basicauth', basicauth)]
d9f046 242     mdproviders = []
cb5426 243     from repoze.who.classifiers import default_request_classifier
CM 244     from repoze.who.classifiers import default_challenge_decider
d9f046 245     log_stream = None
c80cab 246     import os
d9f046 247     if os.environ.get('WHO_LOG'):
CM 248         log_stream = sys.stdout
c51195 249     middleware = PluggableAuthenticationMiddleware(
CM 250         app,
251         identifiers,
252         authenticators,
253         challengers,
b9c2d6 254         mdproviders,
c51195 255         default_request_classifier,
CM 256         default_challenge_decider,
db4cf5 257         log_stream = log_stream,
c51195 258         log_level = logging.DEBUG
7dfea7 259         )
d85ba6 260     return middleware