Merge lp:~leonardr/lazr.restful/forward-port into lp:lazr.restful
- forward-port
- Merge into trunk
Proposed by
Leonard Richardson
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/lazr.restful/forward-port |
Merge into: | lp:lazr.restful |
Diff against target: |
527 lines 8 files modified
setup.py (+0/-1) src/lazr/restful/NEWS.txt (+9/-0) src/lazr/restful/_resource.py (+1/-1) src/lazr/restful/docs/webservice.txt (+5/-3) src/lazr/restful/docs/wsgi.txt (+0/-225) src/lazr/restful/testing/webservice.py (+0/-33) src/lazr/restful/version.txt (+1/-1) src/lazr/restful/wsgi.py (+0/-144) |
To merge this branch: | bzr merge lp:~leonardr/lazr.restful/forward-port |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Deryck Hodge (community) | code | Approve | |
Review via email: mp+13009@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote : | # |
Revision history for this message
Deryck Hodge (deryck) wrote : | # |
Looks good to me. Thanks for bringing this change forward! Those being plagued by this error when doing non-ascii in bug descriptions will clap and cheer. :)
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'setup.py' | |||
2 | --- setup.py 2009-10-01 15:24:08 +0000 | |||
3 | +++ setup.py 2009-10-07 17:20:23 +0000 | |||
4 | @@ -63,7 +63,6 @@ | |||
5 | 63 | 'lazr.lifecycle', | 63 | 'lazr.lifecycle', |
6 | 64 | 'lazr.uri', | 64 | 'lazr.uri', |
7 | 65 | 'martian==0.11', | 65 | 'martian==0.11', |
8 | 66 | 'oauth', | ||
9 | 67 | 'setuptools', | 66 | 'setuptools', |
10 | 68 | 'simplejson', | 67 | 'simplejson', |
11 | 69 | 'van.testing', | 68 | 'van.testing', |
12 | 70 | 69 | ||
13 | === modified file 'src/lazr/restful/NEWS.txt' | |||
14 | --- src/lazr/restful/NEWS.txt 2009-10-06 16:09:51 +0000 | |||
15 | +++ src/lazr/restful/NEWS.txt 2009-10-07 17:20:23 +0000 | |||
16 | @@ -2,6 +2,15 @@ | |||
17 | 2 | NEWS for lazr.restful | 2 | NEWS for lazr.restful |
18 | 3 | ===================== | 3 | ===================== |
19 | 4 | 4 | ||
20 | 5 | 0.9.9 (2009-10-07) | ||
21 | 6 | ================== | ||
22 | 7 | |||
23 | 8 | The authentication-related WSGI middleware classes have been split | ||
24 | 9 | into a separate project, lazr.authentication. | ||
25 | 10 | |||
26 | 11 | Fixed a bug that prevented some incoming strings from being loaded by | ||
27 | 12 | simplejson. | ||
28 | 13 | |||
29 | 5 | 0.9.8 (2009-10-06) | 14 | 0.9.8 (2009-10-06) |
30 | 6 | ================== | 15 | ================== |
31 | 7 | 16 | ||
32 | 8 | 17 | ||
33 | === modified file 'src/lazr/restful/_resource.py' | |||
34 | --- src/lazr/restful/_resource.py 2009-09-24 15:23:48 +0000 | |||
35 | +++ src/lazr/restful/_resource.py 2009-10-07 17:20:23 +0000 | |||
36 | @@ -882,7 +882,7 @@ | |||
37 | 882 | self.request.response.setStatus(415) | 882 | self.request.response.setStatus(415) |
38 | 883 | return None, 'Expected a media type of %s.' % self.JSON_TYPE | 883 | return None, 'Expected a media type of %s.' % self.JSON_TYPE |
39 | 884 | try: | 884 | try: |
41 | 885 | h = simplejson.loads(unicode(representation)) | 885 | h = simplejson.loads(representation.decode('utf-8')) |
42 | 886 | except ValueError: | 886 | except ValueError: |
43 | 887 | self.request.response.setStatus(400) | 887 | self.request.response.setStatus(400) |
44 | 888 | return None, "Entity-body was not a well-formed JSON document." | 888 | return None, "Entity-body was not a well-formed JSON document." |
45 | 889 | 889 | ||
46 | === modified file 'src/lazr/restful/docs/webservice.txt' | |||
47 | --- src/lazr/restful/docs/webservice.txt 2009-09-02 18:54:22 +0000 | |||
48 | +++ src/lazr/restful/docs/webservice.txt 2009-10-07 17:20:23 +0000 | |||
49 | @@ -1815,11 +1815,13 @@ | |||
50 | 1815 | 1815 | ||
51 | 1816 | A PATCH request will automatically result in a modification event | 1816 | A PATCH request will automatically result in a modification event |
52 | 1817 | being sent out about the modified object, which means that | 1817 | being sent out about the modified object, which means that |
55 | 1818 | modify_cookbook() will be run. Here, we modify the name of one of the | 1818 | modify_cookbook() will be run. Here, we modify the name and the |
56 | 1819 | cookbooks. | 1819 | cuisine of one of the cookbooks. Note that the cuisine contains |
57 | 1820 | non-ASCII characters. | ||
58 | 1820 | 1821 | ||
59 | 1821 | >>> headers = {'CONTENT_TYPE' : 'application/json'} | 1822 | >>> headers = {'CONTENT_TYPE' : 'application/json'} |
61 | 1822 | >>> body = '{"name" : "The Joy of Cooking (revised)"}' | 1823 | >>> body = '''{"name" : "The Joy of Cooking (revised)", |
62 | 1824 | ... "cuisine" : "\xd7\x97\xd7\x95\xd7\x9e\xd7\x95\xd7\xa1"}''' | ||
63 | 1823 | 1825 | ||
64 | 1824 | >>> patch_request = create_web_service_request( | 1826 | >>> patch_request = create_web_service_request( |
65 | 1825 | ... '/beta/cookbooks/The%20Joy%20of%20Cooking', body=body, | 1827 | ... '/beta/cookbooks/The%20Joy%20of%20Cooking', body=body, |
66 | 1826 | 1828 | ||
67 | === removed file 'src/lazr/restful/docs/wsgi.txt' | |||
68 | --- src/lazr/restful/docs/wsgi.txt 2009-10-06 15:59:10 +0000 | |||
69 | +++ src/lazr/restful/docs/wsgi.txt 1970-01-01 00:00:00 +0000 | |||
70 | @@ -1,225 +0,0 @@ | |||
71 | 1 | WSGI Middleware | ||
72 | 2 | =============== | ||
73 | 3 | |||
74 | 4 | lazr.restful defines some simple WSGI middleware for doing | ||
75 | 5 | authentication. This is mainly for setting up testing scenarios for | ||
76 | 6 | lazr.restfulclient, but you can use the middleware in real | ||
77 | 7 | applications. | ||
78 | 8 | |||
79 | 9 | BasicAuthMiddleware | ||
80 | 10 | ------------------- | ||
81 | 11 | |||
82 | 12 | The BasicAuthMiddleware implements HTTP Basic Auth. Its constructor | ||
83 | 13 | takes a number of arguments, including a callback function that | ||
84 | 14 | performs the actual authentication. This function returns an object | ||
85 | 15 | identifying the user who's trying to authenticate. If the | ||
86 | 16 | authentication credentials are invalid, it's supposed to return None. | ||
87 | 17 | |||
88 | 18 | First, let's create a really simple WSGI application that responds to | ||
89 | 19 | any request with a 200 response code. | ||
90 | 20 | |||
91 | 21 | >>> from lazr.restful.testing.webservice import WebServiceApplication | ||
92 | 22 | >>> from lazr.restful.example.base.tests.test_integration import ( | ||
93 | 23 | ... CookbookWebServiceTestPublication) | ||
94 | 24 | |||
95 | 25 | >>> def dummy_application(environ, start_response): | ||
96 | 26 | ... start_response('200', [('Content-type','text/plain')]) | ||
97 | 27 | ... return ['Success'] | ||
98 | 28 | |||
99 | 29 | Now let's protect that application. Here's an authentication callback | ||
100 | 30 | function. | ||
101 | 31 | |||
102 | 32 | >>> def authenticate(username, password): | ||
103 | 33 | ... """Accepts "user/password", rejects everything else. | ||
104 | 34 | ... | ||
105 | 35 | ... :return: The username, if the credentials are valid. | ||
106 | 36 | ... None, otherwise. | ||
107 | 37 | ... """ | ||
108 | 38 | ... if username == "user" and password == "password": | ||
109 | 39 | ... return username | ||
110 | 40 | ... return None | ||
111 | 41 | |||
112 | 42 | >>> print authenticate("user", "password") | ||
113 | 43 | user | ||
114 | 44 | |||
115 | 45 | >>> print authenticate("notuser", "password") | ||
116 | 46 | None | ||
117 | 47 | |||
118 | 48 | Here's a WSGI application that protects the application using | ||
119 | 49 | BasicAuthMiddleware. | ||
120 | 50 | |||
121 | 51 | >>> from lazr.restful.wsgi import BasicAuthMiddleware | ||
122 | 52 | >>> def protected_application(): | ||
123 | 53 | ... return BasicAuthMiddleware( | ||
124 | 54 | ... dummy_application, realm="WSGI middleware test", | ||
125 | 55 | ... protect_path_pattern=".*protected.*", | ||
126 | 56 | ... authenticate_with=authenticate) | ||
127 | 57 | |||
128 | 58 | >>> import wsgi_intercept | ||
129 | 59 | >>> from wsgi_intercept.httplib2_intercept import install | ||
130 | 60 | >>> install() | ||
131 | 61 | >>> wsgi_intercept.add_wsgi_intercept( | ||
132 | 62 | ... 'basictest', 80, protected_application) | ||
133 | 63 | |||
134 | 64 | Most of the application's URLs are not protected by the | ||
135 | 65 | middleware. You can access them without providing credentials. | ||
136 | 66 | |||
137 | 67 | >>> import httplib2 | ||
138 | 68 | >>> client = httplib2.Http() | ||
139 | 69 | >>> response, body = client.request('http://basictest/') | ||
140 | 70 | >>> print response['status'] | ||
141 | 71 | 200 | ||
142 | 72 | >>> print body | ||
143 | 73 | Success | ||
144 | 74 | |||
145 | 75 | Any URL that includes the string "protected" is protected by the | ||
146 | 76 | middleware, and cannot be accessed without credentials. | ||
147 | 77 | |||
148 | 78 | >>> response, body = client.request('http://basictest/protected/') | ||
149 | 79 | >>> print response['status'] | ||
150 | 80 | 401 | ||
151 | 81 | >>> print response['www-authenticate'] | ||
152 | 82 | Basic realm="WSGI middleware test" | ||
153 | 83 | |||
154 | 84 | >>> response, body = client.request( | ||
155 | 85 | ... 'http://basictest/this-is-protected-as-well/') | ||
156 | 86 | >>> print response['status'] | ||
157 | 87 | 401 | ||
158 | 88 | |||
159 | 89 | The check_credentials() implementation given at the beginning of the | ||
160 | 90 | test will only accept the user/password combination "user"/"password". | ||
161 | 91 | Provide a bad username or password and you'll get a 401. | ||
162 | 92 | |||
163 | 93 | >>> client.add_credentials("baduser", "baspassword") | ||
164 | 94 | >>> response, body = client.request('http://basictest/protected/') | ||
165 | 95 | >>> print response['status'] | ||
166 | 96 | 401 | ||
167 | 97 | |||
168 | 98 | Provide the correct credentials and you'll get a 200, even for the | ||
169 | 99 | protected URIs. | ||
170 | 100 | |||
171 | 101 | >>> client.add_credentials("user", "password") | ||
172 | 102 | >>> response, body = client.request('http://basictest/protected/') | ||
173 | 103 | >>> print response['status'] | ||
174 | 104 | 200 | ||
175 | 105 | |||
176 | 106 | OAuthMiddleware | ||
177 | 107 | --------------- | ||
178 | 108 | |||
179 | 109 | The OAuthMiddleware implements section 7 ("Accessing Protected | ||
180 | 110 | Resources") of the OAuth specification. That is, it makes sure that | ||
181 | 111 | incoming consumer keys and access tokens pass some application-defined | ||
182 | 112 | test. It does not help you serve request tokens or exchange a request | ||
183 | 113 | token for an access token. | ||
184 | 114 | |||
185 | 115 | We'll use OAuthMiddleware to protect the same simple application we | ||
186 | 116 | protected earlier with BasicAuthMiddleware. But since we're using | ||
187 | 117 | OAuth, we'll be checking a consumer key and access token, instead of a | ||
188 | 118 | username and password. | ||
189 | 119 | |||
190 | 120 | >>> from oauth.oauth import OAuthConsumer, OAuthToken | ||
191 | 121 | |||
192 | 122 | >>> valid_consumer = OAuthConsumer("consumer", '') | ||
193 | 123 | >>> valid_token = OAuthToken("token", "secret") | ||
194 | 124 | |||
195 | 125 | >>> def authenticate(consumer, token, parameters): | ||
196 | 126 | ... """Accepts the valid consumer and token, rejects everything else. | ||
197 | 127 | ... | ||
198 | 128 | ... :return: The consumer, if the credentials are valid. | ||
199 | 129 | ... None, otherwise. | ||
200 | 130 | ... """ | ||
201 | 131 | ... if consumer == valid_consumer and token == valid_token: | ||
202 | 132 | ... return consumer | ||
203 | 133 | ... return None | ||
204 | 134 | |||
205 | 135 | >>> print authenticate(valid_consumer, valid_token, None).key | ||
206 | 136 | consumer | ||
207 | 137 | |||
208 | 138 | >>> invalid_consumer = OAuthConsumer("other consumer", '') | ||
209 | 139 | >>> print authenticate(invalid_consumer, valid_token, None) | ||
210 | 140 | None | ||
211 | 141 | |||
212 | 142 | To test the OAuthMiddleware's security features, we'll also need to | ||
213 | 143 | create a data store. In a real application the data store would | ||
214 | 144 | probably be a database containing the registered consumer keys and | ||
215 | 145 | tokens. We're using a simple data store designed for testing, and | ||
216 | 146 | telling it about the one valid consumer and token. | ||
217 | 147 | |||
218 | 148 | >>> from lazr.restful.testing.webservice import SimpleOAuthDataStore | ||
219 | 149 | >>> data_store = SimpleOAuthDataStore( | ||
220 | 150 | ... {valid_consumer.key : valid_consumer}, | ||
221 | 151 | ... {valid_token.key : valid_token}) | ||
222 | 152 | |||
223 | 153 | >>> print data_store.lookup_consumer("consumer").key | ||
224 | 154 | consumer | ||
225 | 155 | >>> print data_store.lookup_consumer("badconsumer") | ||
226 | 156 | None | ||
227 | 157 | |||
228 | 158 | The data store tracks the use of OAuth nonces. If you call the data | ||
229 | 159 | store's lookup_nonce() twice with the same values, the first call will | ||
230 | 160 | return False and the second call will return True. | ||
231 | 161 | |||
232 | 162 | >>> print data_store.lookup_nonce("consumer", "token", "nonce") | ||
233 | 163 | False | ||
234 | 164 | >>> print data_store.lookup_nonce("consumer", "token", "nonce") | ||
235 | 165 | True | ||
236 | 166 | |||
237 | 167 | >>> print data_store.lookup_nonce("newconsumer", "token", "nonce") | ||
238 | 168 | False | ||
239 | 169 | |||
240 | 170 | Now let's protect an application with lazr.restful's OAuthMiddleware, | ||
241 | 171 | using our authentication technique and our simple data store. | ||
242 | 172 | |||
243 | 173 | >>> from lazr.restful.wsgi import OAuthMiddleware | ||
244 | 174 | >>> def protected_application(): | ||
245 | 175 | ... return OAuthMiddleware( | ||
246 | 176 | ... dummy_application, realm="OAuth test", | ||
247 | 177 | ... authenticate_with=authenticate, data_store=data_store) | ||
248 | 178 | |||
249 | 179 | >>> wsgi_intercept.add_wsgi_intercept( | ||
250 | 180 | ... 'oauthtest', 80, protected_application) | ||
251 | 181 | >>> client = httplib2.Http() | ||
252 | 182 | |||
253 | 183 | A properly signed request will go through to the underlying WSGI | ||
254 | 184 | application. | ||
255 | 185 | |||
256 | 186 | >>> from oauth.oauth import ( | ||
257 | 187 | ... OAuthRequest, OAuthSignatureMethod_PLAINTEXT) | ||
258 | 188 | >>> def sign_request(url, consumer=valid_consumer, token=valid_token): | ||
259 | 189 | ... request = OAuthRequest().from_consumer_and_token( | ||
260 | 190 | ... consumer, token, http_url=url) | ||
261 | 191 | ... request.sign_request( | ||
262 | 192 | ... OAuthSignatureMethod_PLAINTEXT(), consumer, token) | ||
263 | 193 | ... headers = request.to_header('OAuth test') | ||
264 | 194 | ... return headers | ||
265 | 195 | |||
266 | 196 | >>> url = 'http://oauthtest/' | ||
267 | 197 | >>> headers = sign_request(url) | ||
268 | 198 | >>> response, body = client.request(url, headers=headers) | ||
269 | 199 | >>> print response['status'] | ||
270 | 200 | 200 | ||
271 | 201 | >>> print body | ||
272 | 202 | Success | ||
273 | 203 | |||
274 | 204 | If you replay a signed HTTP request that worked the first time, it | ||
275 | 205 | will fail the second time, because you'll be sending a nonce that was | ||
276 | 206 | already used. | ||
277 | 207 | |||
278 | 208 | >>> response, body = client.request(url, headers=headers) | ||
279 | 209 | >>> print response['status'] | ||
280 | 210 | 401 | ||
281 | 211 | |||
282 | 212 | An unsigned request will fail. | ||
283 | 213 | |||
284 | 214 | >>> response, body = client.request('http://oauthtest/') | ||
285 | 215 | >>> print response['status'] | ||
286 | 216 | 401 | ||
287 | 217 | |||
288 | 218 | A request signed with invalid credentials will fail. | ||
289 | 219 | |||
290 | 220 | >>> bad_token = OAuthToken("token", "badsecret") | ||
291 | 221 | >>> headers = sign_request(url, token=bad_token) | ||
292 | 222 | >>> response, body = client.request(url, headers=headers) | ||
293 | 223 | >>> print response['status'] | ||
294 | 224 | 401 | ||
295 | 225 | |||
296 | 226 | 0 | ||
297 | === modified file 'src/lazr/restful/testing/webservice.py' | |||
298 | --- src/lazr/restful/testing/webservice.py 2009-10-06 15:59:10 +0000 | |||
299 | +++ src/lazr/restful/testing/webservice.py 2009-10-07 17:20:23 +0000 | |||
300 | @@ -11,7 +11,6 @@ | |||
301 | 11 | 'pprint_entry', | 11 | 'pprint_entry', |
302 | 12 | 'WebServiceTestPublication', | 12 | 'WebServiceTestPublication', |
303 | 13 | 'WebServiceTestRequest', | 13 | 'WebServiceTestRequest', |
304 | 14 | 'SimpleOAuthDataStore', | ||
305 | 15 | 'TestPublication', | 14 | 'TestPublication', |
306 | 16 | ] | 15 | ] |
307 | 17 | 16 | ||
308 | @@ -22,8 +21,6 @@ | |||
309 | 22 | from urlparse import urljoin | 21 | from urlparse import urljoin |
310 | 23 | import wsgi_intercept | 22 | import wsgi_intercept |
311 | 24 | 23 | ||
312 | 25 | from oauth.oauth import OAuthDataStore | ||
313 | 26 | |||
314 | 27 | from zope.component import adapts, getUtility, queryMultiAdapter | 24 | from zope.component import adapts, getUtility, queryMultiAdapter |
315 | 28 | from zope.interface import implements | 25 | from zope.interface import implements |
316 | 29 | from zope.publisher.browser import BrowserRequest | 26 | from zope.publisher.browser import BrowserRequest |
317 | @@ -363,33 +360,3 @@ | |||
318 | 363 | def __str__(self): | 360 | def __str__(self): |
319 | 364 | return "http://dummy" | 361 | return "http://dummy" |
320 | 365 | __call__ = __str__ | 362 | __call__ = __str__ |
321 | 366 | |||
322 | 367 | |||
323 | 368 | class SimpleOAuthDataStore(OAuthDataStore): | ||
324 | 369 | """A very simple implementation of the oauth library's OAuthDataStore.""" | ||
325 | 370 | |||
326 | 371 | def __init__(self, consumers={}, tokens={}): | ||
327 | 372 | """Initialize with no nonces.""" | ||
328 | 373 | self.consumers = consumers | ||
329 | 374 | self.tokens = tokens | ||
330 | 375 | self.nonces = set() | ||
331 | 376 | |||
332 | 377 | def lookup_token(self, token_type, token_field): | ||
333 | 378 | """Turn a token key into an OAuthToken object.""" | ||
334 | 379 | return self.tokens.get(token_field) | ||
335 | 380 | |||
336 | 381 | def lookup_consumer(self, consumer): | ||
337 | 382 | """Turn a consumer key into an OAuthConsumer object.""" | ||
338 | 383 | return self.consumers.get(consumer) | ||
339 | 384 | |||
340 | 385 | def lookup_nonce(self, consumer, token, nonce): | ||
341 | 386 | """Make sure a nonce has not already been used. | ||
342 | 387 | |||
343 | 388 | If the nonce has not been used, add it to the set | ||
344 | 389 | so that a future call to this method will return False. | ||
345 | 390 | """ | ||
346 | 391 | key = (consumer, token, nonce) | ||
347 | 392 | if key in self.nonces: | ||
348 | 393 | return True | ||
349 | 394 | self.nonces.add(key) | ||
350 | 395 | return False | ||
351 | 396 | 363 | ||
352 | === modified file 'src/lazr/restful/version.txt' | |||
353 | --- src/lazr/restful/version.txt 2009-10-06 16:09:51 +0000 | |||
354 | +++ src/lazr/restful/version.txt 2009-10-07 17:20:23 +0000 | |||
355 | @@ -1,1 +1,1 @@ | |||
357 | 1 | 0.9.8 | 1 | 0.9.9 |
358 | 2 | 2 | ||
359 | === modified file 'src/lazr/restful/wsgi.py' | |||
360 | --- src/lazr/restful/wsgi.py 2009-10-06 15:59:10 +0000 | |||
361 | +++ src/lazr/restful/wsgi.py 2009-10-07 17:20:23 +0000 | |||
362 | @@ -2,13 +2,10 @@ | |||
363 | 2 | 2 | ||
364 | 3 | __metaclass__ = type | 3 | __metaclass__ = type |
365 | 4 | __all__ = [ | 4 | __all__ = [ |
366 | 5 | 'BasicAuthMiddleware', | ||
367 | 6 | 'WSGIApplication', | 5 | 'WSGIApplication', |
368 | 7 | ] | 6 | ] |
369 | 8 | 7 | ||
370 | 9 | from pkg_resources import resource_string | 8 | from pkg_resources import resource_string |
371 | 10 | import re | ||
372 | 11 | import urlparse | ||
373 | 12 | from wsgiref.simple_server import make_server as wsgi_make_server | 9 | from wsgiref.simple_server import make_server as wsgi_make_server |
374 | 13 | 10 | ||
375 | 14 | from zope.component import getUtility | 11 | from zope.component import getUtility |
376 | @@ -16,9 +13,6 @@ | |||
377 | 16 | from zope.interface import implements | 13 | from zope.interface import implements |
378 | 17 | from zope.publisher.publish import publish | 14 | from zope.publisher.publish import publish |
379 | 18 | 15 | ||
380 | 19 | from oauth.oauth import ( | ||
381 | 20 | OAuthError, OAuthRequest, OAuthServer, OAuthSignatureMethod_PLAINTEXT) | ||
382 | 21 | |||
383 | 22 | from lazr.restful.interfaces import ( | 16 | from lazr.restful.interfaces import ( |
384 | 23 | IWebServiceConfiguration, IServiceRootResource) | 17 | IWebServiceConfiguration, IServiceRootResource) |
385 | 24 | from lazr.restful.simple import Publication, Request | 18 | from lazr.restful.simple import Publication, Request |
386 | @@ -66,141 +60,3 @@ | |||
387 | 66 | """Create a WSGI server object for a particular web service.""" | 60 | """Create a WSGI server object for a particular web service.""" |
388 | 67 | cls.configure_server(host, port, config_package, config_file) | 61 | cls.configure_server(host, port, config_package, config_file) |
389 | 68 | return wsgi_make_server(host, int(port), cls) | 62 | return wsgi_make_server(host, int(port), cls) |
390 | 69 | |||
391 | 70 | |||
392 | 71 | class AuthenticationMiddleware(object): | ||
393 | 72 | """A base class for middleware that authenticates HTTP requests. | ||
394 | 73 | |||
395 | 74 | This class implements a generic HTTP authentication workflow: | ||
396 | 75 | check whether the requested resource is protected, get credentials | ||
397 | 76 | from the WSGI environment, validate them (using a callback | ||
398 | 77 | function) and either allow or deny acces. | ||
399 | 78 | """ | ||
400 | 79 | |||
401 | 80 | def __init__(self, application, authenticate_with, | ||
402 | 81 | realm="Restricted area", protect_path_pattern='.*'): | ||
403 | 82 | """Constructor. | ||
404 | 83 | |||
405 | 84 | :param application: A WSGI application. | ||
406 | 85 | |||
407 | 86 | :param authenticate_with: A callback function that takes some | ||
408 | 87 | number of credential arguemnts (the number and type | ||
409 | 88 | depends on the implementation of | ||
410 | 89 | getCredentialsFromEnvironment()) and returns an object | ||
411 | 90 | representing the authenticated user. If the credentials | ||
412 | 91 | are invalid or don't identify any existing user, the | ||
413 | 92 | function should return None. | ||
414 | 93 | |||
415 | 94 | :param realm: The string to give out as the authentication realm. | ||
416 | 95 | :param protect_path_pattern: A regular expression string. URL | ||
417 | 96 | paths matching this string will be protected with the | ||
418 | 97 | authentication method. URL paths not matching this string | ||
419 | 98 | can be accessed without authenticating. | ||
420 | 99 | """ | ||
421 | 100 | self.application = application | ||
422 | 101 | self.authenticate_with = authenticate_with | ||
423 | 102 | self.realm = realm | ||
424 | 103 | self.protect_path_pattern = re.compile(protect_path_pattern) | ||
425 | 104 | |||
426 | 105 | def _unauthorized(self, start_response): | ||
427 | 106 | """Short-circuit the request with a 401 error code.""" | ||
428 | 107 | start_response("401 Unauthorized", | ||
429 | 108 | [('WWW-Authenticate', | ||
430 | 109 | 'Basic realm="%s"' % self.realm)]) | ||
431 | 110 | return ['401 Unauthorized'] | ||
432 | 111 | |||
433 | 112 | def __call__(self, environ, start_response): | ||
434 | 113 | """Protect certain resources by checking auth credentials.""" | ||
435 | 114 | path_info = environ.get('PATH_INFO', '/') | ||
436 | 115 | if not self.protect_path_pattern.match(path_info): | ||
437 | 116 | environ['authenticated_user'] = None | ||
438 | 117 | return self.application(environ, start_response) | ||
439 | 118 | |||
440 | 119 | try: | ||
441 | 120 | credentials = self.getCredentialsFromEnvironment(environ) | ||
442 | 121 | except ValueError: | ||
443 | 122 | credentials = None | ||
444 | 123 | if credentials is None: | ||
445 | 124 | return self._unauthorized(start_response) | ||
446 | 125 | |||
447 | 126 | authenticated_user = self.authenticate_with(*credentials) | ||
448 | 127 | if authenticated_user is None: | ||
449 | 128 | return self._unauthorized(start_response) | ||
450 | 129 | |||
451 | 130 | environ['authenticated_user'] = authenticated_user | ||
452 | 131 | |||
453 | 132 | return self.application(environ, start_response) | ||
454 | 133 | |||
455 | 134 | def getCredentialsFromEnvironment(self, environment): | ||
456 | 135 | """Retrieve a set of credentials from the environment. | ||
457 | 136 | |||
458 | 137 | This superclass implementation ignores the environment | ||
459 | 138 | entirely, and so never authenticates anybody. | ||
460 | 139 | |||
461 | 140 | :param environment: The WSGI environment. | ||
462 | 141 | :return: A list of objects to be passed into the authenticate_with | ||
463 | 142 | callback function, or None if the credentials could not | ||
464 | 143 | be determined. | ||
465 | 144 | """ | ||
466 | 145 | return None | ||
467 | 146 | |||
468 | 147 | |||
469 | 148 | class BasicAuthMiddleware(AuthenticationMiddleware): | ||
470 | 149 | """WSGI middleware that implements HTTP Basic Auth.""" | ||
471 | 150 | |||
472 | 151 | def getCredentialsFromEnvironment(self, environ): | ||
473 | 152 | authorization = environ.get('HTTP_AUTHORIZATION') | ||
474 | 153 | if authorization is None: | ||
475 | 154 | return None | ||
476 | 155 | |||
477 | 156 | method, auth = authorization.split(' ', 1) | ||
478 | 157 | if method.lower() != 'basic': | ||
479 | 158 | return None | ||
480 | 159 | |||
481 | 160 | auth = auth.strip().decode('base64') | ||
482 | 161 | username, password = auth.split(':', 1) | ||
483 | 162 | return username, password | ||
484 | 163 | |||
485 | 164 | |||
486 | 165 | class OAuthMiddleware(AuthenticationMiddleware): | ||
487 | 166 | """WSGI middleware that implements (part of) OAuth. | ||
488 | 167 | |||
489 | 168 | This middleware only protects resources by making sure requests | ||
490 | 169 | are signed with a valid consumer and access token. It does not | ||
491 | 170 | help clients get request tokens or exchange request tokens for | ||
492 | 171 | access tokens. | ||
493 | 172 | """ | ||
494 | 173 | |||
495 | 174 | def __init__(self, application, authenticate_with, data_store=None, | ||
496 | 175 | realm="Restricted area", protect_path_pattern='.*'): | ||
497 | 176 | """See `AuthenticationMiddleware.` | ||
498 | 177 | |||
499 | 178 | :param data_store: An OAuthDataStore. | ||
500 | 179 | """ | ||
501 | 180 | super(OAuthMiddleware, self).__init__( | ||
502 | 181 | application, authenticate_with, realm, protect_path_pattern) | ||
503 | 182 | self.data_store = data_store | ||
504 | 183 | |||
505 | 184 | def getCredentialsFromEnvironment(self, environ): | ||
506 | 185 | http_method = environ['REQUEST_METHOD'] | ||
507 | 186 | |||
508 | 187 | # Recreate the URL. | ||
509 | 188 | url_scheme = environ['wsgi.url_scheme'] | ||
510 | 189 | hostname = environ['HTTP_HOST'] | ||
511 | 190 | path = environ['PATH_INFO'] | ||
512 | 191 | query_string = environ['QUERY_STRING'] | ||
513 | 192 | original_url = urlparse.urlunparse( | ||
514 | 193 | (url_scheme, hostname, path, '', query_string, '')) | ||
515 | 194 | headers = {'Authorization' : environ.get('HTTP_AUTHORIZATION', '')} | ||
516 | 195 | request = OAuthRequest().from_request( | ||
517 | 196 | http_method, original_url, headers=headers, | ||
518 | 197 | query_string=query_string) | ||
519 | 198 | if request is None: | ||
520 | 199 | return None | ||
521 | 200 | server = OAuthServer(self.data_store) | ||
522 | 201 | server.add_signature_method(OAuthSignatureMethod_PLAINTEXT()) | ||
523 | 202 | try: | ||
524 | 203 | consumer, token, parameters = server.verify_request(request) | ||
525 | 204 | except OAuthError, e: | ||
526 | 205 | return None | ||
527 | 206 | return consumer, token, parameters |
This branch forward-ports a branch that intellectronica accidentally landed on the now-defunct PQM-managed lazr.restful branch.