Merge lp:~mbp/launchpad/701545-oauth into lp:launchpad
- 701545-oauth
- Merge into devel
Status: | Rejected | ||||
---|---|---|---|---|---|
Rejected by: | Martin Pool | ||||
Proposed branch: | lp:~mbp/launchpad/701545-oauth | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
624 lines (+9/-537) 6 files modified
lib/canonical/launchpad/webapp/authentication.py (+2/-2) lib/canonical/launchpad/webapp/tests/test_authentication.py (+5/-2) lib/canonical/launchpad/webapp/tests/test_publication.py (+2/-2) lib/contrib/oauth.py (+0/-529) setup.py (+0/-1) versions.cfg (+0/-1) |
||||
To merge this branch: | bzr merge lp:~mbp/launchpad/701545-oauth | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
j.c.sackett | code* | Pending | |
Review via email: mp+49541@code.launchpad.net |
This proposal supersedes a proposal from 2011-02-03.
Commit message
Description of the change
Remove redundant copy of oauth.py.
Launchpad contains a copy of oauth.py, which is a bit out of date. It also has a proper copy of the python-oauth module in lp-sourcedeps.
This was previously attempted in <https:/
This can't/shouldn't land until the meta-lp-deps landing has been pushed all the way through.
The actual patch is similar to what was previously submitted with the addition of removing the setuptools dependency
j.c.sackett (jcsackett) wrote : Posted in a previous version of this proposal | # |
Curtis Hovey (sinzui) wrote : Posted in a previous version of this proposal | # |
Hi Martin.
Thank you for cleaning up the code. We normally write copyrights as a range. Are you aware of utilities/
Francis J. Lacoste (flacoste) wrote : Posted in a previous version of this proposal | # |
On February 3, 2011, Martin Pool wrote:
> If this is acceptable would someone please sponsor landing of it?
All Canonical Bazaar Developers can land branches directly through PQM
nowawayds :-)
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
Thanks, Curtis. I was not aware of that. I will send this to pqm.
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
On 4 February 2011 03:52, Francis J. Lacoste <email address hidden> wrote:
> On February 3, 2011, Martin Pool wrote:
>> If this is acceptable would someone please sponsor landing of it?
>
> All Canonical Bazaar Developers can land branches directly through PQM
> nowawayds :-)
No, I still get a gpgv error from pqm.
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
spm helped me sort this out and it's in the queue now.
- Martin
On 04/02/2011 11:35 AM, "Martin Pool" <email address hidden> wrote:
> On 4 February 2011 03:52, Francis J. Lacoste <email address hidden>
wrote:
>> On February 3, 2011, Martin Pool wrote:
>>> If this is acceptable would someone please sponsor landing of it?
>>
>> All Canonical Bazaar Developers can land branches directly through PQM
>> nowawayds :-)
>
> No, I still get a gpgv error from pqm.
- 12370. By Martin Pool
-
Also remove oauth from versions.cfg
Curtis Hovey (sinzui) wrote : | # |
This is good to land once the deps are in place.
Martin Pool (mbp) wrote : | # |
Per my comments on bug 701545, getting the dependencies to work is too hard to be worthwhile.
Unmerged revisions
- 12370. By Martin Pool
-
Also remove oauth from versions.cfg
- 12369. By Martin Pool
-
Use external python-oauth and remove contrib/oauth.py
- 12368. By Martin Pool
-
Remove dependency on oauth egg
Preview Diff
1 | === modified file 'lib/canonical/launchpad/webapp/authentication.py' | |||
2 | --- lib/canonical/launchpad/webapp/authentication.py 2011-02-04 14:41:18 +0000 | |||
3 | +++ lib/canonical/launchpad/webapp/authentication.py 2011-02-13 12:17:26 +0000 | |||
4 | @@ -1,4 +1,4 @@ | |||
6 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
10 | @@ -18,7 +18,7 @@ | |||
11 | 18 | import random | 18 | import random |
12 | 19 | from UserDict import UserDict | 19 | from UserDict import UserDict |
13 | 20 | 20 | ||
15 | 21 | from contrib.oauth import OAuthRequest | 21 | from oauth.oauth import OAuthRequest |
16 | 22 | from zope.annotation.interfaces import IAnnotations | 22 | from zope.annotation.interfaces import IAnnotations |
17 | 23 | from zope.app.security.interfaces import ILoginPassword | 23 | from zope.app.security.interfaces import ILoginPassword |
18 | 24 | from zope.app.security.principalregistry import UnauthenticatedPrincipal | 24 | from zope.app.security.principalregistry import UnauthenticatedPrincipal |
19 | 25 | 25 | ||
20 | === modified file 'lib/canonical/launchpad/webapp/tests/test_authentication.py' | |||
21 | --- lib/canonical/launchpad/webapp/tests/test_authentication.py 2011-02-04 14:41:18 +0000 | |||
22 | +++ lib/canonical/launchpad/webapp/tests/test_authentication.py 2011-02-13 12:17:26 +0000 | |||
23 | @@ -1,4 +1,4 @@ | |||
25 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
26 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
27 | 3 | 3 | ||
28 | 4 | """Tests authentication.py""" | 4 | """Tests authentication.py""" |
29 | @@ -10,7 +10,7 @@ | |||
30 | 10 | 10 | ||
31 | 11 | from zope.app.security.principalregistry import UnauthenticatedPrincipal | 11 | from zope.app.security.principalregistry import UnauthenticatedPrincipal |
32 | 12 | 12 | ||
34 | 13 | from contrib.oauth import OAuthRequest | 13 | from oauth.oauth import OAuthRequest |
35 | 14 | 14 | ||
36 | 15 | from canonical.config import config | 15 | from canonical.config import config |
37 | 16 | from canonical.launchpad.ftests import login | 16 | from canonical.launchpad.ftests import login |
38 | @@ -67,6 +67,9 @@ | |||
39 | 67 | # | 67 | # |
40 | 68 | # Note that the 'realm' parameter is not returned, because it's not | 68 | # Note that the 'realm' parameter is not returned, because it's not |
41 | 69 | # included in the OAuth calculations. | 69 | # included in the OAuth calculations. |
42 | 70 | # | ||
43 | 71 | # Now we use the separate oauth module (bug 701545), and | ||
44 | 72 | # this has been fixed upstream, but we might as well keep the test. | ||
45 | 70 | headers = OAuthRequest._split_header( | 73 | headers = OAuthRequest._split_header( |
46 | 71 | 'OAuth realm="foo", oauth_consumer_key="justtesting"') | 74 | 'OAuth realm="foo", oauth_consumer_key="justtesting"') |
47 | 72 | self.assertEquals(headers, | 75 | self.assertEquals(headers, |
48 | 73 | 76 | ||
49 | === modified file 'lib/canonical/launchpad/webapp/tests/test_publication.py' | |||
50 | --- lib/canonical/launchpad/webapp/tests/test_publication.py 2011-02-04 14:41:18 +0000 | |||
51 | +++ lib/canonical/launchpad/webapp/tests/test_publication.py 2011-02-13 12:17:26 +0000 | |||
52 | @@ -1,4 +1,4 @@ | |||
54 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
55 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
56 | 3 | 3 | ||
57 | 4 | """Tests publication.py""" | 4 | """Tests publication.py""" |
58 | @@ -9,7 +9,7 @@ | |||
59 | 9 | import sys | 9 | import sys |
60 | 10 | import unittest | 10 | import unittest |
61 | 11 | 11 | ||
63 | 12 | from contrib.oauth import ( | 12 | from oauth.oauth import ( |
64 | 13 | OAuthRequest, | 13 | OAuthRequest, |
65 | 14 | OAuthSignatureMethod_PLAINTEXT, | 14 | OAuthSignatureMethod_PLAINTEXT, |
66 | 15 | ) | 15 | ) |
67 | 16 | 16 | ||
68 | === removed file 'lib/contrib/oauth.py' | |||
69 | --- lib/contrib/oauth.py 2011-02-04 14:41:18 +0000 | |||
70 | +++ lib/contrib/oauth.py 1970-01-01 00:00:00 +0000 | |||
71 | @@ -1,529 +0,0 @@ | |||
72 | 1 | # pylint: disable-msg=C0301,E0602,E0211,E0213,W0105,W0231,W0702 | ||
73 | 2 | |||
74 | 3 | import cgi | ||
75 | 4 | import urllib | ||
76 | 5 | import time | ||
77 | 6 | import random | ||
78 | 7 | import urlparse | ||
79 | 8 | import hmac | ||
80 | 9 | import base64 | ||
81 | 10 | |||
82 | 11 | VERSION = '1.0' # Hi Blaine! | ||
83 | 12 | HTTP_METHOD = 'GET' | ||
84 | 13 | SIGNATURE_METHOD = 'PLAINTEXT' | ||
85 | 14 | |||
86 | 15 | # Generic exception class | ||
87 | 16 | class OAuthError(RuntimeError): | ||
88 | 17 | def __init__(self, message='OAuth error occured'): | ||
89 | 18 | self.message = message | ||
90 | 19 | |||
91 | 20 | # optional WWW-Authenticate header (401 error) | ||
92 | 21 | def build_authenticate_header(realm=''): | ||
93 | 22 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} | ||
94 | 23 | |||
95 | 24 | # url escape | ||
96 | 25 | def escape(s): | ||
97 | 26 | # escape '/' too | ||
98 | 27 | return urllib.quote(s, safe='~') | ||
99 | 28 | |||
100 | 29 | # util function: current timestamp | ||
101 | 30 | # seconds since epoch (UTC) | ||
102 | 31 | def generate_timestamp(): | ||
103 | 32 | return int(time.time()) | ||
104 | 33 | |||
105 | 34 | # util function: nonce | ||
106 | 35 | # pseudorandom number | ||
107 | 36 | def generate_nonce(length=8): | ||
108 | 37 | return ''.join(str(random.randint(0, 9)) for i in range(length)) | ||
109 | 38 | |||
110 | 39 | # OAuthConsumer is a data type that represents the identity of the Consumer | ||
111 | 40 | # via its shared secret with the Service Provider. | ||
112 | 41 | class OAuthConsumer(object): | ||
113 | 42 | key = None | ||
114 | 43 | secret = None | ||
115 | 44 | |||
116 | 45 | def __init__(self, key, secret): | ||
117 | 46 | self.key = key | ||
118 | 47 | self.secret = secret | ||
119 | 48 | |||
120 | 49 | # OAuthToken is a data type that represents an End User via either an access | ||
121 | 50 | # or request token. | ||
122 | 51 | class OAuthToken(object): | ||
123 | 52 | # access tokens and request tokens | ||
124 | 53 | key = None | ||
125 | 54 | secret = None | ||
126 | 55 | |||
127 | 56 | ''' | ||
128 | 57 | key = the token | ||
129 | 58 | secret = the token secret | ||
130 | 59 | ''' | ||
131 | 60 | def __init__(self, key, secret): | ||
132 | 61 | self.key = key | ||
133 | 62 | self.secret = secret | ||
134 | 63 | |||
135 | 64 | def to_string(self): | ||
136 | 65 | return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) | ||
137 | 66 | |||
138 | 67 | # return a token from something like: | ||
139 | 68 | # oauth_token_secret=digg&oauth_token=digg | ||
140 | 69 | @staticmethod | ||
141 | 70 | def from_string(s): | ||
142 | 71 | params = cgi.parse_qs(s, keep_blank_values=False) | ||
143 | 72 | key = params['oauth_token'][0] | ||
144 | 73 | secret = params['oauth_token_secret'][0] | ||
145 | 74 | return OAuthToken(key, secret) | ||
146 | 75 | |||
147 | 76 | def __str__(self): | ||
148 | 77 | return self.to_string() | ||
149 | 78 | |||
150 | 79 | # OAuthRequest represents the request and can be serialized | ||
151 | 80 | class OAuthRequest(object): | ||
152 | 81 | ''' | ||
153 | 82 | OAuth parameters: | ||
154 | 83 | - oauth_consumer_key | ||
155 | 84 | - oauth_token | ||
156 | 85 | - oauth_signature_method | ||
157 | 86 | - oauth_signature | ||
158 | 87 | - oauth_timestamp | ||
159 | 88 | - oauth_nonce | ||
160 | 89 | - oauth_version | ||
161 | 90 | ... any additional parameters, as defined by the Service Provider. | ||
162 | 91 | ''' | ||
163 | 92 | parameters = None # oauth parameters | ||
164 | 93 | http_method = HTTP_METHOD | ||
165 | 94 | http_url = None | ||
166 | 95 | version = VERSION | ||
167 | 96 | |||
168 | 97 | def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): | ||
169 | 98 | self.http_method = http_method | ||
170 | 99 | self.http_url = http_url | ||
171 | 100 | self.parameters = parameters or {} | ||
172 | 101 | |||
173 | 102 | def set_parameter(self, parameter, value): | ||
174 | 103 | self.parameters[parameter] = value | ||
175 | 104 | |||
176 | 105 | def get_parameter(self, parameter): | ||
177 | 106 | try: | ||
178 | 107 | return self.parameters[parameter] | ||
179 | 108 | except: | ||
180 | 109 | raise OAuthError('Parameter not found: %s' % parameter) | ||
181 | 110 | |||
182 | 111 | def _get_timestamp_nonce(self): | ||
183 | 112 | return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') | ||
184 | 113 | |||
185 | 114 | # get any non-oauth parameters | ||
186 | 115 | def get_nonoauth_parameters(self): | ||
187 | 116 | parameters = {} | ||
188 | 117 | for k, v in self.parameters.iteritems(): | ||
189 | 118 | # ignore oauth parameters | ||
190 | 119 | if k.find('oauth_') < 0: | ||
191 | 120 | parameters[k] = v | ||
192 | 121 | return parameters | ||
193 | 122 | |||
194 | 123 | # serialize as a header for an HTTPAuth request | ||
195 | 124 | def to_header(self, realm=''): | ||
196 | 125 | auth_header = 'OAuth realm="%s"' % realm | ||
197 | 126 | # add the oauth parameters | ||
198 | 127 | if self.parameters: | ||
199 | 128 | for k, v in self.parameters.iteritems(): | ||
200 | 129 | auth_header += ', %s="%s"' % (k, v) | ||
201 | 130 | return {'Authorization': auth_header} | ||
202 | 131 | |||
203 | 132 | # serialize as post data for a POST request | ||
204 | 133 | def to_postdata(self): | ||
205 | 134 | return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()) | ||
206 | 135 | |||
207 | 136 | # serialize as a url for a GET request | ||
208 | 137 | def to_url(self): | ||
209 | 138 | return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) | ||
210 | 139 | |||
211 | 140 | # return a string that consists of all the parameters that need to be signed | ||
212 | 141 | def get_normalized_parameters(self): | ||
213 | 142 | params = self.parameters | ||
214 | 143 | try: | ||
215 | 144 | # exclude the signature if it exists | ||
216 | 145 | del params['oauth_signature'] | ||
217 | 146 | except: | ||
218 | 147 | pass | ||
219 | 148 | key_values = params.items() | ||
220 | 149 | # sort lexicographically, first after key, then after value | ||
221 | 150 | key_values.sort() | ||
222 | 151 | # combine key value pairs in string and escape | ||
223 | 152 | return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values) | ||
224 | 153 | |||
225 | 154 | # just uppercases the http method | ||
226 | 155 | def get_normalized_http_method(self): | ||
227 | 156 | return self.http_method.upper() | ||
228 | 157 | |||
229 | 158 | # parses the url and rebuilds it to be scheme://host/path | ||
230 | 159 | def get_normalized_http_url(self): | ||
231 | 160 | parts = urlparse.urlparse(self.http_url) | ||
232 | 161 | url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path | ||
233 | 162 | return url_string | ||
234 | 163 | |||
235 | 164 | # set the signature parameter to the result of build_signature | ||
236 | 165 | def sign_request(self, signature_method, consumer, token): | ||
237 | 166 | # set the signature method | ||
238 | 167 | self.set_parameter('oauth_signature_method', signature_method.get_name()) | ||
239 | 168 | # set the signature | ||
240 | 169 | self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) | ||
241 | 170 | |||
242 | 171 | def build_signature(self, signature_method, consumer, token): | ||
243 | 172 | # call the build signature method within the signature method | ||
244 | 173 | return signature_method.build_signature(self, consumer, token) | ||
245 | 174 | |||
246 | 175 | @staticmethod | ||
247 | 176 | def from_request(http_method, http_url, headers=None, postdata=None, parameters=None): | ||
248 | 177 | |||
249 | 178 | # let the library user override things however they'd like, if they know | ||
250 | 179 | # which parameters to use then go for it, for example XMLRPC might want to | ||
251 | 180 | # do this | ||
252 | 181 | if parameters is not None: | ||
253 | 182 | return OAuthRequest(http_method, http_url, parameters) | ||
254 | 183 | |||
255 | 184 | # from the headers | ||
256 | 185 | if headers is not None: | ||
257 | 186 | try: | ||
258 | 187 | auth_header = headers['Authorization'] | ||
259 | 188 | # check that the authorization header is OAuth | ||
260 | 189 | auth_header.index('OAuth') | ||
261 | 190 | # get the parameters from the header | ||
262 | 191 | parameters = OAuthRequest._split_header(auth_header) | ||
263 | 192 | return OAuthRequest(http_method, http_url, parameters) | ||
264 | 193 | except: | ||
265 | 194 | raise OAuthError('Unable to parse OAuth parameters from Authorization header.') | ||
266 | 195 | |||
267 | 196 | # from the parameter string (post body) | ||
268 | 197 | if http_method == 'POST' and postdata is not None: | ||
269 | 198 | parameters = OAuthRequest._split_url_string(postdata) | ||
270 | 199 | |||
271 | 200 | # from the url string | ||
272 | 201 | elif http_method == 'GET': | ||
273 | 202 | param_str = urlparse.urlparse(http_url).query | ||
274 | 203 | parameters = OAuthRequest._split_url_string(param_str) | ||
275 | 204 | |||
276 | 205 | if parameters: | ||
277 | 206 | return OAuthRequest(http_method, http_url, parameters) | ||
278 | 207 | |||
279 | 208 | raise OAuthError('Missing all OAuth parameters. OAuth parameters must be in the headers, post body, or url.') | ||
280 | 209 | |||
281 | 210 | @staticmethod | ||
282 | 211 | def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): | ||
283 | 212 | if not parameters: | ||
284 | 213 | parameters = {} | ||
285 | 214 | |||
286 | 215 | defaults = { | ||
287 | 216 | 'oauth_consumer_key': oauth_consumer.key, | ||
288 | 217 | 'oauth_timestamp': generate_timestamp(), | ||
289 | 218 | 'oauth_nonce': generate_nonce(), | ||
290 | 219 | 'oauth_version': OAuthRequest.version, | ||
291 | 220 | } | ||
292 | 221 | |||
293 | 222 | defaults.update(parameters) | ||
294 | 223 | parameters = defaults | ||
295 | 224 | |||
296 | 225 | if token: | ||
297 | 226 | parameters['oauth_token'] = token.key | ||
298 | 227 | |||
299 | 228 | return OAuthRequest(http_method, http_url, parameters) | ||
300 | 229 | |||
301 | 230 | @staticmethod | ||
302 | 231 | def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): | ||
303 | 232 | if not parameters: | ||
304 | 233 | parameters = {} | ||
305 | 234 | |||
306 | 235 | parameters['oauth_token'] = token.key | ||
307 | 236 | |||
308 | 237 | if callback: | ||
309 | 238 | parameters['oauth_callback'] = escape(callback) | ||
310 | 239 | |||
311 | 240 | return OAuthRequest(http_method, http_url, parameters) | ||
312 | 241 | |||
313 | 242 | # util function: turn Authorization: header into parameters, has to do some unescaping | ||
314 | 243 | @staticmethod | ||
315 | 244 | def _split_header(header): | ||
316 | 245 | params = {} | ||
317 | 246 | header = header.lstrip() | ||
318 | 247 | if not header.startswith('OAuth '): | ||
319 | 248 | raise ValueError("not an OAuth header: %r" % header) | ||
320 | 249 | header = header[6:] | ||
321 | 250 | parts = header.split(',') | ||
322 | 251 | for param in parts: | ||
323 | 252 | # remove whitespace | ||
324 | 253 | param = param.strip() | ||
325 | 254 | # split key-value | ||
326 | 255 | param_parts = param.split('=', 1) | ||
327 | 256 | if param_parts[0] == 'realm': | ||
328 | 257 | # Realm header is not an OAuth parameter according to rfc5849 | ||
329 | 258 | # section 3.4.1.3.1. | ||
330 | 259 | continue | ||
331 | 260 | # remove quotes and unescape the value | ||
332 | 261 | params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) | ||
333 | 262 | return params | ||
334 | 263 | |||
335 | 264 | # util function: turn url string into parameters, has to do some unescaping | ||
336 | 265 | @staticmethod | ||
337 | 266 | def _split_url_string(param_str): | ||
338 | 267 | parameters = cgi.parse_qs(param_str, keep_blank_values=False) | ||
339 | 268 | for k, v in parameters.iteritems(): | ||
340 | 269 | parameters[k] = urllib.unquote(v[0]) | ||
341 | 270 | return parameters | ||
342 | 271 | |||
343 | 272 | # OAuthServer is a worker to check a requests validity against a data store | ||
344 | 273 | class OAuthServer(object): | ||
345 | 274 | timestamp_threshold = 300 # in seconds, five minutes | ||
346 | 275 | version = VERSION | ||
347 | 276 | signature_methods = None | ||
348 | 277 | data_store = None | ||
349 | 278 | |||
350 | 279 | def __init__(self, data_store=None, signature_methods=None): | ||
351 | 280 | self.data_store = data_store | ||
352 | 281 | self.signature_methods = signature_methods or {} | ||
353 | 282 | |||
354 | 283 | def set_data_store(self, oauth_data_store): | ||
355 | 284 | self.data_store = oauth_data_store | ||
356 | 285 | |||
357 | 286 | def get_data_store(self): | ||
358 | 287 | return self.data_store | ||
359 | 288 | |||
360 | 289 | def add_signature_method(self, signature_method): | ||
361 | 290 | self.signature_methods[signature_method.get_name()] = signature_method | ||
362 | 291 | return self.signature_methods | ||
363 | 292 | |||
364 | 293 | # process a request_token request | ||
365 | 294 | # returns the request token on success | ||
366 | 295 | def fetch_request_token(self, oauth_request): | ||
367 | 296 | try: | ||
368 | 297 | # get the request token for authorization | ||
369 | 298 | token = self._get_token(oauth_request, 'request') | ||
370 | 299 | except: | ||
371 | 300 | # no token required for the initial token request | ||
372 | 301 | version = self._get_version(oauth_request) | ||
373 | 302 | consumer = self._get_consumer(oauth_request) | ||
374 | 303 | self._check_signature(oauth_request, consumer, None) | ||
375 | 304 | # fetch a new token | ||
376 | 305 | token = self.data_store.fetch_request_token(consumer) | ||
377 | 306 | return token | ||
378 | 307 | |||
379 | 308 | # process an access_token request | ||
380 | 309 | # returns the access token on success | ||
381 | 310 | def fetch_access_token(self, oauth_request): | ||
382 | 311 | version = self._get_version(oauth_request) | ||
383 | 312 | consumer = self._get_consumer(oauth_request) | ||
384 | 313 | # get the request token | ||
385 | 314 | token = self._get_token(oauth_request, 'request') | ||
386 | 315 | self._check_signature(oauth_request, consumer, token) | ||
387 | 316 | new_token = self.data_store.fetch_access_token(consumer, token) | ||
388 | 317 | return new_token | ||
389 | 318 | |||
390 | 319 | # verify an api call, checks all the parameters | ||
391 | 320 | def verify_request(self, oauth_request): | ||
392 | 321 | # -> consumer and token | ||
393 | 322 | version = self._get_version(oauth_request) | ||
394 | 323 | consumer = self._get_consumer(oauth_request) | ||
395 | 324 | # get the access token | ||
396 | 325 | token = self._get_token(oauth_request, 'access') | ||
397 | 326 | self._check_signature(oauth_request, consumer, token) | ||
398 | 327 | parameters = oauth_request.get_nonoauth_parameters() | ||
399 | 328 | return consumer, token, parameters | ||
400 | 329 | |||
401 | 330 | # authorize a request token | ||
402 | 331 | def authorize_token(self, token, user): | ||
403 | 332 | return self.data_store.authorize_request_token(token, user) | ||
404 | 333 | |||
405 | 334 | # get the callback url | ||
406 | 335 | def get_callback(self, oauth_request): | ||
407 | 336 | return oauth_request.get_parameter('oauth_callback') | ||
408 | 337 | |||
409 | 338 | # optional support for the authenticate header | ||
410 | 339 | def build_authenticate_header(self, realm=''): | ||
411 | 340 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} | ||
412 | 341 | |||
413 | 342 | # verify the correct version request for this server | ||
414 | 343 | def _get_version(self, oauth_request): | ||
415 | 344 | try: | ||
416 | 345 | version = oauth_request.get_parameter('oauth_version') | ||
417 | 346 | except: | ||
418 | 347 | version = VERSION | ||
419 | 348 | if version and version != self.version: | ||
420 | 349 | raise OAuthError('OAuth version %s not supported' % str(version)) | ||
421 | 350 | return version | ||
422 | 351 | |||
423 | 352 | # figure out the signature with some defaults | ||
424 | 353 | def _get_signature_method(self, oauth_request): | ||
425 | 354 | try: | ||
426 | 355 | signature_method = oauth_request.get_parameter('oauth_signature_method') | ||
427 | 356 | except: | ||
428 | 357 | signature_method = SIGNATURE_METHOD | ||
429 | 358 | try: | ||
430 | 359 | # get the signature method object | ||
431 | 360 | signature_method = self.signature_methods[signature_method] | ||
432 | 361 | except: | ||
433 | 362 | signature_method_names = ', '.join(self.signature_methods.keys()) | ||
434 | 363 | raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) | ||
435 | 364 | |||
436 | 365 | return signature_method | ||
437 | 366 | |||
438 | 367 | def _get_consumer(self, oauth_request): | ||
439 | 368 | consumer_key = oauth_request.get_parameter('oauth_consumer_key') | ||
440 | 369 | if not consumer_key: | ||
441 | 370 | raise OAuthError('Invalid consumer key') | ||
442 | 371 | consumer = self.data_store.lookup_consumer(consumer_key) | ||
443 | 372 | if not consumer: | ||
444 | 373 | raise OAuthError('Invalid consumer') | ||
445 | 374 | return consumer | ||
446 | 375 | |||
447 | 376 | # try to find the token for the provided request token key | ||
448 | 377 | def _get_token(self, oauth_request, token_type='access'): | ||
449 | 378 | token_field = oauth_request.get_parameter('oauth_token') | ||
450 | 379 | token = self.data_store.lookup_token(token_type, token_field) | ||
451 | 380 | if not token: | ||
452 | 381 | raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) | ||
453 | 382 | return token | ||
454 | 383 | |||
455 | 384 | def _check_signature(self, oauth_request, consumer, token): | ||
456 | 385 | timestamp, nonce = oauth_request._get_timestamp_nonce() | ||
457 | 386 | self._check_timestamp(timestamp) | ||
458 | 387 | self._check_nonce(consumer, token, nonce) | ||
459 | 388 | signature_method = self._get_signature_method(oauth_request) | ||
460 | 389 | try: | ||
461 | 390 | signature = oauth_request.get_parameter('oauth_signature') | ||
462 | 391 | except: | ||
463 | 392 | raise OAuthError('Missing signature') | ||
464 | 393 | # attempt to construct the same signature | ||
465 | 394 | built = signature_method.build_signature(oauth_request, consumer, token) | ||
466 | 395 | if signature != built: | ||
467 | 396 | key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) | ||
468 | 397 | raise OAuthError('Signature does not match. Expected: %s Got: %s Expected signature base string: %s' % (built, signature, base)) | ||
469 | 398 | |||
470 | 399 | def _check_timestamp(self, timestamp): | ||
471 | 400 | # verify that timestamp is recentish | ||
472 | 401 | timestamp = int(timestamp) | ||
473 | 402 | now = int(time.time()) | ||
474 | 403 | lapsed = now - timestamp | ||
475 | 404 | if lapsed > self.timestamp_threshold: | ||
476 | 405 | raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) | ||
477 | 406 | |||
478 | 407 | def _check_nonce(self, consumer, token, nonce): | ||
479 | 408 | # verify that the nonce is uniqueish | ||
480 | 409 | try: | ||
481 | 410 | self.data_store.lookup_nonce(consumer, token, nonce) | ||
482 | 411 | raise OAuthError('Nonce already used: %s' % str(nonce)) | ||
483 | 412 | except: | ||
484 | 413 | pass | ||
485 | 414 | |||
486 | 415 | # OAuthClient is a worker to attempt to execute a request | ||
487 | 416 | class OAuthClient(object): | ||
488 | 417 | consumer = None | ||
489 | 418 | token = None | ||
490 | 419 | |||
491 | 420 | def __init__(self, oauth_consumer, oauth_token): | ||
492 | 421 | self.consumer = oauth_consumer | ||
493 | 422 | self.token = oauth_token | ||
494 | 423 | |||
495 | 424 | def get_consumer(self): | ||
496 | 425 | return self.consumer | ||
497 | 426 | |||
498 | 427 | def get_token(self): | ||
499 | 428 | return self.token | ||
500 | 429 | |||
501 | 430 | def fetch_request_token(self, oauth_request): | ||
502 | 431 | # -> OAuthToken | ||
503 | 432 | raise NotImplementedError | ||
504 | 433 | |||
505 | 434 | def fetch_access_token(self, oauth_request): | ||
506 | 435 | # -> OAuthToken | ||
507 | 436 | raise NotImplementedError | ||
508 | 437 | |||
509 | 438 | def access_resource(self, oauth_request): | ||
510 | 439 | # -> some protected resource | ||
511 | 440 | raise NotImplementedError | ||
512 | 441 | |||
513 | 442 | # OAuthDataStore is a database abstraction used to lookup consumers and tokens | ||
514 | 443 | class OAuthDataStore(object): | ||
515 | 444 | |||
516 | 445 | def lookup_consumer(self, key): | ||
517 | 446 | # -> OAuthConsumer | ||
518 | 447 | raise NotImplementedError | ||
519 | 448 | |||
520 | 449 | def lookup_token(self, oauth_consumer, token_type, token_token): | ||
521 | 450 | # -> OAuthToken | ||
522 | 451 | raise NotImplementedError | ||
523 | 452 | |||
524 | 453 | def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): | ||
525 | 454 | # -> OAuthToken | ||
526 | 455 | raise NotImplementedError | ||
527 | 456 | |||
528 | 457 | def fetch_request_token(self, oauth_consumer): | ||
529 | 458 | # -> OAuthToken | ||
530 | 459 | raise NotImplementedError | ||
531 | 460 | |||
532 | 461 | def fetch_access_token(self, oauth_consumer, oauth_token): | ||
533 | 462 | # -> OAuthToken | ||
534 | 463 | raise NotImplementedError | ||
535 | 464 | |||
536 | 465 | def authorize_request_token(self, oauth_token, user): | ||
537 | 466 | # -> OAuthToken | ||
538 | 467 | raise NotImplementedError | ||
539 | 468 | |||
540 | 469 | # OAuthSignatureMethod is a strategy class that implements a signature method | ||
541 | 470 | class OAuthSignatureMethod(object): | ||
542 | 471 | def get_name(): | ||
543 | 472 | # -> str | ||
544 | 473 | raise NotImplementedError | ||
545 | 474 | |||
546 | 475 | def build_signature_base_string(oauth_request, oauth_consumer, oauth_token): | ||
547 | 476 | # -> str key, str raw | ||
548 | 477 | raise NotImplementedError | ||
549 | 478 | |||
550 | 479 | def build_signature(oauth_request, oauth_consumer, oauth_token): | ||
551 | 480 | # -> str | ||
552 | 481 | raise NotImplementedError | ||
553 | 482 | |||
554 | 483 | class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): | ||
555 | 484 | |||
556 | 485 | def get_name(self): | ||
557 | 486 | return 'HMAC-SHA1' | ||
558 | 487 | |||
559 | 488 | def build_signature_base_string(self, oauth_request, consumer, token): | ||
560 | 489 | sig = ( | ||
561 | 490 | escape(oauth_request.get_normalized_http_method()), | ||
562 | 491 | escape(oauth_request.get_normalized_http_url()), | ||
563 | 492 | escape(oauth_request.get_normalized_parameters()), | ||
564 | 493 | ) | ||
565 | 494 | |||
566 | 495 | key = '%s&' % escape(consumer.secret) | ||
567 | 496 | if token: | ||
568 | 497 | key += escape(token.secret) | ||
569 | 498 | raw = '&'.join(sig) | ||
570 | 499 | return key, raw | ||
571 | 500 | |||
572 | 501 | def build_signature(self, oauth_request, consumer, token): | ||
573 | 502 | # build the base signature string | ||
574 | 503 | key, raw = self.build_signature_base_string(oauth_request, consumer, token) | ||
575 | 504 | |||
576 | 505 | # hmac object | ||
577 | 506 | try: | ||
578 | 507 | import hashlib # 2.5 | ||
579 | 508 | hashed = hmac.new(key, raw, hashlib.sha1) | ||
580 | 509 | except: | ||
581 | 510 | import sha # deprecated | ||
582 | 511 | hashed = hmac.new(key, raw, sha) | ||
583 | 512 | |||
584 | 513 | # calculate the digest base 64 | ||
585 | 514 | return base64.b64encode(hashed.digest()) | ||
586 | 515 | |||
587 | 516 | class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): | ||
588 | 517 | |||
589 | 518 | def get_name(self): | ||
590 | 519 | return 'PLAINTEXT' | ||
591 | 520 | |||
592 | 521 | def build_signature_base_string(self, oauth_request, consumer, token): | ||
593 | 522 | # concatenate the consumer key and secret | ||
594 | 523 | sig = escape(consumer.secret) | ||
595 | 524 | if token: | ||
596 | 525 | sig = '&'.join((sig, escape(token.secret))) | ||
597 | 526 | return sig | ||
598 | 527 | |||
599 | 528 | def build_signature(self, oauth_request, consumer, token): | ||
600 | 529 | return self.build_signature_base_string(oauth_request, consumer, token) | ||
601 | 530 | 0 | ||
602 | === modified file 'setup.py' | |||
603 | --- setup.py 2011-01-06 22:21:02 +0000 | |||
604 | +++ setup.py 2011-02-13 12:17:26 +0000 | |||
605 | @@ -56,7 +56,6 @@ | |||
606 | 56 | 'meliae', | 56 | 'meliae', |
607 | 57 | 'mercurial', | 57 | 'mercurial', |
608 | 58 | 'mocker', | 58 | 'mocker', |
609 | 59 | 'oauth', | ||
610 | 60 | 'paramiko', | 59 | 'paramiko', |
611 | 61 | 'psycopg2', | 60 | 'psycopg2', |
612 | 62 | 'python-memcached', | 61 | 'python-memcached', |
613 | 63 | 62 | ||
614 | === modified file 'versions.cfg' | |||
615 | --- versions.cfg 2011-02-09 15:25:57 +0000 | |||
616 | +++ versions.cfg 2011-02-13 12:17:26 +0000 | |||
617 | @@ -46,7 +46,6 @@ | |||
618 | 46 | mercurial = 1.6.2 | 46 | mercurial = 1.6.2 |
619 | 47 | mocker = 0.10.1 | 47 | mocker = 0.10.1 |
620 | 48 | mozrunner = 1.3.4 | 48 | mozrunner = 1.3.4 |
621 | 49 | oauth = 1.0 | ||
622 | 50 | paramiko = 1.7.4 | 49 | paramiko = 1.7.4 |
623 | 51 | Paste = 1.7.2 | 50 | Paste = 1.7.2 |
624 | 52 | PasteDeploy = 1.3.3 | 51 | PasteDeploy = 1.3.3 |
Martin--
This looks good. Thanks!