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
1=== modified file 'setup.py'
2--- setup.py 2009-09-16 13:54:11 +0000
3+++ setup.py 2009-09-29 19:40:26 +0000
4@@ -56,7 +56,7 @@
5 license='LGPL v3',
6 install_requires=[
7 'httplib2',
8- 'lazr.restful>=0.9.6',
9+ 'lazr.restful>=0.9.8',
10 'setuptools',
11 'wadllib>=1.1.4',
12 'wsgi_intercept',
13
14=== modified file 'src/lazr/restfulclient/NEWS.txt'
15--- src/lazr/restfulclient/NEWS.txt 2009-09-16 13:54:53 +0000
16+++ src/lazr/restfulclient/NEWS.txt 2009-09-29 19:40:26 +0000
17@@ -2,6 +2,11 @@
18 NEWS for lazr.restfulclient
19 ===========================
20
21+0.9.7 (2009-09-30)
22+==================
23+
24+- Added support for HTTP Basic Auth.
25+
26 0.9.6 (2009-09-16)
27 ==================
28
29
30=== modified file 'src/lazr/restfulclient/_browser.py'
31--- src/lazr/restfulclient/_browser.py 2009-03-26 20:50:28 +0000
32+++ src/lazr/restfulclient/_browser.py 2009-09-29 19:40:26 +0000
33@@ -25,7 +25,9 @@
34
35 __metaclass__ = type
36 __all__ = [
37+ 'BasicHttpAuthorizer',
38 'Browser',
39+ 'HttpAuthorizer',
40 'RestfulHttp',
41 ]
42
43@@ -77,13 +79,12 @@
44 react when its cache is a MultipleRepresentationCache.
45 """
46
47- def __init__(self, credentials, cache=None, timeout=None,
48+ def __init__(self, authorizer=None, cache=None, timeout=None,
49 proxy_info=None):
50 super(RestfulHttp, self).__init__(cache, timeout, proxy_info)
51- # The credentials are not used in this class, but you can
52- # use them in a subclass.
53- self.restful_credentials = credentials
54-
55+ self.authorizer = authorizer
56+ if self.authorizer is not None:
57+ self.authorizer.authorizeSession(self)
58
59 def _request(self, conn, host, absolute_uri, request_uri, method, body,
60 headers, redirections, cachekey):
61@@ -103,6 +104,14 @@
62 if 'accept-encoding' in headers:
63 headers['te'] = 'deflate, gzip'
64 del headers['accept-encoding']
65+ if headers.has_key('authorization'):
66+ # There's an authorization header left over from a
67+ # previous request that resulted in a redirect. Remove it
68+ # and start again.
69+ del headers['authorization']
70+ if self.authorizer is not None:
71+ self.authorizer.authorizeRequest(
72+ absolute_uri, method, body, headers)
73 return super(RestfulHttp, self)._request(
74 conn, host, absolute_uri, request_uri, method, body, headers,
75 redirections, cachekey)
76@@ -123,6 +132,47 @@
77 return None
78
79
80+class HttpAuthorizer:
81+ """Handles authentication for HTTP requests.
82+
83+ There are two ways to authenticate.
84+
85+ The authorize_session() method is called once when the client is
86+ initialized. This works for authentication methods like Basic
87+ Auth. The authorize_request is called for every HTTP request,
88+ which is useful for authentication methods like Digest and OAuth.
89+
90+ The base class is a null authorizer which does not perform any
91+ authentication at all.
92+ """
93+ def authorizeSession(self, client):
94+ """Set up credentials for the entire session."""
95+ pass
96+
97+ def authorizeRequest(self, absolute_uri, method, body, headers):
98+ """Set up credentials for a single request.
99+
100+ This probably involves setting the Authentication header.
101+ """
102+ pass
103+
104+
105+class BasicHttpAuthorizer(HttpAuthorizer):
106+ """Handles authentication for services that use HTTP Basic Auth."""
107+
108+ def __init__(self, username, password):
109+ """Constructor.
110+
111+ :param username: User to send as authorization for all requests.
112+ :param password: Password to send as authorization for all requests.
113+ """
114+ self.username = username
115+ self.password = password
116+
117+ def authorizeSession(self, client):
118+ client.add_credentials(self.username, self.password)
119+
120+
121 class MultipleRepresentationCache(FileCache):
122 """A cache that can hold different representations of the same resource.
123
124
125=== added file 'src/lazr/restfulclient/docs/authorizer.txt'
126--- src/lazr/restfulclient/docs/authorizer.txt 1970-01-01 00:00:00 +0000
127+++ src/lazr/restfulclient/docs/authorizer.txt 2009-09-29 19:40:26 +0000
128@@ -0,0 +1,81 @@
129+Authorizers
130+===========
131+
132+Authorizers are objects that encapsulate knowledge about a particular
133+web service's authentication scheme. lazr.restfulclient includes
134+authorizers for common HTTP authentication schemes.
135+
136+The BasicHttpRequestAuthorizer
137+------------------------------
138+
139+This authorizer handles HTTP Basic Auth. To test it, we'll create a
140+fake web service that serves some dummy WADL.
141+
142+ >>> import pkg_resources
143+ >>> wadl_string = pkg_resources.resource_string(
144+ ... 'wadllib.tests.data', 'launchpad-wadl.xml')
145+
146+ >>> def dummy_application(environ, start_response):
147+ ... start_response(
148+ ... '200', [('Content-type','application/vnd.sun.wadl+xml')])
149+ ... return [wadl_string]
150+
151+
152+The WADL file will be protected with HTTP Basic Auth. To access it,
153+you'll need to provide a username of "user" and a password of
154+"password".
155+
156+ >>> def authenticate(username, password):
157+ ... """Accepts "user/password", rejects everything else.
158+ ...
159+ ... :return: The username, if the credentials are valid.
160+ ... None, otherwise.
161+ ... """
162+ ... if username == "user" and password == "password":
163+ ... return username
164+ ... return None
165+
166+ >>> from lazr.restful.wsgi import BasicAuthMiddleware
167+ >>> def protected_application():
168+ ... return BasicAuthMiddleware(
169+ ... dummy_application, authenticate_with=authenticate)
170+
171+Finally, we'll set up a WSGI intercept so that we can test the web
172+service by making HTTP requests to http://api.launchpad.dev/. (This is
173+the hostname mentioned in the WADL file.)
174+
175+ >>> import wsgi_intercept
176+ >>> from wsgi_intercept.httplib2_intercept import install
177+ >>> install()
178+ >>> wsgi_intercept.add_wsgi_intercept(
179+ ... 'api.launchpad.dev', 80, protected_application)
180+
181+With no HttpAuthorizer, a ServiceRoot can't get access to the web service.
182+
183+ >>> from lazr.restfulclient.resource import ServiceRoot
184+ >>> client = ServiceRoot(None, "http://api.launchpad.dev/")
185+ Traceback (most recent call last):
186+ ...
187+ HTTPError: HTTP Error 401: Unauthorized
188+
189+We can't get access if the authorizer doesn't have the right
190+credentials.
191+
192+ >>> from lazr.restfulclient._browser import BasicHttpAuthorizer
193+
194+ >>> bad_authorizer = BasicHttpAuthorizer("baduser", "badpassword")
195+ >>> client = ServiceRoot(bad_authorizer, "http://api.launchpad.dev/")
196+ Traceback (most recent call last):
197+ ...
198+ HTTPError: HTTP Error 401: Unauthorized
199+
200+If we provide the right credentials, we can retrieve the WADL. We'll
201+still get an exception, because our fake web service is too fake for
202+ServiceRoot--it doesn't serve any JSON resources--but we're able to
203+make HTTP requests without getting 401 errors.
204+
205+ >>> authorizer = BasicHttpAuthorizer("user", "password")
206+ >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
207+ Traceback (most recent call last):
208+ ...
209+ ValueError: No JSON object could be decoded
210
211=== modified file 'src/lazr/restfulclient/resource.py'
212--- src/lazr/restfulclient/resource.py 2009-08-25 20:36:47 +0000
213+++ src/lazr/restfulclient/resource.py 2009-09-29 19:40:26 +0000
214@@ -363,7 +363,7 @@
215 # instantiating resources of a certain WADL type.
216 RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile}
217
218- def __init__(self, credentials, service_root, cache=None,
219+ def __init__(self, authorizer, service_root, cache=None,
220 timeout=None, proxy_info=None):
221 """Root access to a lazr.restful API.
222
223@@ -372,10 +372,9 @@
224 :type service_root: string
225 """
226 self._root_uri = URI(service_root)
227- self.credentials = credentials
228 # Get the WADL definition.
229 self._browser = Browser(
230- self, self.credentials, cache, timeout, proxy_info)
231+ self, authorizer, cache, timeout, proxy_info)
232 self._wadl = self._browser.get_wadl_application(self._root_uri)
233
234 # Get the root resource.
235@@ -384,8 +383,8 @@
236 self._browser.get(root_resource), 'application/json')
237 super(ServiceRoot, self).__init__(None, bound_root)
238
239- def httpFactory(self, credentials, cache, timeout, proxy_info):
240- return RestfulHttp(credentials, cache, timeout, proxy_info)
241+ def httpFactory(self, authorizer, cache, timeout, proxy_info):
242+ return RestfulHttp(authorizer, cache, timeout, proxy_info)
243
244 def load(self, url):
245 """Load a resource given its URL."""
246
247=== modified file 'src/lazr/restfulclient/version.txt'
248--- src/lazr/restfulclient/version.txt 2009-09-16 13:54:11 +0000
249+++ src/lazr/restfulclient/version.txt 2009-09-29 19:40:26 +0000
250@@ -1,1 +1,1 @@
251-0.9.6
252+0.9.7

Subscribers

People subscribed via source and target branches