Merge lp:~leonardr/launchpad/revert-oauth-aware-website into lp:launchpad
- revert-oauth-aware-website
- Merge into devel
Proposed by
Leonard Richardson
Status: | Merged |
---|---|
Merged at revision: | 11597 |
Proposed branch: | lp:~leonardr/launchpad/revert-oauth-aware-website |
Merge into: | lp:launchpad |
Diff against target: |
1091 lines (+148/-550) 9 files modified
lib/canonical/launchpad/browser/oauth.py (+7/-106) lib/canonical/launchpad/database/oauth.py (+8/-11) lib/canonical/launchpad/pagetests/oauth/authorize-token.txt (+19/-207) lib/canonical/launchpad/webapp/authentication.py (+5/-131) lib/canonical/launchpad/webapp/servers.py (+94/-3) lib/canonical/launchpad/zcml/launchpad.zcml (+2/-2) lib/lp/services/job/runner.py (+2/-5) lib/lp/testing/__init__.py (+1/-5) lib/lp/testing/_webservice.py (+10/-80) |
To merge this branch: | bzr merge lp:~leonardr/launchpad/revert-oauth-aware-website |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Review via email: mp+36040@code.launchpad.net |
Commit message
Description of the change
This branch reverts my recent branch to make parts of the Launchpad website accept OAuth-signed requests. I'm reverting it not because the code is bad, but because the requirements changed immediately after I merged this branch, rendering it moot.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/canonical/launchpad/browser/oauth.py' |
2 | --- lib/canonical/launchpad/browser/oauth.py 2010-09-15 20:06:13 +0000 |
3 | +++ lib/canonical/launchpad/browser/oauth.py 2010-09-21 16:32:05 +0000 |
4 | @@ -11,13 +11,11 @@ |
5 | |
6 | from lazr.restful import HTTPResource |
7 | import simplejson |
8 | -from zope.authentication.interfaces import IUnauthenticatedPrincipal |
9 | from zope.component import getUtility |
10 | from zope.formlib.form import ( |
11 | Action, |
12 | Actions, |
13 | ) |
14 | -from zope.security.interfaces import Unauthorized |
15 | |
16 | from canonical.launchpad.interfaces.oauth import ( |
17 | IOAuthConsumerSet, |
18 | @@ -31,15 +29,9 @@ |
19 | ) |
20 | from canonical.launchpad.webapp.authentication import ( |
21 | check_oauth_signature, |
22 | - extract_oauth_access_token, |
23 | get_oauth_authorization, |
24 | - get_oauth_principal |
25 | - ) |
26 | -from canonical.launchpad.webapp.interfaces import ( |
27 | - AccessLevel, |
28 | - ILaunchBag, |
29 | - OAuthPermission, |
30 | - ) |
31 | + ) |
32 | +from canonical.launchpad.webapp.interfaces import OAuthPermission |
33 | from lp.app.errors import UnexpectedFormData |
34 | from lp.registry.interfaces.distribution import IDistributionSet |
35 | from lp.registry.interfaces.pillar import IPillarNameSet |
36 | @@ -106,7 +98,6 @@ |
37 | return u'oauth_token=%s&oauth_token_secret=%s' % ( |
38 | token.key, token.secret) |
39 | |
40 | - |
41 | def token_exists_and_is_not_reviewed(form, action): |
42 | return form.token is not None and not form.token.is_reviewed |
43 | |
44 | @@ -115,10 +106,8 @@ |
45 | """Return a list of `Action`s for each possible `OAuthPermission`.""" |
46 | actions = Actions() |
47 | actions_excluding_grant_permissions = Actions() |
48 | - |
49 | def success(form, action, data): |
50 | form.reviewToken(action.permission) |
51 | - |
52 | for permission in OAuthPermission.items: |
53 | action = Action( |
54 | permission.title, name=permission.name, success=success, |
55 | @@ -129,86 +118,7 @@ |
56 | actions_excluding_grant_permissions.append(action) |
57 | return actions, actions_excluding_grant_permissions |
58 | |
59 | - |
60 | -class CredentialManagerAwareMixin: |
61 | - """A view for which a browser may authenticate with an OAuth token. |
62 | - |
63 | - The OAuth token must be signed with a token that has the |
64 | - GRANT_PERMISSIONS access level, and the browser must present |
65 | - itself as the Launchpad Credentials Manager. |
66 | - """ |
67 | - # A prefix identifying the Launchpad Credential Manager's |
68 | - # User-Agent string. |
69 | - GRANT_PERMISSIONS_USER_AGENT_PREFIX = "Launchpad Credentials Manager" |
70 | - |
71 | - def ensureRequestIsAuthorizedOrSigned(self): |
72 | - """Find the user who initiated the request. |
73 | - |
74 | - This property is used by a view that wants to reject access |
75 | - unless the end-user is authenticated with cookie auth, HTTP |
76 | - Basic Auth, *or* a properly authorized OAuth token. |
77 | - |
78 | - If the user is logged in with cookie auth or HTTP Basic, then |
79 | - other parts of Launchpad have taken care of the login and we |
80 | - don't have to do anything. But if the user's browser has |
81 | - signed the request with an OAuth token, other parts of |
82 | - Launchpad won't recognize that as an attempt to authorize the |
83 | - request. |
84 | - |
85 | - This method does the OAuth part of the work. It checks that |
86 | - the OAuth token is valid, that it's got the correct access |
87 | - level, and that the User-Agent is one that's allowed to sign |
88 | - requests with OAuth tokens. |
89 | - |
90 | - :return: The user who Launchpad identifies as the principal. |
91 | - Or, if Launchpad identifies no one as the principal, the user |
92 | - whose valid GRANT_PERMISSIONS OAuth token was used to sign |
93 | - the request. |
94 | - |
95 | - :raise Unauthorized: If the request is unauthorized and |
96 | - unsigned, improperly signed, anonymously signed, or signed |
97 | - with a token that does not have the right access level. |
98 | - """ |
99 | - user = getUtility(ILaunchBag).user |
100 | - if user is not None: |
101 | - return user |
102 | - # The normal Launchpad code was not able to identify any |
103 | - # user, but we're going to try a little harder before |
104 | - # concluding that no one's logged in. If the incoming |
105 | - # request is signed by an OAuth access token with the |
106 | - # GRANT_PERMISSIONS access level, we will force a |
107 | - # temporary login with the user whose access token this |
108 | - # is. |
109 | - token = extract_oauth_access_token(self.request) |
110 | - if token is None: |
111 | - # The request is not OAuth-signed. The normal Launchpad |
112 | - # code had it right: no one is authenticated. |
113 | - raise Unauthorized("Anonymous access is not allowed.") |
114 | - principal = get_oauth_principal(self.request) |
115 | - if IUnauthenticatedPrincipal.providedBy(principal): |
116 | - # The request is OAuth-signed, but as the anonymous |
117 | - # user. |
118 | - raise Unauthorized("Anonymous access is not allowed.") |
119 | - if token.permission != AccessLevel.GRANT_PERMISSIONS: |
120 | - # The request is OAuth-signed, but the token has |
121 | - # the wrong access level. |
122 | - raise Unauthorized("OAuth token has insufficient access level.") |
123 | - |
124 | - # Both the consumer key and the User-Agent must identify the |
125 | - # Launchpad Credentials Manager. |
126 | - must_start_with_prefix = [ |
127 | - token.consumer.key, self.request.getHeader("User-Agent")] |
128 | - for string in must_start_with_prefix: |
129 | - if not string.startswith( |
130 | - self.GRANT_PERMISSIONS_USER_AGENT_PREFIX): |
131 | - raise Unauthorized( |
132 | - "Only the Launchpad Credentials Manager can access this " |
133 | - "page by signing requests with an OAuth token.") |
134 | - return principal.person |
135 | - |
136 | - |
137 | -class OAuthAuthorizeTokenView( |
138 | - LaunchpadFormView, JSONTokenMixin, CredentialManagerAwareMixin): |
139 | +class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin): |
140 | """Where users authorize consumers to access Launchpad on their behalf.""" |
141 | |
142 | actions, actions_excluding_grant_permissions = ( |
143 | @@ -257,12 +167,6 @@ |
144 | and len(allowed_permissions) > 1): |
145 | allowed_permissions.remove(OAuthPermission.GRANT_PERMISSIONS.name) |
146 | |
147 | - # GRANT_PERMISSIONS may only be requested by a specific User-Agent. |
148 | - if (OAuthPermission.GRANT_PERMISSIONS.name in allowed_permissions |
149 | - and not self.request.getHeader("User-Agent").startswith( |
150 | - self.GRANT_PERMISSIONS_USER_AGENT_PREFIX)): |
151 | - allowed_permissions.remove(OAuthPermission.GRANT_PERMISSIONS.name) |
152 | - |
153 | for action in self.actions: |
154 | if (action.permission.name in allowed_permissions |
155 | or action.permission is OAuthPermission.UNAUTHORIZED): |
156 | @@ -280,10 +184,9 @@ |
157 | return actions |
158 | |
159 | def initialize(self): |
160 | - self.oauth_authorized_user = self.ensureRequestIsAuthorizedOrSigned() |
161 | self.storeTokenContext() |
162 | - |
163 | - key = self.request.form.get('oauth_token') |
164 | + form = get_oauth_authorization(self.request) |
165 | + key = form.get('oauth_token') |
166 | if key: |
167 | self.token = getUtility(IOAuthRequestTokenSet).getByKey(key) |
168 | super(OAuthAuthorizeTokenView, self).initialize() |
169 | @@ -314,8 +217,7 @@ |
170 | self.token_context = context |
171 | |
172 | def reviewToken(self, permission): |
173 | - self.token.review(self.user or self.oauth_authorized_user, |
174 | - permission, self.token_context) |
175 | + self.token.review(self.user, permission, self.token_context) |
176 | callback = self.request.form.get('oauth_callback') |
177 | if callback: |
178 | self.next_url = callback |
179 | @@ -343,7 +245,7 @@ |
180 | return context |
181 | |
182 | |
183 | -class OAuthTokenAuthorizedView(LaunchpadView, CredentialManagerAwareMixin): |
184 | +class OAuthTokenAuthorizedView(LaunchpadView): |
185 | """Where users who reviewed tokens may get redirected to. |
186 | |
187 | If the consumer didn't include an oauth_callback when sending the user to |
188 | @@ -352,7 +254,6 @@ |
189 | """ |
190 | |
191 | def initialize(self): |
192 | - authorized_user = self.ensureRequestIsAuthorizedOrSigned() |
193 | key = self.request.form.get('oauth_token') |
194 | self.token = getUtility(IOAuthRequestTokenSet).getByKey(key) |
195 | assert self.token.is_reviewed, ( |
196 | |
197 | === modified file 'lib/canonical/launchpad/database/oauth.py' |
198 | --- lib/canonical/launchpad/database/oauth.py 2010-09-15 20:55:03 +0000 |
199 | +++ lib/canonical/launchpad/database/oauth.py 2010-09-21 16:32:05 +0000 |
200 | @@ -60,14 +60,14 @@ |
201 | |
202 | # How many hours should a request token be valid for? |
203 | REQUEST_TOKEN_VALIDITY = 12 |
204 | -# The OAuth Core 1.0 spec (http://oauth.net/core/1.0/#nonce) says that |
205 | -# a timestamp "MUST be equal or greater than the timestamp used in |
206 | -# previous requests," but this is likely to cause problems if the |
207 | -# client does request pipelining, so we use a time window (relative to |
208 | -# the timestamp of the existing OAuthNonce) to check if the timestamp |
209 | -# can is acceptable. As suggested by Robert, we use a window which is |
210 | -# at least twice the size of our hard time out. This is a safe bet |
211 | -# since no requests should take more than one hard time out. |
212 | +# The OAuth Core 1.0 spec (http://oauth.net/core/1.0/#nonce) says that a |
213 | +# timestamp "MUST be equal or greater than the timestamp used in previous |
214 | +# requests," but this is likely to cause problems if the client does request |
215 | +# pipelining, so we use a time window (relative to the timestamp of the |
216 | +# existing OAuthNonce) to check if the timestamp can is acceptable. As |
217 | +# suggested by Robert, we use a window which is at least twice the size of our |
218 | +# hard time out. This is a safe bet since no requests should take more than |
219 | +# one hard time out. |
220 | TIMESTAMP_ACCEPTANCE_WINDOW = 60 # seconds |
221 | # If the timestamp is far in the future because of a client's clock skew, |
222 | # it will effectively invalidate the authentication tokens when the clock is |
223 | @@ -77,7 +77,6 @@ |
224 | # amount. |
225 | TIMESTAMP_SKEW_WINDOW = 60*60 # seconds, +/- |
226 | |
227 | - |
228 | class OAuthBase(SQLBase): |
229 | """Base class for all OAuth database classes.""" |
230 | |
231 | @@ -94,7 +93,6 @@ |
232 | |
233 | getStore = _get_store |
234 | |
235 | - |
236 | class OAuthConsumer(OAuthBase): |
237 | """See `IOAuthConsumer`.""" |
238 | implements(IOAuthConsumer) |
239 | @@ -325,7 +323,6 @@ |
240 | The key will have a length of 20 and we'll make sure it's not yet in the |
241 | given table. The secret will have a length of 80. |
242 | """ |
243 | - |
244 | key_length = 20 |
245 | key = create_unique_token_for_table(key_length, getattr(table, "key")) |
246 | secret_length = 80 |
247 | |
248 | === modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt' |
249 | --- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-09-16 21:34:31 +0000 |
250 | +++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-09-21 16:32:05 +0000 |
251 | @@ -1,6 +1,4 @@ |
252 | -*************************** |
253 | -Authorizing a request token |
254 | -*************************** |
255 | += Authorizing a request token = |
256 | |
257 | Once the consumer gets a request token, it must send the user to |
258 | Launchpad's +authorize-token page in order for the user to authenticate |
259 | @@ -21,10 +19,9 @@ |
260 | The oauth_token parameter, on the other hand, is required in the |
261 | Launchpad implementation. |
262 | |
263 | -Access to the page |
264 | -================== |
265 | - |
266 | -The +authorize-token page is restricted to authenticated users. |
267 | +The +authorize-token page is restricted to logged in users, so users will |
268 | +first be asked to log in. (We won't show the actual login process because |
269 | +it involves OpenID, which would complicate this test quite a bit.) |
270 | |
271 | >>> from urllib import urlencode |
272 | >>> params = dict( |
273 | @@ -33,18 +30,7 @@ |
274 | >>> browser.open(url) |
275 | Traceback (most recent call last): |
276 | ... |
277 | - Unauthorized: Anonymous access is not allowed. |
278 | - |
279 | -However, the details of the authentication are different than from any |
280 | -other part of Launchpad. Unlike with other pages, a user can authorize |
281 | -an OAuth token by signing their outgoing requests with an _existing_ |
282 | -OAuth token. This makes it possible for a desktop client to retrieve |
283 | -this page without knowing the end-user's username and password, or |
284 | -making them navigate the arbitrarily complex OpenID login procedure. |
285 | - |
286 | -But, let's deal with that a little later. First let's show how the |
287 | -process works through HTTP Basic Auth (the testing equivalent of a |
288 | -regular username-and-password login). |
289 | + Unauthorized:... |
290 | |
291 | >>> browser = setupBrowser(auth='Basic no-priv@canonical.com:test') |
292 | >>> browser.open(url) |
293 | @@ -58,10 +44,6 @@ |
294 | ... |
295 | See all applications authorized to access Launchpad on your behalf. |
296 | |
297 | - |
298 | -Using the page |
299 | -============== |
300 | - |
301 | This page contains one submit button for each item of OAuthPermission, |
302 | except for 'Grant Permissions', which must be specifically requested. |
303 | |
304 | @@ -92,34 +74,7 @@ |
305 | that isn't enough for the application. The user always has the option |
306 | to deny permission altogether. |
307 | |
308 | - >>> def filter_user_agent(key, value, new_value): |
309 | - ... """A filter to replace the User-Agent header in a list of headers. |
310 | - ... |
311 | - ... [XXX bug=638058] This is a hack to work around a bug in |
312 | - ... zope.testbrowser. |
313 | - ... """ |
314 | - ... |
315 | - ... if key.lower() == "user-agent": |
316 | - ... return (key, new_value) |
317 | - ... return (key, value) |
318 | - |
319 | - >>> def print_access_levels(allow_permission, user_agent=None): |
320 | - ... if user_agent is not None: |
321 | - ... # [XXX bug=638058] This is a hack to work around a bug in |
322 | - ... # zope.testbrowser which prevents browser.addHeader |
323 | - ... # from working with User-Agent. |
324 | - ... mech_browser = browser.mech_browser |
325 | - ... # Store the original User-Agent for later. |
326 | - ... old_user_agent = [ |
327 | - ... value for key, value in mech_browser.addheaders |
328 | - ... if key.lower() == "user-agent"][0] |
329 | - ... # Replace the User-Agent with the value passed into this |
330 | - ... # function. |
331 | - ... mech_browser.addheaders = [ |
332 | - ... filter_user_agent(key, value, user_agent) |
333 | - ... for key, value in mech_browser.addheaders] |
334 | - ... |
335 | - ... # Okay, now we can make the request. |
336 | + >>> def print_access_levels(allow_permission): |
337 | ... browser.open( |
338 | ... "http://launchpad.dev/+authorize-token?%s&%s" |
339 | ... % (urlencode(params), allow_permission)) |
340 | @@ -127,13 +82,6 @@ |
341 | ... actions = main_content.findAll('input', attrs={'type': 'submit'}) |
342 | ... for action in actions: |
343 | ... print action['value'] |
344 | - ... |
345 | - ... if user_agent is not None: |
346 | - ... # Finally, restore the old User-Agent. |
347 | - ... mech_browser.addheaders = [ |
348 | - ... filter_user_agent(key, value, old_user_agent) |
349 | - ... for key, value in mech_browser.addheaders] |
350 | - |
351 | |
352 | >>> print_access_levels( |
353 | ... 'allow_permission=WRITE_PUBLIC&allow_permission=WRITE_PRIVATE') |
354 | @@ -142,38 +90,23 @@ |
355 | Change Anything |
356 | |
357 | The only time the 'Grant Permissions' permission shows up in this list |
358 | -is if a client identifying itself as the Launchpad Credentials Manager |
359 | -specifically requests it, and no other permission. (Also requesting |
360 | -UNAUTHORIZED is okay--it will show up anyway.) |
361 | - |
362 | - >>> USER_AGENT = "Launchpad Credentials Manager v1.0" |
363 | - >>> print_access_levels( |
364 | - ... 'allow_permission=GRANT_PERMISSIONS', USER_AGENT) |
365 | - No Access |
366 | - Grant Permissions |
367 | - |
368 | - >>> print_access_levels( |
369 | - ... ('allow_permission=GRANT_PERMISSIONS&' |
370 | - ... 'allow_permission=UNAUTHORIZED'), |
371 | - ... USER_AGENT) |
372 | - No Access |
373 | - Grant Permissions |
374 | - |
375 | - >>> print_access_levels( |
376 | - ... ('allow_permission=WRITE_PUBLIC&' |
377 | - ... 'allow_permission=GRANT_PERMISSIONS')) |
378 | - No Access |
379 | - Change Non-Private Data |
380 | - |
381 | -If a client asks for GRANT_PERMISSIONS but doesn't claim to be the |
382 | -Launchpad Credentials Manager, Launchpad will not show GRANT_PERMISSIONS. |
383 | +is if the client specifically requests it, and no other |
384 | +permission. (Also requesting UNAUTHORIZED is okay--it will show up |
385 | +anyway.) |
386 | |
387 | >>> print_access_levels('allow_permission=GRANT_PERMISSIONS') |
388 | No Access |
389 | - Read Non-Private Data |
390 | + Grant Permissions |
391 | + |
392 | + >>> print_access_levels( |
393 | + ... 'allow_permission=GRANT_PERMISSIONS&allow_permission=UNAUTHORIZED') |
394 | + No Access |
395 | + Grant Permissions |
396 | + |
397 | + >>> print_access_levels( |
398 | + ... 'allow_permission=WRITE_PUBLIC&allow_permission=GRANT_PERMISSIONS') |
399 | + No Access |
400 | Change Non-Private Data |
401 | - Read Anything |
402 | - Change Anything |
403 | |
404 | If an application doesn't specify any valid access levels, or only |
405 | specifies the UNAUTHORIZED access level, Launchpad will show all the |
406 | @@ -330,124 +263,3 @@ |
407 | This request for accessing Launchpad on your behalf has been |
408 | reviewed ... ago. |
409 | See all applications authorized to access Launchpad on your behalf. |
410 | - |
411 | -Access through OAuth |
412 | -==================== |
413 | - |
414 | -Now it's time to show how to go through the same process without |
415 | -knowing the end-user's username and password. All you need is an OAuth |
416 | -token issued with the GRANT_PERMISSIONS access level, in the name of |
417 | -the Launchpad Credentials Manager. |
418 | - |
419 | -Let's go through the approval process again, without ever sending the |
420 | -user's username or password over HTTP. First we'll create a new user, |
421 | -and a GRANT_PERMISSIONS access token that they can use to sign |
422 | -requests. |
423 | - |
424 | - >>> login(ANONYMOUS) |
425 | - >>> user = factory.makePerson(name="test-user", password="never-used") |
426 | - >>> logout() |
427 | - |
428 | - >>> from oauth.oauth import OAuthConsumer |
429 | - >>> manager_consumer = OAuthConsumer("Launchpad Credentials Manager", "") |
430 | - |
431 | - >>> from lp.testing import oauth_access_token_for |
432 | - >>> login_person(user) |
433 | - >>> grant_permissions_token = oauth_access_token_for( |
434 | - ... manager_consumer.key, user, "GRANT_PERMISSIONS") |
435 | - >>> logout() |
436 | - |
437 | -Next, we'll give the new user an OAuth request token that needs to be |
438 | -approved using a web browser. |
439 | - |
440 | - >>> login_person(user) |
441 | - >>> consumer = getUtility(IOAuthConsumerSet).getByKey('foobar123451432') |
442 | - >>> request_token = consumer.newRequestToken() |
443 | - >>> logout() |
444 | - |
445 | - >>> params = dict(oauth_token=request_token.key) |
446 | - >>> url = "http://launchpad.dev/+authorize-token?%s" % urlencode(params) |
447 | - |
448 | -Next, we'll create a browser object that knows how to sign requests |
449 | -with the new user's existing access token. |
450 | - |
451 | - >>> from lp.testing import OAuthSigningBrowser |
452 | - >>> browser = OAuthSigningBrowser( |
453 | - ... manager_consumer, grant_permissions_token, USER_AGENT) |
454 | - >>> browser.open(url) |
455 | - >>> print browser.title |
456 | - Authorize application to access Launchpad on your behalf |
457 | - |
458 | -The browser object can approve the request and see the appropriate |
459 | -messages, even though we never gave it the user's password. |
460 | - |
461 | - >>> browser.getControl('Read Anything').click() |
462 | - |
463 | - >>> browser.url |
464 | - 'http://launchpad.dev/+token-authorized?...' |
465 | - >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent')) |
466 | - Almost finished ... |
467 | - To finish authorizing the application identified as foobar123451432 to |
468 | - access Launchpad on your behalf you should go back to the application |
469 | - window in which you started the process and inform it that you have done |
470 | - your part of the process. |
471 | - |
472 | -OAuth error conditions |
473 | ----------------------- |
474 | - |
475 | -The OAuth token used to sign the requests must have the |
476 | -GRANT_PERMISSIONS access level; no other access level will work. |
477 | - |
478 | - >>> login(ANONYMOUS) |
479 | - >>> insufficient_token = oauth_access_token_for( |
480 | - ... manager_consumer.key, user, "WRITE_PRIVATE") |
481 | - >>> logout() |
482 | - |
483 | - >>> browser = OAuthSigningBrowser( |
484 | - ... manager_consumer, insufficient_token, USER_AGENT) |
485 | - >>> browser.open(url) |
486 | - Traceback (most recent call last): |
487 | - ... |
488 | - Unauthorized: OAuth token has insufficient access level. |
489 | - |
490 | -The OAuth token must be for the Launchpad Credentials Manager, or it |
491 | -cannot be used. (Launchpad shouldn't even _issue_ a GRANT_PERMISSIONS |
492 | -token for any other consumer, but even if it somehow does, that token |
493 | -can't be used for this.) |
494 | - |
495 | - >>> login(ANONYMOUS) |
496 | - >>> wrong_consumer = OAuthConsumer( |
497 | - ... "Not the Launchpad Credentials Manager", "") |
498 | - >>> wrong_consumer_token = oauth_access_token_for( |
499 | - ... wrong_consumer.key, user, "GRANT_PERMISSIONS") |
500 | - >>> logout() |
501 | - |
502 | - >>> browser = OAuthSigningBrowser(wrong_consumer, wrong_consumer_token) |
503 | - >>> browser.open(url) |
504 | - Traceback (most recent call last): |
505 | - ... |
506 | - Unauthorized: Only the Launchpad Credentials Manager can access |
507 | - this page by signing requests with an OAuth token. |
508 | - |
509 | -Signing with an anonymous token will also not work. |
510 | - |
511 | - >>> from oauth.oauth import OAuthToken |
512 | - >>> anonymous_token = OAuthToken(key="", secret="") |
513 | - >>> browser = OAuthSigningBrowser(manager_consumer, anonymous_token) |
514 | - >>> browser.open(url) |
515 | - Traceback (most recent call last): |
516 | - ... |
517 | - Unauthorized: Anonymous access is not allowed. |
518 | - |
519 | -Even if it presents the right token, the user agent sending the signed |
520 | -request must *also* identify *itself* as the Launchpad Credentials |
521 | -Manager. |
522 | - |
523 | - >>> browser = OAuthSigningBrowser( |
524 | - ... manager_consumer, grant_permissions_token, |
525 | - ... "Not the Launchpad Credentials Manager") |
526 | - >>> browser.open(url) |
527 | - Traceback (most recent call last): |
528 | - ... |
529 | - Unauthorized: Only the Launchpad Credentials Manager can access |
530 | - this page by signing requests with an OAuth token. |
531 | |
532 | === modified file 'lib/canonical/launchpad/webapp/authentication.py' |
533 | --- lib/canonical/launchpad/webapp/authentication.py 2010-09-16 15:40:56 +0000 |
534 | +++ lib/canonical/launchpad/webapp/authentication.py 2010-09-21 16:32:05 +0000 |
535 | @@ -5,21 +5,16 @@ |
536 | |
537 | __all__ = [ |
538 | 'check_oauth_signature', |
539 | - 'extract_oauth_access_token', |
540 | - 'get_oauth_principal', |
541 | 'get_oauth_authorization', |
542 | 'LaunchpadLoginSource', |
543 | 'LaunchpadPrincipal', |
544 | - 'OAuthSignedRequest', |
545 | 'PlacelessAuthUtility', |
546 | 'SSHADigestEncryptor', |
547 | ] |
548 | |
549 | |
550 | import binascii |
551 | -from datetime import datetime |
552 | import hashlib |
553 | -import pytz |
554 | import random |
555 | from UserDict import UserDict |
556 | |
557 | @@ -28,18 +23,13 @@ |
558 | from zope.app.security.interfaces import ILoginPassword |
559 | from zope.app.security.principalregistry import UnauthenticatedPrincipal |
560 | from zope.authentication.interfaces import IUnauthenticatedPrincipal |
561 | - |
562 | from zope.component import ( |
563 | adapts, |
564 | getUtility, |
565 | ) |
566 | from zope.event import notify |
567 | -from zope.interface import ( |
568 | - alsoProvides, |
569 | - implements, |
570 | - ) |
571 | +from zope.interface import implements |
572 | from zope.preference.interfaces import IPreferenceGroup |
573 | -from zope.security.interfaces import Unauthorized |
574 | from zope.security.proxy import removeSecurityProxy |
575 | from zope.session.interfaces import ISession |
576 | |
577 | @@ -54,14 +44,6 @@ |
578 | ILaunchpadPrincipal, |
579 | IPlacelessAuthUtility, |
580 | IPlacelessLoginSource, |
581 | - OAuthPermission, |
582 | - ) |
583 | -from canonical.launchpad.interfaces.oauth import ( |
584 | - ClockSkew, |
585 | - IOAuthConsumerSet, |
586 | - IOAuthSignedRequest, |
587 | - NonceAlreadyUsed, |
588 | - TimestampOrderingError, |
589 | ) |
590 | from lp.registry.interfaces.person import ( |
591 | IPerson, |
592 | @@ -69,113 +51,6 @@ |
593 | ) |
594 | |
595 | |
596 | -def extract_oauth_access_token(request): |
597 | - """Find the OAuth access token that signed the given request. |
598 | - |
599 | - :param request: An incoming request. |
600 | - |
601 | - :return: an IOAuthAccessToken, or None if the request is not |
602 | - signed at all. |
603 | - |
604 | - :raise Unauthorized: If the token is invalid or the request is an |
605 | - anonymously-signed request that doesn't meet our requirements. |
606 | - """ |
607 | - # Fetch OAuth authorization information from the request. |
608 | - form = get_oauth_authorization(request) |
609 | - |
610 | - consumer_key = form.get('oauth_consumer_key') |
611 | - consumers = getUtility(IOAuthConsumerSet) |
612 | - consumer = consumers.getByKey(consumer_key) |
613 | - token_key = form.get('oauth_token') |
614 | - anonymous_request = (token_key == '') |
615 | - |
616 | - if consumer_key is None: |
617 | - # Either the client's OAuth implementation is broken, or |
618 | - # the user is trying to make an unauthenticated request |
619 | - # using wget or another OAuth-ignorant application. |
620 | - # Try to retrieve a consumer based on the User-Agent |
621 | - # header. |
622 | - anonymous_request = True |
623 | - consumer_key = request.getHeader('User-Agent', '') |
624 | - if consumer_key == '': |
625 | - raise Unauthorized( |
626 | - 'Anonymous requests must provide a User-Agent.') |
627 | - consumer = consumers.getByKey(consumer_key) |
628 | - |
629 | - if consumer is None: |
630 | - if anonymous_request: |
631 | - # This is the first time anyone has tried to make an |
632 | - # anonymous request using this consumer name (or user |
633 | - # agent). Dynamically create the consumer. |
634 | - # |
635 | - # In the normal website this wouldn't be possible |
636 | - # because GET requests have their transactions rolled |
637 | - # back. But webservice requests always have their |
638 | - # transactions committed so that we can keep track of |
639 | - # the OAuth nonces and prevent replay attacks. |
640 | - if consumer_key == '' or consumer_key is None: |
641 | - raise Unauthorized("No consumer key specified.") |
642 | - consumer = consumers.new(consumer_key, '') |
643 | - else: |
644 | - # An unknown consumer can never make a non-anonymous |
645 | - # request, because access tokens are registered with a |
646 | - # specific, known consumer. |
647 | - raise Unauthorized('Unknown consumer (%s).' % consumer_key) |
648 | - if anonymous_request: |
649 | - # Skip the OAuth verification step and let the user access the |
650 | - # web service as an unauthenticated user. |
651 | - # |
652 | - # XXX leonardr 2009-12-15 bug=496964: Ideally we'd be |
653 | - # auto-creating a token for the anonymous user the first |
654 | - # time, passing it through the OAuth verification step, |
655 | - # and using it on all subsequent anonymous requests. |
656 | - return None |
657 | - |
658 | - token = consumer.getAccessToken(token_key) |
659 | - if token is None: |
660 | - raise Unauthorized('Unknown access token (%s).' % token_key) |
661 | - return token |
662 | - |
663 | - |
664 | -def get_oauth_principal(request): |
665 | - """Find the principal to use for this OAuth-signed request. |
666 | - |
667 | - :param request: An incoming request. |
668 | - :return: An ILaunchpadPrincipal with the appropriate access level. |
669 | - """ |
670 | - token = extract_oauth_access_token(request) |
671 | - |
672 | - if token is None: |
673 | - # The consumer is making an anonymous request. If there was a |
674 | - # problem with the access token, extract_oauth_access_token |
675 | - # would have raised Unauthorized. |
676 | - alsoProvides(request, IOAuthSignedRequest) |
677 | - auth_utility = getUtility(IPlacelessAuthUtility) |
678 | - return auth_utility.unauthenticatedPrincipal() |
679 | - |
680 | - form = get_oauth_authorization(request) |
681 | - nonce = form.get('oauth_nonce') |
682 | - timestamp = form.get('oauth_timestamp') |
683 | - try: |
684 | - token.checkNonceAndTimestamp(nonce, timestamp) |
685 | - except (NonceAlreadyUsed, TimestampOrderingError, ClockSkew), e: |
686 | - raise Unauthorized('Invalid nonce/timestamp: %s' % e) |
687 | - now = datetime.now(pytz.timezone('UTC')) |
688 | - if token.permission == OAuthPermission.UNAUTHORIZED: |
689 | - raise Unauthorized('Unauthorized token (%s).' % token.key) |
690 | - elif token.date_expires is not None and token.date_expires <= now: |
691 | - raise Unauthorized('Expired token (%s).' % token.key) |
692 | - elif not check_oauth_signature(request, token.consumer, token): |
693 | - raise Unauthorized('Invalid signature.') |
694 | - else: |
695 | - # Everything is fine, let's return the principal. |
696 | - pass |
697 | - alsoProvides(request, IOAuthSignedRequest) |
698 | - return getUtility(IPlacelessLoginSource).getPrincipal( |
699 | - token.person.account.id, access_level=token.permission, |
700 | - scope=token.context) |
701 | - |
702 | - |
703 | class PlacelessAuthUtility: |
704 | """An authentication service which holds no state aside from its |
705 | ZCML configuration, implemented as a utility. |
706 | @@ -200,8 +75,9 @@ |
707 | # as the login form is never visited for BasicAuth. |
708 | # This we treat each request as a separate |
709 | # login/logout. |
710 | - notify( |
711 | - BasicAuthLoggedInEvent(request, login, principal)) |
712 | + notify(BasicAuthLoggedInEvent( |
713 | + request, login, principal |
714 | + )) |
715 | return principal |
716 | |
717 | def _authenticateUsingCookieAuth(self, request): |
718 | @@ -314,8 +190,7 @@ |
719 | plaintext = str(plaintext) |
720 | if salt is None: |
721 | salt = self.generate_salt() |
722 | - v = binascii.b2a_base64( |
723 | - hashlib.sha1(plaintext + salt).digest() + salt) |
724 | + v = binascii.b2a_base64(hashlib.sha1(plaintext + salt).digest() + salt) |
725 | return v[:-1] |
726 | |
727 | def validate(self, plaintext, encrypted): |
728 | @@ -459,7 +334,6 @@ |
729 | |
730 | # zope.app.apidoc expects our principals to be adaptable into IAnnotations, so |
731 | # we use these dummy adapters here just to make that code not OOPS. |
732 | - |
733 | class TemporaryPrincipalAnnotations(UserDict): |
734 | implements(IAnnotations) |
735 | adapts(ILaunchpadPrincipal, IPreferenceGroup) |
736 | |
737 | === modified file 'lib/canonical/launchpad/webapp/servers.py' |
738 | --- lib/canonical/launchpad/webapp/servers.py 2010-09-16 21:08:51 +0000 |
739 | +++ lib/canonical/launchpad/webapp/servers.py 2010-09-21 16:32:05 +0000 |
740 | @@ -8,6 +8,7 @@ |
741 | __metaclass__ = type |
742 | |
743 | import cgi |
744 | +from datetime import datetime |
745 | import threading |
746 | import xmlrpclib |
747 | |
748 | @@ -21,6 +22,7 @@ |
749 | WebServiceRequestTraversal, |
750 | ) |
751 | from lazr.uri import URI |
752 | +import pytz |
753 | import transaction |
754 | from transaction.interfaces import ISynchronizer |
755 | from zc.zservertracelog.tracelog import Server as ZServerTracelogServer |
756 | @@ -48,7 +50,10 @@ |
757 | XMLRPCRequest, |
758 | XMLRPCResponse, |
759 | ) |
760 | -from zope.security.interfaces import IParticipation |
761 | +from zope.security.interfaces import ( |
762 | + IParticipation, |
763 | + Unauthorized, |
764 | + ) |
765 | from zope.security.proxy import ( |
766 | isinstance as zope_isinstance, |
767 | removeSecurityProxy, |
768 | @@ -63,9 +68,17 @@ |
769 | IPrivateApplication, |
770 | IWebServiceApplication, |
771 | ) |
772 | +from canonical.launchpad.interfaces.oauth import ( |
773 | + ClockSkew, |
774 | + IOAuthConsumerSet, |
775 | + IOAuthSignedRequest, |
776 | + NonceAlreadyUsed, |
777 | + TimestampOrderingError, |
778 | + ) |
779 | import canonical.launchpad.layers |
780 | from canonical.launchpad.webapp.authentication import ( |
781 | - get_oauth_principal, |
782 | + check_oauth_signature, |
783 | + get_oauth_authorization, |
784 | ) |
785 | from canonical.launchpad.webapp.authorization import ( |
786 | LAUNCHPAD_SECURITY_POLICY_CACHE_KEY, |
787 | @@ -80,6 +93,8 @@ |
788 | INotificationRequest, |
789 | INotificationResponse, |
790 | IPlacelessAuthUtility, |
791 | + IPlacelessLoginSource, |
792 | + OAuthPermission, |
793 | ) |
794 | from canonical.launchpad.webapp.notifications import ( |
795 | NotificationList, |
796 | @@ -1201,7 +1216,83 @@ |
797 | if request_path.startswith("/%s" % web_service_config.path_override): |
798 | return super(WebServicePublication, self).getPrincipal(request) |
799 | |
800 | - return get_oauth_principal(request) |
801 | + # Fetch OAuth authorization information from the request. |
802 | + form = get_oauth_authorization(request) |
803 | + |
804 | + consumer_key = form.get('oauth_consumer_key') |
805 | + consumers = getUtility(IOAuthConsumerSet) |
806 | + consumer = consumers.getByKey(consumer_key) |
807 | + token_key = form.get('oauth_token') |
808 | + anonymous_request = (token_key == '') |
809 | + |
810 | + if consumer_key is None: |
811 | + # Either the client's OAuth implementation is broken, or |
812 | + # the user is trying to make an unauthenticated request |
813 | + # using wget or another OAuth-ignorant application. |
814 | + # Try to retrieve a consumer based on the User-Agent |
815 | + # header. |
816 | + anonymous_request = True |
817 | + consumer_key = request.getHeader('User-Agent', '') |
818 | + if consumer_key == '': |
819 | + raise Unauthorized( |
820 | + 'Anonymous requests must provide a User-Agent.') |
821 | + consumer = consumers.getByKey(consumer_key) |
822 | + |
823 | + if consumer is None: |
824 | + if anonymous_request: |
825 | + # This is the first time anyone has tried to make an |
826 | + # anonymous request using this consumer name (or user |
827 | + # agent). Dynamically create the consumer. |
828 | + # |
829 | + # In the normal website this wouldn't be possible |
830 | + # because GET requests have their transactions rolled |
831 | + # back. But webservice requests always have their |
832 | + # transactions committed so that we can keep track of |
833 | + # the OAuth nonces and prevent replay attacks. |
834 | + if consumer_key == '' or consumer_key is None: |
835 | + raise Unauthorized("No consumer key specified.") |
836 | + consumer = consumers.new(consumer_key, '') |
837 | + else: |
838 | + # An unknown consumer can never make a non-anonymous |
839 | + # request, because access tokens are registered with a |
840 | + # specific, known consumer. |
841 | + raise Unauthorized('Unknown consumer (%s).' % consumer_key) |
842 | + if anonymous_request: |
843 | + # Skip the OAuth verification step and let the user access the |
844 | + # web service as an unauthenticated user. |
845 | + # |
846 | + # XXX leonardr 2009-12-15 bug=496964: Ideally we'd be |
847 | + # auto-creating a token for the anonymous user the first |
848 | + # time, passing it through the OAuth verification step, |
849 | + # and using it on all subsequent anonymous requests. |
850 | + alsoProvides(request, IOAuthSignedRequest) |
851 | + auth_utility = getUtility(IPlacelessAuthUtility) |
852 | + return auth_utility.unauthenticatedPrincipal() |
853 | + token = consumer.getAccessToken(token_key) |
854 | + if token is None: |
855 | + raise Unauthorized('Unknown access token (%s).' % token_key) |
856 | + nonce = form.get('oauth_nonce') |
857 | + timestamp = form.get('oauth_timestamp') |
858 | + try: |
859 | + token.checkNonceAndTimestamp(nonce, timestamp) |
860 | + except (NonceAlreadyUsed, TimestampOrderingError, ClockSkew), e: |
861 | + raise Unauthorized('Invalid nonce/timestamp: %s' % e) |
862 | + now = datetime.now(pytz.timezone('UTC')) |
863 | + if token.permission == OAuthPermission.UNAUTHORIZED: |
864 | + raise Unauthorized('Unauthorized token (%s).' % token.key) |
865 | + elif token.date_expires is not None and token.date_expires <= now: |
866 | + raise Unauthorized('Expired token (%s).' % token.key) |
867 | + elif not check_oauth_signature(request, consumer, token): |
868 | + raise Unauthorized('Invalid signature.') |
869 | + else: |
870 | + # Everything is fine, let's return the principal. |
871 | + pass |
872 | + alsoProvides(request, IOAuthSignedRequest) |
873 | + principal = getUtility(IPlacelessLoginSource).getPrincipal( |
874 | + token.person.account.id, access_level=token.permission, |
875 | + scope=token.context) |
876 | + |
877 | + return principal |
878 | |
879 | |
880 | class LaunchpadWebServiceRequestTraversal(WebServiceRequestTraversal): |
881 | |
882 | === modified file 'lib/canonical/launchpad/zcml/launchpad.zcml' |
883 | --- lib/canonical/launchpad/zcml/launchpad.zcml 2010-09-09 21:09:00 +0000 |
884 | +++ lib/canonical/launchpad/zcml/launchpad.zcml 2010-09-21 16:32:05 +0000 |
885 | @@ -266,14 +266,14 @@ |
886 | name="+authorize-token" |
887 | class="canonical.launchpad.browser.OAuthAuthorizeTokenView" |
888 | template="../templates/oauth-authorize.pt" |
889 | - permission="zope.Public" /> |
890 | + permission="launchpad.AnyPerson" /> |
891 | |
892 | <browser:page |
893 | for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication" |
894 | name="+token-authorized" |
895 | class="canonical.launchpad.browser.OAuthTokenAuthorizedView" |
896 | template="../templates/token-authorized.pt" |
897 | - permission="zope.Public" /> |
898 | + permission="launchpad.AnyPerson" /> |
899 | |
900 | <browser:page |
901 | for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication" |
902 | |
903 | === modified file 'lib/lp/services/job/runner.py' |
904 | --- lib/lp/services/job/runner.py 2010-08-20 20:31:18 +0000 |
905 | +++ lib/lp/services/job/runner.py 2010-09-21 16:32:05 +0000 |
906 | @@ -217,10 +217,8 @@ |
907 | self.logger.exception( |
908 | "Failed to notify users about a failure.") |
909 | info = sys.exc_info() |
910 | - self.error_utility.raising(info) |
911 | - oops = self.error_utility.getLastOopsReport() |
912 | # Returning the oops says something went wrong. |
913 | - return oops |
914 | + return self.error_utility.raising(info) |
915 | |
916 | def _doOops(self, job, info): |
917 | """Report an OOPS for the provided job and info. |
918 | @@ -229,8 +227,7 @@ |
919 | :param info: The standard sys.exc_info() value. |
920 | :return: the Oops that was reported. |
921 | """ |
922 | - self.error_utility.raising(info) |
923 | - oops = self.error_utility.getLastOopsReport() |
924 | + oops = self.error_utility.raising(info) |
925 | job.notifyOops(oops) |
926 | return oops |
927 | |
928 | |
929 | === modified file 'lib/lp/testing/__init__.py' |
930 | --- lib/lp/testing/__init__.py 2010-09-20 12:56:53 +0000 |
931 | +++ lib/lp/testing/__init__.py 2010-09-21 16:32:05 +0000 |
932 | @@ -28,7 +28,6 @@ |
933 | 'map_branch_contents', |
934 | 'normalize_whitespace', |
935 | 'oauth_access_token_for', |
936 | - 'OAuthSigningBrowser', |
937 | 'person_logged_in', |
938 | 'record_statements', |
939 | 'run_with_login', |
940 | @@ -146,7 +145,6 @@ |
941 | launchpadlib_credentials_for, |
942 | launchpadlib_for, |
943 | oauth_access_token_for, |
944 | - OAuthSigningBrowser, |
945 | ) |
946 | from lp.testing.fixture import ZopeEventHandlerFixture |
947 | from lp.testing.matchers import Provides |
948 | @@ -224,7 +222,7 @@ |
949 | |
950 | class StormStatementRecorder: |
951 | """A storm tracer to count queries. |
952 | - |
953 | + |
954 | This exposes the count and queries as lp.testing._webservice.QueryCollector |
955 | does permitting its use with the HasQueryCount matcher. |
956 | |
957 | @@ -683,7 +681,6 @@ |
958 | def assertTextMatchesExpressionIgnoreWhitespace(self, |
959 | regular_expression_txt, |
960 | text): |
961 | - |
962 | def normalise_whitespace(text): |
963 | return ' '.join(text.split()) |
964 | pattern = re.compile( |
965 | @@ -860,7 +857,6 @@ |
966 | callable, and events are the events emitted by the callable. |
967 | """ |
968 | events = [] |
969 | - |
970 | def on_notify(event): |
971 | events.append(event) |
972 | old_subscribers = zope.event.subscribers[:] |
973 | |
974 | === modified file 'lib/lp/testing/_webservice.py' |
975 | --- lib/lp/testing/_webservice.py 2010-09-16 15:40:56 +0000 |
976 | +++ lib/lp/testing/_webservice.py 2010-09-21 16:32:05 +0000 |
977 | @@ -9,104 +9,34 @@ |
978 | 'launchpadlib_credentials_for', |
979 | 'launchpadlib_for', |
980 | 'oauth_access_token_for', |
981 | - 'OAuthSigningBrowser', |
982 | ] |
983 | |
984 | |
985 | import shutil |
986 | import tempfile |
987 | + |
988 | +from launchpadlib.credentials import ( |
989 | + AccessToken, |
990 | + Credentials, |
991 | + ) |
992 | +from launchpadlib.launchpad import Launchpad |
993 | import transaction |
994 | -from urllib2 import BaseHandler |
995 | - |
996 | -from oauth.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT |
997 | - |
998 | from zope.app.publication.interfaces import IEndRequestEvent |
999 | from zope.app.testing import ztapi |
1000 | -from zope.testbrowser.testing import Browser |
1001 | from zope.component import getUtility |
1002 | import zope.testing.cleanup |
1003 | |
1004 | -from launchpadlib.credentials import ( |
1005 | - AccessToken, |
1006 | - Credentials, |
1007 | - ) |
1008 | -from launchpadlib.launchpad import Launchpad |
1009 | - |
1010 | -from lp.testing._login import ( |
1011 | - login, |
1012 | - logout, |
1013 | - ) |
1014 | - |
1015 | from canonical.launchpad.interfaces import ( |
1016 | IOAuthConsumerSet, |
1017 | IPersonSet, |
1018 | - OAUTH_REALM, |
1019 | ) |
1020 | from canonical.launchpad.webapp.adapter import get_request_statements |
1021 | from canonical.launchpad.webapp.interaction import ANONYMOUS |
1022 | from canonical.launchpad.webapp.interfaces import OAuthPermission |
1023 | - |
1024 | - |
1025 | -class OAuthSigningHandler(BaseHandler): |
1026 | - """A urllib2 handler that signs requests with an OAuth token.""" |
1027 | - |
1028 | - def __init__(self, consumer, token): |
1029 | - """Constructor |
1030 | - |
1031 | - :param consumer: An OAuth consumer. |
1032 | - :param token: An OAuth token. |
1033 | - """ |
1034 | - self.consumer = consumer |
1035 | - self.token = token |
1036 | - |
1037 | - def default_open(self, req): |
1038 | - """Set the Authorization header for the outgoing request.""" |
1039 | - signer = OAuthRequest.from_consumer_and_token( |
1040 | - self.consumer, self.token) |
1041 | - signer.sign_request( |
1042 | - OAuthSignatureMethod_PLAINTEXT(), self.consumer, self.token) |
1043 | - auth_header = signer.to_header(OAUTH_REALM)['Authorization'] |
1044 | - req.headers['Authorization'] = auth_header |
1045 | - |
1046 | - |
1047 | -class UserAgentFilteringHandler(BaseHandler): |
1048 | - """A urllib2 handler that replaces the User-Agent header. |
1049 | - |
1050 | - [XXX bug=638058] This is a hack to work around a bug in |
1051 | - zope.testbrowser. |
1052 | - """ |
1053 | - def __init__(self, user_agent): |
1054 | - """Constructor.""" |
1055 | - self.user_agent = user_agent |
1056 | - |
1057 | - def default_open(self, req): |
1058 | - """Set the User-Agent header for the outgoing request.""" |
1059 | - req.headers['User-Agent'] = self.user_agent |
1060 | - |
1061 | - |
1062 | -class OAuthSigningBrowser(Browser): |
1063 | - """A browser that signs each outgoing request with an OAuth token. |
1064 | - |
1065 | - This lets us simulate the behavior of the Launchpad Credentials |
1066 | - Manager. |
1067 | - """ |
1068 | - def __init__(self, consumer, token, user_agent=None): |
1069 | - """Constructor. |
1070 | - |
1071 | - :param consumer: An OAuth consumer. |
1072 | - :param token: An OAuth token. |
1073 | - :param user_agent: The User-Agent string to send. |
1074 | - """ |
1075 | - super(OAuthSigningBrowser, self).__init__() |
1076 | - self.mech_browser.add_handler( |
1077 | - OAuthSigningHandler(consumer, token)) |
1078 | - if user_agent is not None: |
1079 | - self.mech_browser.add_handler( |
1080 | - UserAgentFilteringHandler(user_agent)) |
1081 | - |
1082 | - # This will give us tracebacks instead of unhelpful error |
1083 | - # messages. |
1084 | - self.handleErrors = False |
1085 | +from lp.testing._login import ( |
1086 | + login, |
1087 | + logout, |
1088 | + ) |
1089 | |
1090 | |
1091 | def oauth_access_token_for(consumer_name, person, permission, context=None): |
I'm sad so see the backed out. This is okay to land.