Merge lp:~leonardr/launchpad/revert-oauth-aware-website into lp:launchpad
- revert-oauth-aware-website
- Merge into devel
Proposed by
Leonard Richardson
on 2010-09-20
| 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 | 2010-09-20 | Approve on 2010-09-20 |
|
Review via email:
|
|||
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.