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