Merge lp:~leonardr/lazr.restful/remove-authentication into lp:lazr.restful
- remove-authentication
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella | Approve | ||
Review via email: mp+12991@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
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 |
This branch removes WSGI middleware from lazr.restful that was moved into lazr.authentica tion. 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.