Merge lp:~leonardr/lazr.restfulclient/basic-auth into lp:lazr.restfulclient

Proposed by Leonard Richardson
Status: Merged
Approved by: Brad Crittenden
Approved revision: 77
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restfulclient/basic-auth
Merge into: lp:lazr.restfulclient
Diff against target: 252 lines
6 files modified
setup.py (+1/-1)
src/lazr/restfulclient/NEWS.txt (+5/-0)
src/lazr/restfulclient/_browser.py (+55/-5)
src/lazr/restfulclient/docs/authorizer.txt (+81/-0)
src/lazr/restfulclient/resource.py (+4/-5)
src/lazr/restfulclient/version.txt (+1/-1)
To merge this branch: bzr merge lp:~leonardr/lazr.restfulclient/basic-auth
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+12612@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch changes the way lazr.restfulclient does (or doesn't do) authentication. Previously, the ServiceRoot constructor took a "credentials" argument that didn't do anything, but that could be used by subclasses. This branch introduces the idea of "authorizers", objects that are used at specific points in the lifecycle to make sure HTTP requests are made with the correct Authorization header.

This change makes it easy to add code for common authorization techniques to lazr.restfulclient. The only authorizers in this branch are one that doesn't do anything (only useful as a superclass) and one that supports HTTP Basic Auth. I'll be moving in the OAuth authorization code from launchpadlib as an authorizer.

Revision history for this message
Brad Crittenden (bac) wrote :

Very nice branch Leonard.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'setup.py'
--- setup.py 2009-09-16 13:54:11 +0000
+++ setup.py 2009-09-29 19:40:26 +0000
@@ -56,7 +56,7 @@
56 license='LGPL v3',56 license='LGPL v3',
57 install_requires=[57 install_requires=[
58 'httplib2',58 'httplib2',
59 'lazr.restful>=0.9.6',59 'lazr.restful>=0.9.8',
60 'setuptools',60 'setuptools',
61 'wadllib>=1.1.4',61 'wadllib>=1.1.4',
62 'wsgi_intercept',62 'wsgi_intercept',
6363
=== modified file 'src/lazr/restfulclient/NEWS.txt'
--- src/lazr/restfulclient/NEWS.txt 2009-09-16 13:54:53 +0000
+++ src/lazr/restfulclient/NEWS.txt 2009-09-29 19:40:26 +0000
@@ -2,6 +2,11 @@
2NEWS for lazr.restfulclient2NEWS for lazr.restfulclient
3===========================3===========================
44
50.9.7 (2009-09-30)
6==================
7
8- Added support for HTTP Basic Auth.
9
50.9.6 (2009-09-16)100.9.6 (2009-09-16)
6==================11==================
712
813
=== modified file 'src/lazr/restfulclient/_browser.py'
--- src/lazr/restfulclient/_browser.py 2009-03-26 20:50:28 +0000
+++ src/lazr/restfulclient/_browser.py 2009-09-29 19:40:26 +0000
@@ -25,7 +25,9 @@
2525
26__metaclass__ = type26__metaclass__ = type
27__all__ = [27__all__ = [
28 'BasicHttpAuthorizer',
28 'Browser',29 'Browser',
30 'HttpAuthorizer',
29 'RestfulHttp',31 'RestfulHttp',
30 ]32 ]
3133
@@ -77,13 +79,12 @@
77 react when its cache is a MultipleRepresentationCache.79 react when its cache is a MultipleRepresentationCache.
78 """80 """
7981
80 def __init__(self, credentials, cache=None, timeout=None,82 def __init__(self, authorizer=None, cache=None, timeout=None,
81 proxy_info=None):83 proxy_info=None):
82 super(RestfulHttp, self).__init__(cache, timeout, proxy_info)84 super(RestfulHttp, self).__init__(cache, timeout, proxy_info)
83 # The credentials are not used in this class, but you can85 self.authorizer = authorizer
84 # use them in a subclass.86 if self.authorizer is not None:
85 self.restful_credentials = credentials87 self.authorizer.authorizeSession(self)
86
8788
88 def _request(self, conn, host, absolute_uri, request_uri, method, body,89 def _request(self, conn, host, absolute_uri, request_uri, method, body,
89 headers, redirections, cachekey):90 headers, redirections, cachekey):
@@ -103,6 +104,14 @@
103 if 'accept-encoding' in headers:104 if 'accept-encoding' in headers:
104 headers['te'] = 'deflate, gzip'105 headers['te'] = 'deflate, gzip'
105 del headers['accept-encoding']106 del headers['accept-encoding']
107 if headers.has_key('authorization'):
108 # There's an authorization header left over from a
109 # previous request that resulted in a redirect. Remove it
110 # and start again.
111 del headers['authorization']
112 if self.authorizer is not None:
113 self.authorizer.authorizeRequest(
114 absolute_uri, method, body, headers)
106 return super(RestfulHttp, self)._request(115 return super(RestfulHttp, self)._request(
107 conn, host, absolute_uri, request_uri, method, body, headers,116 conn, host, absolute_uri, request_uri, method, body, headers,
108 redirections, cachekey)117 redirections, cachekey)
@@ -123,6 +132,47 @@
123 return None132 return None
124133
125134
135class HttpAuthorizer:
136 """Handles authentication for HTTP requests.
137
138 There are two ways to authenticate.
139
140 The authorize_session() method is called once when the client is
141 initialized. This works for authentication methods like Basic
142 Auth. The authorize_request is called for every HTTP request,
143 which is useful for authentication methods like Digest and OAuth.
144
145 The base class is a null authorizer which does not perform any
146 authentication at all.
147 """
148 def authorizeSession(self, client):
149 """Set up credentials for the entire session."""
150 pass
151
152 def authorizeRequest(self, absolute_uri, method, body, headers):
153 """Set up credentials for a single request.
154
155 This probably involves setting the Authentication header.
156 """
157 pass
158
159
160class BasicHttpAuthorizer(HttpAuthorizer):
161 """Handles authentication for services that use HTTP Basic Auth."""
162
163 def __init__(self, username, password):
164 """Constructor.
165
166 :param username: User to send as authorization for all requests.
167 :param password: Password to send as authorization for all requests.
168 """
169 self.username = username
170 self.password = password
171
172 def authorizeSession(self, client):
173 client.add_credentials(self.username, self.password)
174
175
126class MultipleRepresentationCache(FileCache):176class MultipleRepresentationCache(FileCache):
127 """A cache that can hold different representations of the same resource.177 """A cache that can hold different representations of the same resource.
128178
129179
=== added file 'src/lazr/restfulclient/docs/authorizer.txt'
--- src/lazr/restfulclient/docs/authorizer.txt 1970-01-01 00:00:00 +0000
+++ src/lazr/restfulclient/docs/authorizer.txt 2009-09-29 19:40:26 +0000
@@ -0,0 +1,81 @@
1Authorizers
2===========
3
4Authorizers are objects that encapsulate knowledge about a particular
5web service's authentication scheme. lazr.restfulclient includes
6authorizers for common HTTP authentication schemes.
7
8The BasicHttpRequestAuthorizer
9------------------------------
10
11This authorizer handles HTTP Basic Auth. To test it, we'll create a
12fake web service that serves some dummy WADL.
13
14 >>> import pkg_resources
15 >>> wadl_string = pkg_resources.resource_string(
16 ... 'wadllib.tests.data', 'launchpad-wadl.xml')
17
18 >>> def dummy_application(environ, start_response):
19 ... start_response(
20 ... '200', [('Content-type','application/vnd.sun.wadl+xml')])
21 ... return [wadl_string]
22
23
24The WADL file will be protected with HTTP Basic Auth. To access it,
25you'll need to provide a username of "user" and a password of
26"password".
27
28 >>> def authenticate(username, password):
29 ... """Accepts "user/password", rejects everything else.
30 ...
31 ... :return: The username, if the credentials are valid.
32 ... None, otherwise.
33 ... """
34 ... if username == "user" and password == "password":
35 ... return username
36 ... return None
37
38 >>> from lazr.restful.wsgi import BasicAuthMiddleware
39 >>> def protected_application():
40 ... return BasicAuthMiddleware(
41 ... dummy_application, authenticate_with=authenticate)
42
43Finally, we'll set up a WSGI intercept so that we can test the web
44service by making HTTP requests to http://api.launchpad.dev/. (This is
45the hostname mentioned in the WADL file.)
46
47 >>> import wsgi_intercept
48 >>> from wsgi_intercept.httplib2_intercept import install
49 >>> install()
50 >>> wsgi_intercept.add_wsgi_intercept(
51 ... 'api.launchpad.dev', 80, protected_application)
52
53With no HttpAuthorizer, a ServiceRoot can't get access to the web service.
54
55 >>> from lazr.restfulclient.resource import ServiceRoot
56 >>> client = ServiceRoot(None, "http://api.launchpad.dev/")
57 Traceback (most recent call last):
58 ...
59 HTTPError: HTTP Error 401: Unauthorized
60
61We can't get access if the authorizer doesn't have the right
62credentials.
63
64 >>> from lazr.restfulclient._browser import BasicHttpAuthorizer
65
66 >>> bad_authorizer = BasicHttpAuthorizer("baduser", "badpassword")
67 >>> client = ServiceRoot(bad_authorizer, "http://api.launchpad.dev/")
68 Traceback (most recent call last):
69 ...
70 HTTPError: HTTP Error 401: Unauthorized
71
72If we provide the right credentials, we can retrieve the WADL. We'll
73still get an exception, because our fake web service is too fake for
74ServiceRoot--it doesn't serve any JSON resources--but we're able to
75make HTTP requests without getting 401 errors.
76
77 >>> authorizer = BasicHttpAuthorizer("user", "password")
78 >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
79 Traceback (most recent call last):
80 ...
81 ValueError: No JSON object could be decoded
082
=== modified file 'src/lazr/restfulclient/resource.py'
--- src/lazr/restfulclient/resource.py 2009-08-25 20:36:47 +0000
+++ src/lazr/restfulclient/resource.py 2009-09-29 19:40:26 +0000
@@ -363,7 +363,7 @@
363 # instantiating resources of a certain WADL type.363 # instantiating resources of a certain WADL type.
364 RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile}364 RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile}
365365
366 def __init__(self, credentials, service_root, cache=None,366 def __init__(self, authorizer, service_root, cache=None,
367 timeout=None, proxy_info=None):367 timeout=None, proxy_info=None):
368 """Root access to a lazr.restful API.368 """Root access to a lazr.restful API.
369369
@@ -372,10 +372,9 @@
372 :type service_root: string372 :type service_root: string
373 """373 """
374 self._root_uri = URI(service_root)374 self._root_uri = URI(service_root)
375 self.credentials = credentials
376 # Get the WADL definition.375 # Get the WADL definition.
377 self._browser = Browser(376 self._browser = Browser(
378 self, self.credentials, cache, timeout, proxy_info)377 self, authorizer, cache, timeout, proxy_info)
379 self._wadl = self._browser.get_wadl_application(self._root_uri)378 self._wadl = self._browser.get_wadl_application(self._root_uri)
380379
381 # Get the root resource.380 # Get the root resource.
@@ -384,8 +383,8 @@
384 self._browser.get(root_resource), 'application/json')383 self._browser.get(root_resource), 'application/json')
385 super(ServiceRoot, self).__init__(None, bound_root)384 super(ServiceRoot, self).__init__(None, bound_root)
386385
387 def httpFactory(self, credentials, cache, timeout, proxy_info):386 def httpFactory(self, authorizer, cache, timeout, proxy_info):
388 return RestfulHttp(credentials, cache, timeout, proxy_info)387 return RestfulHttp(authorizer, cache, timeout, proxy_info)
389388
390 def load(self, url):389 def load(self, url):
391 """Load a resource given its URL."""390 """Load a resource given its URL."""
392391
=== modified file 'src/lazr/restfulclient/version.txt'
--- src/lazr/restfulclient/version.txt 2009-09-16 13:54:11 +0000
+++ src/lazr/restfulclient/version.txt 2009-09-29 19:40:26 +0000
@@ -1,1 +1,1 @@
10.9.610.9.7

Subscribers

People subscribed via source and target branches