Merge lp:~leonardr/lazr.restful/remove-authentication into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/remove-authentication
Merge into: lp:lazr.restful
Diff against target: 490 lines
6 files modified
setup.py (+0/-1)
src/lazr/restful/NEWS.txt (+6/-0)
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/remove-authentication
Reviewer Review Type Date Requested Status
Gavin Panella Approve
Review via email: mp+12991@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch removes WSGI middleware from lazr.restful that was moved into lazr.authentication. The code is not used within lazr.restful itself (which is why it was moved), so there's no effect on the main body of code.

Revision history for this message
Gavin Panella (allenap) :
review: Approve

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

Subscribers

People subscribed via source and target branches