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