Merge lp:~leonardr/lazr.restful/forward-port into lp:lazr.restful

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
Reviewer Review Type Date Requested Status
Deryck Hodge (community) code Approve
Review via email: mp+13009@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch forward-ports a branch that intellectronica accidentally landed on the now-defunct PQM-managed lazr.restful branch.

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

Subscribers

People subscribed via source and target branches