Merge lp:~leonardr/launchpadlib/remove-broken-code into lp:launchpadlib

Proposed by Leonard Richardson
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: 98
Merged at revision: 97
Proposed branch: lp:~leonardr/launchpadlib/remove-broken-code
Merge into: lp:launchpadlib
Diff against target: 1409 lines (+30/-1255)
10 files modified
src/launchpadlib/NEWS.txt (+6/-0)
src/launchpadlib/__init__.py (+1/-1)
src/launchpadlib/apps.py (+0/-119)
src/launchpadlib/bin/launchpad-credentials-console (+0/-54)
src/launchpadlib/credentials.py (+3/-310)
src/launchpadlib/docs/browser.txt (+0/-151)
src/launchpadlib/docs/command-line.txt (+3/-171)
src/launchpadlib/docs/introduction.txt (+14/-120)
src/launchpadlib/docs/trusted-client.txt (+0/-224)
src/launchpadlib/testing/helpers.py (+3/-105)
To merge this branch: bzr merge lp:~leonardr/launchpadlib/remove-broken-code
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) code Approve
Review via email: mp+38935@code.launchpad.net

Description of the change

Early this year I spent some time writing some "fake web browser" code for launchpadlib. This code would take a user's Launchpad username and password, log in to Launchpad for them, and go through the motions of authorizing a request token.

I wrote the fake browser code because every single one of our third-party developers was looking at our system and writing their own fake browser code. If we have to have fake browser code, I thought, better that it should all be in one place.

Shortly after I wrote this code, it broke. It broke because we stopped processing logins through Launchpad and started using the Ubuntu Single Sign-On service. I could have changed the fake browser to go through USSO, but since we want Launchpad to become an OpenID consumer, that would do nothing but push off (at great expense) the day when the whole "fake web browser" thing breaks for good.

Benji and I are now completing an alternative to the system that made our developers want to write fake browser code. In the meantime, it turns out that the very presence of this broken code has been confusing developers and making them write code that's broken in new and interesting ways. I'm taking this opportunity to remove the code from Launchpad.

When you look at this branch, your first reaction will be "he's removing all this code and tests and not replacing it with anything"! Keep in mind that this code is all *broken*. RequestTokenAuthorizationEngine and TrustedTokenAuthorizationConsoleApp assumes a workflow that doesn't exist anymore. The test of DummyAuthorizeRequestTokenWithBrowser wasn't testing that the token was actually authorized (it's not, anymore), only that an attempt was made--that's why I had to comment that test out.

The SimulatedLaunchpadBrowser is an interesting case. That code does appear to work! The tests still pass! And because of that, TrustedTokenAuthorizationConsoleApp and RequestTokenAuthorizationEngine appear to work. But this code only works for *users like us*. Old-school users who created their Launchpad accounts before we started using USSO. These are users who have hashed passwords stored in the Launchpad database, such that Launchpad can process HTTP Basic Auth requests for them. A user who created their account within the past few months will not be able to use any of this code.

This is really bad. I've seen at least one developer copy SimulatedLaunchpadBrowser's Basic Auth idea for their own fake-browser code, and then I had to spend quite a bit of time understanding why it appeared to work for some users but not others. I think the best solution is to remove this code from launchpadlib altogether, even though it means we can no longer test the end-to-end process of obtaining an access token.

I'm leaving RequestTokenAuthorizationEngine in (but removing almost all the code), even though there's now only one implementation, because we do plan to come up with another implementation eventually. (One that asks for your USSO password if you use USSO, and opens a web browser otherwise.)

To post a comment you must log in.
98. By Leonard Richardson

Removed the no-longer-used DummyAuthorizeRequestTokenWithBrowser class.

Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/launchpadlib/NEWS.txt'
--- src/launchpadlib/NEWS.txt 2010-08-23 19:51:55 +0000
+++ src/launchpadlib/NEWS.txt 2010-10-20 13:56:48 +0000
@@ -2,6 +2,12 @@
2NEWS for launchpadlib2NEWS for launchpadlib
3=====================3=====================
44
51.7.0 (Unreleased)
6==================
7
8- Removed "fake Launchpad browser" code that didn't work and was
9 misleading developers.
10
51.6.5 (2010-08-23)111.6.5 (2010-08-23)
6==================12==================
713
814
=== modified file 'src/launchpadlib/__init__.py'
--- src/launchpadlib/__init__.py 2010-08-23 19:51:55 +0000
+++ src/launchpadlib/__init__.py 2010-10-20 13:56:48 +0000
@@ -14,4 +14,4 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.15# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
1616
17__version__ = '1.6.5'17__version__ = '1.7.0'
1818
=== modified file 'src/launchpadlib/apps.py'
--- src/launchpadlib/apps.py 2009-11-02 12:20:47 +0000
+++ src/launchpadlib/apps.py 2010-10-20 13:56:48 +0000
@@ -52,122 +52,3 @@
52 return simplejson.dumps(token)52 return simplejson.dumps(token)
5353
5454
55class TrustedTokenAuthorizationConsoleApp(RequestTokenAuthorizationEngine):
56 """An application that authorizes request tokens."""
57
58 def __init__(self, web_root, consumer_name, request_token,
59 access_levels='', input_method=raw_input):
60 """Constructor.
61
62 :param access_levels: A string of comma-separated access level
63 values. To get an up-to-date list of access levels, pass
64 token_format=Credentials.DICT_TOKEN_FORMAT into
65 Credentials.get_request_token, load the dict as JSON, and look
66 in 'access_levels'.
67 """
68 access_levels = [level.strip() for level in access_levels.split(',')]
69 super(TrustedTokenAuthorizationConsoleApp, self).__init__(
70 web_root, consumer_name, request_token, access_levels)
71
72 self.input_method = input_method
73
74 def run(self):
75 """Try to authorize a request token from user input."""
76 self.error_code = -1 # Start off assuming failure.
77 start = "Launchpad credential client (console)"
78 self.output(start)
79 self.output("-" * len(start))
80
81 try:
82 self()
83 except TokenAuthorizationException, e:
84 print str(e)
85 self.error_code = -1
86 return self.press_enter_to_exit()
87
88 def exit_with(self, code):
89 """Exit the app with the specified error code."""
90 sys.exit(code)
91
92 def get_single_char_input(self, prompt, valid):
93 """Retrieve a single-character line from the input stream."""
94 valid = valid.upper()
95 input = None
96 while input is None:
97 input = self.input_method(prompt).upper()
98 if len(input) != 1 or input not in valid:
99 input = None
100 return input
101
102 def press_enter_to_exit(self):
103 """Make the user hit enter, and then exit with an error code."""
104 prompt = '\nPress enter to go back to "%s". ' % self.consumer_name
105 self.input_method(prompt)
106 self.exit_with(self.error_code)
107
108 def input_username(self, cached_username, suggested_message):
109 """Collect the Launchpad username from the end-user.
110
111 :param cached_username: A username from a previous entry attempt,
112 to be presented as the default.
113 """
114 if cached_username is not None:
115 extra = " [%s] " % cached_username
116 else:
117 extra = "\n(No Launchpad account? Just hit enter.) "
118 username = self.input_method(suggested_message + extra)
119 if username == '':
120 return cached_username
121 return username
122
123 def input_password(self, suggested_message):
124 """Collect the Launchpad password from the end-user."""
125 if self.input_method is raw_input:
126 password = getpass.getpass(suggested_message + " ")
127 else:
128 password = self.input_method(suggested_message)
129 return password
130
131 def input_access_level(self, available_levels, suggested_message,
132 only_one_option=None):
133 """Collect the desired level of access from the end-user."""
134 if only_one_option is not None:
135 self.output(suggested_message)
136 prompt = self.message(
137 'Do you want to give "%(app)s" this level of access? [YN] ')
138 allow = self.get_single_char_input(prompt, "YN")
139 if allow == "Y":
140 return only_one_option['value']
141 else:
142 return self.UNAUTHORIZED_ACCESS_LEVEL
143 else:
144 levels_except_unauthorized = [
145 level for level in available_levels
146 if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL]
147 options = []
148 for i in range(0, len(levels_except_unauthorized)):
149 options.append(
150 "%d: %s" % (i+1, levels_except_unauthorized[i]['title']))
151 self.output(suggested_message)
152 for option in options:
153 self.output(option)
154 allowed = ("".join(map(str, range(1, i+2)))) + "Q"
155 prompt = self.message(
156 'What should "%(app)s" be allowed to do using your '
157 'Launchpad account? [1-%(max)d or Q] ',
158 extra_variables = {'max' : i+1})
159 allow = self.get_single_char_input(prompt, allowed)
160 if allow == "Q":
161 return self.UNAUTHORIZED_ACCESS_LEVEL
162 else:
163 return levels_except_unauthorized[int(allow)-1]['value']
164
165 def user_refused_to_authorize(self, suggested_message):
166 """The user refused to authorize a request token."""
167 self.output(suggested_message)
168 self.error_code = -2
169
170 def user_authorized(self, access_level, suggested_message):
171 """The user authorized a request token with some access level."""
172 self.output(suggested_message)
173 self.error_code = 0
17455
=== removed file 'src/launchpadlib/bin/launchpad-credentials-console'
--- src/launchpadlib/bin/launchpad-credentials-console 2009-10-30 17:28:51 +0000
+++ src/launchpadlib/bin/launchpad-credentials-console 1970-01-01 00:00:00 +0000
@@ -1,54 +0,0 @@
1#!/usr/bin/python
2
3# Copyright 2009 Canonical Ltd.
4
5# This file is part of launchpadlib.
6#
7# launchpadlib is free software: you can redistribute it and/or modify it
8# under the terms of the GNU Lesser General Public License as published by the
9# Free Software Foundation, version 3 of the License.
10#
11# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
14# for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
18
19"""Take the user's Launchpad credentials and authorize a request token.
20
21This script will guide the user through the process of authorizing a
22request token with their username and password. It's not as secure as
23opening the user's web browser, but if you are writing a console
24application and want to keep the authorization process console-based,
25it's better to use this script than write the authorization code yourself.
26"""
27
28__metaclass__ = type
29
30from optparse import OptionParser
31from launchpadlib.apps import (
32 RequestTokenApp, TrustedTokenAuthorizationConsoleApp)
33
34parser = OptionParser()
35parser.usage = "%prog CONSUMER_NAME REQUEST_TOKEN [-r URL] [-a LEVEL,LEVEL,...]"
36parser.add_option("-s", "--site", dest="web_root",
37 help=("The name of the Launchpad site on which the request "
38 "token can be found (default: %default). This can "
39 "also be the URL to the root of the site."),
40 metavar="URL", default="staging")
41parser.add_option("-a", "--access-level", dest="access_levels",
42 help="Any restrictions on your clients' access levels",
43 metavar="[LEVEL,LEVEL,...]", default="")
44
45if __name__ == '__main__':
46 (options, args) = parser.parse_args()
47 if len(args) < 2:
48 parser.error("You must provide both a consumer name and the ID of "
49 "an OAuth request token on Launchpad.")
50 consumer_name, token_id = args[:2]
51 TrustedTokenAuthorizationConsoleApp(
52 options.web_root, consumer_name, token_id,
53 options.access_levels).run()
54
550
=== modified file 'src/launchpadlib/credentials.py'
--- src/launchpadlib/credentials.py 2010-07-19 21:47:03 +0000
+++ src/launchpadlib/credentials.py 2010-10-20 13:56:48 +0000
@@ -184,330 +184,23 @@
184 super(AnonymousAccessToken, self).__init__('','')184 super(AnonymousAccessToken, self).__init__('','')
185185
186186
187class SimulatedLaunchpadBrowser(object):
188 """A programmable substitute for a human-operated web browser.
189
190 Used by client programs to interact with Launchpad's credential
191 pages, without opening them in the user's actual web browser.
192 """
193
194 def __init__(self, web_root=uris.STAGING_WEB_ROOT):
195 self.web_root = uris.lookup_web_root(web_root)
196 self.http = httplib2.Http()
197
198 def _auth_header(self, username, password):
199 """Utility method to generate a Basic auth header."""
200 auth = base64.encodestring("%s:%s" % (username, password))[:-1]
201 return "Basic " + auth
202
203 def get_token_info(self, username, password, request_token,
204 access_levels=''):
205 """Retrieve a JSON representation of a request token.
206
207 This is useful for verifying that the end-user gave a valid
208 username and password, and for reconciling the client's
209 allowable access levels with the access levels defined in
210 Launchpad.
211 """
212 if access_levels != '':
213 s = "&allow_permission="
214 access_levels = s + s.join(access_levels)
215 page = "%s?oauth_token=%s%s" % (
216 authorize_token_page, request_token, access_levels)
217 url = urljoin(self.web_root, page)
218 # We can't use httplib2's add_credentials, because Launchpad
219 # doesn't respond to credential-less access with a 401
220 # response code.
221 headers = {'Accept' : 'application/json',
222 'Referer' : self.web_root}
223 headers['Authorization'] = self._auth_header(username, password)
224 response, content = self.http.request(url, headers=headers)
225 # Detect common error conditions and set the response code
226 # appropriately. This lets code that uses
227 # SimulatedLaunchpadBrowser detect standard response codes
228 # instead of having Launchpad-specific knowledge.
229 location = response.get('content-location')
230 if response.status == 200 and '+login' in location:
231 response.status = 401
232 elif response.get('content-type') != 'application/json':
233 response.status = 500
234 return response, content
235
236 def grant_access(self, username, password, request_token, access_level,
237 context=None):
238 """Grant a level of access to an application on behalf of a user."""
239 headers = {'Content-type' : 'application/x-www-form-urlencoded',
240 'Referer' : self.web_root}
241 headers['Authorization'] = self._auth_header(username, password)
242 body = "oauth_token=%s&field.actions.%s=True" % (
243 quote(request_token), quote(access_level))
244 if context is not None:
245 body += "&lp.context=%s" % quote(context)
246 url = urljoin(self.web_root, "+authorize-token")
247 response, content = self.http.request(
248 url, method="POST", headers=headers, body=body)
249 # This would be much less fragile if Launchpad gave us an
250 # error code to work with.
251 if "Unauthenticated user POSTing to page" in content:
252 response.status = 401 # Unauthorized
253 elif 'Request already reviewed' in content:
254 response.status = 409 # Conflict
255 elif 'What level of access' in content:
256 response.status = 400 # Bad Request
257 elif 'Unable to identify application' in content:
258 response.status = 400 # Bad Request
259 elif not 'Almost finished' in content:
260 response.status = 500 # Internal Server Error
261 return response, content
262
263
264class RequestTokenAuthorizationEngine(object):187class RequestTokenAuthorizationEngine(object):
265188
266 UNAUTHORIZED_ACCESS_LEVEL = "UNAUTHORIZED"189 UNAUTHORIZED_ACCESS_LEVEL = "UNAUTHORIZED"
267190
268 # Suggested messages for clients to display in common situations.
269
270 AUTHENTICATION_FAILURE = "I can't log in with the credentials you gave me. Let's try again."
271
272 CHOOSE_ACCESS_LEVEL = """Now it's time for you to decide how much power to give "%(app)s" over your Launchpad account."""
273
274 CHOOSE_ACCESS_LEVEL_ONE = CHOOSE_ACCESS_LEVEL + """
275
276"%(app)s" says it needs the following level of access to your Launchpad account: "%(level)s". It can't work with any other level of access, so denying this level of access means prohibiting "%(app)s" from using your Launchpad account at all."""
277
278 USER_AUTHORIZED = """Okay, I'm telling Launchpad to grant "%(app)s" access to your account."""
279
280 USER_REFUSED_TO_AUTHORIZE = """Okay, I'm going to cancel the request that "%(app)s" made for access to your account. You can always set this up again later."""
281
282 CLIENT_ERROR = """Sorry, but Launchpad is behaving in a way this client doesn't understand. There might be a bug in the client, a bug in the server, or this client might just be out of date."""
283
284 CONSUMER_MISMATCH = """WARNING: The application you're using told me its name was "%(old_consumer)s", but it told Launchpad its name was "%(real_consumer)s". This is probably not a problem, but it's a little suspicious, so you might want to look into this before continuing. I'll refer to the application as "%(real_consumer)s" from this point on."""
285
286 INPUT_USERNAME = "What email address do you use on Launchpad?"
287
288 INPUT_PASSWORD = "What's your Launchpad password? "
289
290 NONEXISTENT_REQUEST_TOKEN = """Launchpad couldn't find an outstanding request for integration between "%(app)s" and your Launchpad account. Either someone (hopefully you) already set up the integration, or else "%(app)s" is simply wrong and didn't actually set this up with Launchpad. If you still can't use "%(app)s" with Launchpad, try this process again from the beginning."""
291
292 REQUEST_TOKEN_ALREADY_AUTHORIZED = """It looks like you already approved this request to grant "%(app)s" access to your Launchpad account. You shouldn't need to do anything more."""
293
294 SERVER_ERROR = """There seems to be something wrong on the Launchpad server side, and I can't continue. Hopefully this is a temporary problem, but if it persists, it's probably because of a bug in Lauchpad or (less likely) a bug in "%(app)s"."""
295
296 STARTUP_MESSAGE = """An application identified as "%(app)s" wants to access Launchpad on your behalf. I'm the Launchpad credential client and I'm here to ask for your Launchpad username and password."""
297
298 STARTUP_MESSAGE_2 = """I'll use your Launchpad password to give "%(app)s" limited access to your Launchpad account. I will not show your password to "%(app)s" itself."""
299
300 SUCCESS = """You're all done! You should now be able to use Launchpad integration features of "%(app)s." """
301
302 SUCCESS_UNAUTHORIZED = """You're all done! "%(app)s" still doesn't have access to your Launchpad account."""
303
304 TOO_MANY_AUTHENTICATION_FAILURES = """You've failed the password entry too many times. I'm going to exit back to "%(app)s." Try again once you've solved the problem with your Launchpad account."""
305
306 YOU_NEED_A_LAUNCHPAD_ACCOUNT = """OK, you'll need to get yourself a Launchpad account before you can integrate Launchpad into "%(app)s."
307
308I'm opening the Launchpad registration page in your web browser so you can create an account. Once you've created an account, you can try this again."""
309
310 def __init__(self, web_root, consumer_name, request_token,191 def __init__(self, web_root, consumer_name, request_token,
311 allow_access_levels=[], max_failed_attempts=3):192 allow_access_levels=[]):
312 self.web_root = uris.lookup_web_root(web_root)193 self.web_root = uris.lookup_web_root(web_root)
313 self.consumer_name = consumer_name194 self.consumer_name = consumer_name
314 self.request_token = request_token195 self.request_token = request_token
315 self.browser = SimulatedLaunchpadBrowser(self.web_root)
316 self.max_failed_attempts = max_failed_attempts
317 self.allow_access_levels = allow_access_levels196 self.allow_access_levels = allow_access_levels
318 self.text_wrapper = textwrap.TextWrapper(
319 replace_whitespace=False, width=78)
320197
321 def __call__(self):198 def __call__(self):
322199 raise NotImplementedError()
323 self.startup(
324 [self.message(self.STARTUP_MESSAGE),
325 self.message(self.STARTUP_MESSAGE_2)])
326
327 # Have the end-user enter their Launchpad username and password.
328 # Make sure the credentials are valid, and get information
329 # about the request token as a side effect.
330 username, password, token_info = self.get_http_credentials()
331
332 # Update this object with fresh information about the request token.
333 self.token_info = token_info
334 self.reconciled_access_levels = token_info['access_levels']
335 self._check_consumer()
336
337 # Have the end-user choose an access level from the fresh list.
338 if len(self.reconciled_access_levels) == 2:
339 # There's only one choice: allow access at a certain level
340 # or don't allow access at all.
341 message = self.CHOOSE_ACCESS_LEVEL_ONE
342 level = [level for level in self.reconciled_access_levels
343 if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL][0]
344 extra = {'level' : level['title']}
345 only_one_option = level
346 else:
347 message = self.CHOOSE_ACCESS_LEVEL
348 extra = None
349 only_one_option = None
350 access_level = self.input_access_level(
351 self.reconciled_access_levels, self.message(message, extra),
352 only_one_option)
353
354 # Notify the program of the user's choice.
355 if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
356 self.user_refused_to_authorize(
357 self.message(self.USER_REFUSED_TO_AUTHORIZE))
358 else:
359 self.user_authorized(
360 access_level, self.message(self.USER_AUTHORIZED))
361
362 # Try to grant the specified level of access to the request token.
363 response, content = self.browser.grant_access(
364 username, password, self.request_token, access_level)
365 if response.status == 409:
366 raise RequestTokenAlreadyAuthorized(
367 self.message(self.REQUEST_TOKEN_ALREADY_AUTHORIZED))
368 elif response.status == 400:
369 raise ClientError(self.message(self.CLIENT_ERROR))
370 elif response.status == 500:
371 raise ServerError(self.message(self.SERVER_ERROR))
372 if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
373 message = self.SUCCESS_UNAUTHORIZED
374 else:
375 message = self.SUCCESS
376 self.success(self.message(message))
377
378 def get_http_credentials(self, cached_username=None, failed_attempts=0):
379 """Authenticate the user to Launchpad, or raise an exception trying.
380
381 :return: A 3-tuple (username, password,
382 token_info). 'username' and 'password' are the validated
383 Launchpad username and password. 'token_info' is a dict of
384 validated information about the request token, including
385 Launchpad's reconciled list of its available access levels
386 with the access levels the third-party client will accept.
387
388 :param cached_username: If the user has tried to enter their
389 credentials before and failed, this variable will contain the
390 username they entered the first time. This can be presented as
391 a default, since users are more likely to enter the wrong
392 password than the wrong username.
393
394 :param failed_attempts: This method calls itself recursively
395 until failed_attempts equals self.max_failed_attempts.
396 """
397 username = self.input_username(
398 cached_username, self.message(self.INPUT_USERNAME))
399 if username is None:
400 self.open_page_in_user_browser(
401 urljoin(self.web_root, "+login"))
402 raise NoLaunchpadAccount(
403 self.message(self.YOU_NEED_A_LAUNCHPAD_ACCOUNT))
404 password = self.input_password(self.message(self.INPUT_PASSWORD))
405 response, content = self.browser.get_token_info(
406 username, password, self.request_token, self.allow_access_levels)
407 if response.status == 500:
408 raise ServerError(self.message(self.SERVER_ERROR))
409 elif response.status == 401:
410 failed_attempts += 1
411 if failed_attempts == self.max_failed_attempts:
412 raise TooManyAuthenticationFailures(
413 self.message(self.TOO_MANY_AUTHENTICATION_FAILURES))
414 else:
415 self.authentication_failure(
416 self.message(self.AUTHENTICATION_FAILURE))
417 return self.get_http_credentials(username, failed_attempts)
418 token_info = simplejson.loads(content)
419 # If Launchpad provides no information about the request token,
420 # that means the request token doesn't exist.
421 if 'oauth_token' not in token_info:
422 raise RequestTokenAlreadyAuthorized(
423 self.message(self.NONEXISTENT_REQUEST_TOKEN))
424 return username, password, token_info
425
426 def _check_consumer(self):
427 """Sanity-check the server consumer against the client consumer."""
428 real_consumer = self.token_info['oauth_token_consumer']
429 if real_consumer != self.consumer_name:
430 message = self.message(
431 self.CONSUMER_MISMATCH, { 'old_consumer' : self.consumer_name,
432 'real_consumer' : real_consumer })
433 self.server_consumer_differs_from_client_consumer(
434 self.consumer_name, real_consumer, message)
435 self.consumer_name = real_consumer
436
437 def message(self, raw_message, extra_variables=None):
438 """Prepare a message by plugging in the app name."""
439 variables = { 'app' : self.consumer_name }
440 if extra_variables is not None:
441 variables.update(extra_variables)
442 return raw_message % variables
443
444 def open_page_in_user_browser(self, url):
445 """Open a web page in the user's web browser."""
446 webbrowser.open(url)
447
448 # You should define these methods in your subclass.
449
450 def output(self, message):
451 print self.text_wrapper.fill(message)
452
453 def input_username(self, cached_username, suggested_message):
454 """Collect the Launchpad username from the end-user.
455
456 :param cached_username: A username from a previous entry attempt,
457 to be presented as the default.
458 """
459 raise NotImplementedError()
460
461 def input_password(self, suggested_message):
462 """Collect the Launchpad password from the end-user."""
463 raise NotImplementedError()
464
465 def input_access_level(self, available_levels, suggested_message,
466 only_one_option=None):
467 """Collect the desired level of access from the end-user."""
468 raise NotImplementedError()
469
470 def startup(self, suggested_messages):
471 """Hook method called on startup."""
472 for message in suggested_messages:
473 self.output(message)
474 self.output("\n")
475
476 def authentication_failure(self, suggested_message):
477 """The user entered invalid credentials."""
478 self.output(suggested_message)
479 self.output("\n")
480
481 def user_refused_to_authorize(self, suggested_message):
482 """The user refused to authorize a request token."""
483 self.output(suggested_message)
484 self.output("\n")
485
486 def user_authorized(self, access_level, suggested_message):
487 """The user authorized a request token with some access level."""
488 self.output(suggested_message)
489 self.output("\n")
490
491 def server_consumer_differs_from_client_consumer(
492 self, client_name, real_name, suggested_message):
493 """The client seems to be lying or mistaken about its name.
494
495 When requesting a request token, the client told Launchpad
496 that its consumer name was "foo". Now the client is telling the
497 end-user that its name is "bar". Something is fishy and at the very
498 least the end-user should be warned about this.
499 """
500 self.output("\n")
501 self.output(suggested_message)
502 self.output("\n")
503
504 def success(self, suggested_message):
505 """The token was successfully authorized."""
506 self.output(suggested_message)
507200
508201
509class AuthorizeRequestTokenWithBrowser(RequestTokenAuthorizationEngine):202class AuthorizeRequestTokenWithBrowser(RequestTokenAuthorizationEngine):
510 """The simplest and most secure request token authorizer.203 """The simplest (and, right now, only) request token authorizer.
511204
512 This authorizer simply opens up the end-user's web browser to a205 This authorizer simply opens up the end-user's web browser to a
513 Launchpad URL and lets the end-user authorize the request token206 Launchpad URL and lets the end-user authorize the request token
514207
=== removed file 'src/launchpadlib/docs/browser.txt'
--- src/launchpadlib/docs/browser.txt 2009-10-28 18:04:29 +0000
+++ src/launchpadlib/docs/browser.txt 1970-01-01 00:00:00 +0000
@@ -1,151 +0,0 @@
1*******************************
2The simulated Launchpad browser
3*******************************
4
5The SimulatedLaunchpadBrowser class is a scriptable browser-like class
6that can be trusted with the end-user's username and password. It
7fulfils the same function as the user's web browser, but because it's
8scriptable can be used to create non-browser trusted clients.
9
10 >>> username = 'salgado@ubuntu.com'
11 >>> password = 'zeca'
12 >>> web_root = 'http://launchpad.dev:8085/'
13
14Before showing how SimulatedLaunchpadBrowser can authorize a request
15token, let's create a request token to authorize.
16
17 >>> from launchpadlib.credentials import Credentials
18 >>> credentials = Credentials("doctest consumer")
19 >>> context="firefox"
20 >>> validate_url = credentials.get_request_token(
21 ... web_root=web_root, context=context)
22 >>> request_token = credentials._request_token.key
23
24get_token_info()
25================
26
27If you have the end-user's username and password, you can use
28get_token_info() to get information about one of the user's request
29tokens. It's useful for confirming that the end-user gave the correct
30username and password, and for reconciling the list of access levels a
31client will accept with Launchpad's master list.
32
33 >>> from launchpadlib.credentials import SimulatedLaunchpadBrowser
34 >>> from launchpadlib.testing.helpers import TestableLaunchpad
35
36 >>> browser = SimulatedLaunchpadBrowser(web_root)
37
38If you make an unauthorized request, you'll get a 401 error.
39(Launchpad returns 200, but SimulatedLaunchpadBrowser sniffs it and
40changes it to a 401.)
41
42 >>> response, content = browser.get_token_info(
43 ... "baduser", "badpasword", request_token)
44 >>> print response.status
45 401
46
47If you provide the right authorization, you'll get back information
48about your request token.
49
50 >>> response, content = browser.get_token_info(
51 ... username, password, request_token)
52 >>> print response['content-type']
53 application/json
54
55 # XXX leonardr 2009-10-28 bug=462773 These two URLs should be
56 # exactly the same, but the Content-Location is missing the
57 # lp.context.
58 >>> validate_url.startswith(response['content-location'])
59 True
60
61 >>> import simplejson
62 >>> json = simplejson.loads(content)
63 >>> json['oauth_token'] == request_token
64 True
65
66 >>> print json['oauth_token_consumer']
67 doctest consumer
68
69You'll also get information about the available access
70levels.
71
72 >>> print sorted([level['value'] for level in json['access_levels']])
73 ['READ_PRIVATE', ... 'UNAUTHORIZED', ...]
74
75If you provide a list of possible access levels, you'll
76get back a list that reconciles the list you gave with
77Launchpad's access levels.
78
79 >>> response, content = browser.get_token_info(
80 ... username, password, request_token,
81 ... ["READ_PUBLIC", "READ_PRIVATE", "NO_SUCH_ACCESS_LEVEL"])
82
83 >>> print response['content-type']
84 application/json
85
86 >>> json = simplejson.loads(content)
87 >>> print sorted(
88 ... [level['value'] for level in json['access_levels']])
89 ['READ_PRIVATE', 'READ_PUBLIC', 'UNAUTHORIZED']
90
91Note that the nonexistent access level has been removed from the
92reconciled list, and the "Unauthorized" access level (which must
93always be an option) has been added.
94
95grant_access()
96==============
97
98If you have the end-user's username and password, you can use
99grant_access() to authorize a request token.
100
101If you make an unauthorized request, you'll get a 401 error. (As with
102get_token_info(), Launchpad returns 200, but SimulatedLaunchpadBrowser
103sniffs it and changes it to a 401.)
104
105 >>> access_level = "READ_PRIVATE"
106
107 >>> response, content = browser.grant_access(
108 ... "baduser", "badpasword", request_token, access_level, context)
109 >>> print response.status
110 401
111
112If you try to grant an invalid level of access, you'll get a
113400 error.
114
115 >>> response, content = browser.grant_access(
116 ... username, password, request_token,
117 ... "NO_SUCH_ACCESS_LEVEL")
118 >>> print response.status
119 400
120
121If you provide all the necessary information, you'll get a 200
122response code and the request token will be authorized.
123
124 >>> response, content = browser.grant_access(
125 ... username, password, request_token, access_level)
126 >>> print response.status
127 200
128
129If you try to grant access to a request token that's already
130been authorized, you'll get a 409 error.
131
132 >>> response, content = browser.grant_access(
133 ... username, password, request_token, access_level)
134 >>> print response.status
135 409
136
137Now that the request token is authorized, we can exchange it for an
138access token.
139
140 >>> credentials.exchange_request_token_for_access_token(
141 ... web_root=web_root)
142 >>> credentials.access_token.key is None
143 False
144
145If you try to grant access to a request token that's already been
146exchanged for an access token, you'll get a 400 error.
147
148 >>> response, content = browser.grant_access(
149 ... username, password, request_token, access_level)
150 >>> print response.status
151 400
1520
=== modified file 'src/launchpadlib/docs/command-line.txt'
--- src/launchpadlib/docs/command-line.txt 2009-11-03 13:50:27 +0000
+++ src/launchpadlib/docs/command-line.txt 2010-10-20 13:56:48 +0000
@@ -2,12 +2,11 @@
2Command-line scripts2Command-line scripts
3********************3********************
44
5Launchpad includes some command-line scripts to make Launchpad5Launchpad includes one command-line script to make Launchpad
6integration easier for third-party libraries that aren't written in6integration easier for third-party libraries that aren't written in
7Python or that can't do token authorization by opening a user's web7Python.
8browser.
98
10This file tests the workflow underlying the command-line scripts as9This file tests the workflow underlying the command-line script as
11best it can.10best it can.
1211
13RequestTokenApp12RequestTokenApp
@@ -36,170 +35,3 @@
36 >>> print json['oauth_token_consumer']35 >>> print json['oauth_token_consumer']
37 consumer36 consumer
3837
39TrustedTokenAuthorizationConsoleApp
40===================================
41
42This class is called by the command-line script
43launchpad-credentials-console. It asks for the user's Launchpad
44username and password, and authorizes a request token on their
45behalf.
46
47 >>> from launchpadlib.apps import TrustedTokenAuthorizationConsoleApp
48 >>> from launchpadlib.testing.helpers import UserInput
49
50This class does not create the request token, or exchange it for the
51access token--that's the job of the program that calls
52launchpad-credentials-console. So we'll use the request token created
53earlier by RequestTokenApp.
54
55 >>> request_token = json['oauth_token']
56
57Since this is a test, we don't want the application to call sys.exit()
58or try to open up pages in a web browser. This subclass of
59TrustedTokenAuthorizationConsoleApp will print messages instead of
60performing such un-doctest-like actions.
61
62 >>> class ConsoleApp(TrustedTokenAuthorizationConsoleApp):
63 ... def open_page_in_user_browser(self, url):
64 ... """Print a status message."""
65 ... self.output("[If this were a real application, the "
66 ... "end-user's web browser would be opened "
67 ... "to %s]" % url)
68 ...
69 ... def exit_with(self, code):
70 ... print "Application exited with code %d" % code
71
72We'll use a UserInput object to simulate a user typing things in at
73the prompt and hitting enter. This UserInput runs the program
74correctly, entering the Launchpad username and password, choosing an
75access level, and hitting Enter to exit.
76
77 >>> username = "salgado@ubuntu.com"
78 >>> password = "zeca"
79 >>> fake_input = UserInput([username, password, "1", ""])
80
81Here's a successful run of the application. When the request token is
82authorized, the script's response code is 0.
83
84 >>> app = ConsoleApp(
85 ... web_root, consumer_name, request_token,
86 ... 'READ_PRIVATE, READ_PUBLIC', input_method=fake_input)
87 >>> app.run()
88 Launchpad credential client (console)
89 -------------------------------------
90 An application identified as "consumer" wants to access Launchpad...
91 What email address do you use on Launchpad?
92 (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com]
93 What's your Launchpad password? [User input: zeca]
94 Now it's time for you to decide how much power to give "consumer"...
95 1: Read Non-Private Data
96 2: Read Anything
97 What should "consumer" be allowed to do...? [1-2 or Q] [User input: 1]
98 Okay, I'm telling Launchpad to grant "consumer" access to your account.
99 You're all done!...
100 Press enter to go back to "consumer". [User input: ]
101 Application exited with code 0
102
103Now that the request token has been authorized, we'll need to create
104another one to continue the test.
105
106 >>> json = simplejson.loads(token_app.run())
107 >>> request_token = json['oauth_token']
108 >>> app.request_token = request_token
109
110Invalid input is ignored. The user may enter 'Q' instead of a number
111to refuse to authorize the request token. When the user denies access,
112the exit code is -2.
113
114 >>> fake_input = UserInput([username, password, "A", "99", "Q", ""])
115 >>> app.input_method = fake_input
116 >>> app.run()
117 Launchpad credential client (console)
118 -------------------------------------
119 An application identified as "consumer"...
120 What should "consumer" be allowed to do...? [1-2 or Q] [User input: A]
121 What should "consumer" be allowed to do...? [1-2 or Q] [User input: 99]
122 What should "consumer" be allowed to do...? [1-2 or Q] [User input: Q]
123 Okay, I'm going to cancel the request...
124 You're all done! "consumer" still doesn't have access...
125 <BLANKLINE>
126 Press enter to go back to "consumer". [User input: ]
127 Application exited with code -2
128
129When the third-party application will allow only one level of access,
130the end-user is presented with a yes-or-no choice instead of a list to
131choose from. Again, invalid input is ignored.
132
133 >>> json = simplejson.loads(token_app.run())
134 >>> request_token = json['oauth_token']
135 >>> fake_input = UserInput([username, password, "1", "Q", "Y", ""])
136
137 >>> app = ConsoleApp(
138 ... web_root, consumer_name, request_token,
139 ... 'READ_PRIVATE', input_method=fake_input)
140
141 >>> app.run()
142 Launchpad credential client (console)
143 -------------------------------------
144 An application identified as "consumer"...
145 Do you want to give "consumer" this level of access? [YN] [User input: 1]
146 Do you want to give "consumer" this level of access? [YN] [User input: Q]
147 Do you want to give "consumer" this level of access? [YN] [User input: Y]
148 ...
149 Application exited with code 0
150
151
152Error handling
153--------------
154
155When the end-user refuses to authorize the request token, the app
156exits with a return code of -2, as seen above. When any other error
157gets in the way of the authorization of the request token, the app's
158return code is -1.
159
160If the user hits enter when asked for their email address, indicating
161that they don't have a Launchpad account, the app opens their browser
162to the Launchpad login page.
163
164 >>> json = simplejson.loads(token_app.run())
165 >>> app.request_token = json['oauth_token']
166
167 >>> input_nothing = UserInput(["", ""])
168 >>> app.input_method = input_nothing
169
170 >>> app.run()
171 Launchpad credential client (console)
172 -------------------------------------
173 An application identified as "consumer"...
174 [If this were a real application, the end-user's web browser...]
175 OK, you'll need to get yourself a Launchpad account before...
176 <BLANKLINE>
177 I'm opening the Launchpad registration page in your web browser...
178 Press enter to go back to "consumer". [User input: ]
179 Application exited with code -1
180
181If the user keeps entering bad passwords, the app eventually gives up.
182
183 >>> input_bad_password = UserInput(
184 ... [username, "badpw", "", "badpw", "", "badpw", ""])
185 >>> json = simplejson.loads(token_app.run())
186 >>> request_token = json['oauth_token']
187 >>> app.request_token = request_token
188 >>> app.input_method = input_bad_password
189 >>> app.run()
190 Launchpad credential client (console)
191 -------------------------------------
192 An application identified as "consumer"...
193 What email address do you use on Launchpad?
194 (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com]
195 What's your Launchpad password? [User input: badpw]
196 I can't log in with the credentials you gave me. Let's try again.
197 What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ]
198 What's your Launchpad password? [User input: badpw]
199 I can't log in with the credentials you gave me. Let's try again.
200 What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ]
201 What's your Launchpad password? [User input: badpw]
202 You've failed the password entry too many times...
203 Press enter to go back to "consumer". [User input: ]
204 Application exited with code -1
205
20638
=== modified file 'src/launchpadlib/docs/introduction.txt'
--- src/launchpadlib/docs/introduction.txt 2010-08-17 20:21:12 +0000
+++ src/launchpadlib/docs/introduction.txt 2010-10-20 13:56:48 +0000
@@ -211,12 +211,12 @@
211 salgado211 salgado
212212
213Otherwise, the application should obtain authorization from the user213Otherwise, the application should obtain authorization from the user
214and get a new set of credentials directly from Launchpad.214and get a new set of credentials directly from
215Launchpad.
215216
216First we must get a request token. We use 'test_dev' as a shorthand217Unfortunately, we can't test this entire process because it requires
217for the root URL of the Launchpad installation. It's defined in the218opening up a web browser, but we can test the first step, which is to
218'uris' module as 'http://launchpad.dev:8085/', and the launchpadlib219get a request token.
219code knows how to dereference it before using it as a URL.
220220
221 >>> import launchpadlib.credentials221 >>> import launchpadlib.credentials
222 >>> credentials = Credentials('consumer')222 >>> credentials = Credentials('consumer')
@@ -226,6 +226,11 @@
226 >>> authorization_url226 >>> authorization_url
227 'http://launchpad.dev:8085/+authorize-token?oauth_token=...&lp.context=firefox'227 'http://launchpad.dev:8085/+authorize-token?oauth_token=...&lp.context=firefox'
228228
229We use 'test_dev' as a shorthand for the root URL of the Launchpad
230installation. It's defined in the 'uris' module as
231'http://launchpad.dev:8085/', and the launchpadlib code knows how to
232dereference it before using it as a URL.
233
229Information about the request token is kept in the _request_token234Information about the request token is kept in the _request_token
230attribute of the Credentials object.235attribute of the Credentials object.
231236
@@ -236,121 +241,10 @@
236 >>> print credentials._request_token.context241 >>> print credentials._request_token.context
237 firefox242 firefox
238243
239Now the user must authorize that token, so we'll use the244Now the user must authorize that token, and this is the part we can't
240SimulatedLaunchpadBrowser to pretend the user is authorizing it.245test--it requires opening a web browser. Once the token is authorized
241246on the server side, we can call exchange_request_token_for_access_token()
242 >>> from launchpadlib.credentials import SimulatedLaunchpadBrowser247on our Credentials object, which will then be ready to use.
243 >>> browser = SimulatedLaunchpadBrowser(web_root='test_dev')
244 >>> response, content = browser.grant_access(
245 ... "foo.bar@canonical.com", "test",
246 ... credentials._request_token.key, "WRITE_PRIVATE",
247 ... credentials._request_token.context)
248 >>> response['status']
249 '200'
250
251After that we can exchange that request token for an access token.
252
253 >>> credentials.exchange_request_token_for_access_token(
254 ... web_root='test_dev')
255
256Once that's done, our credentials will be complete and ready to use.
257
258 >>> credentials.consumer.key
259 'consumer'
260 >>> credentials.access_token
261 <launchpadlib.credentials.AccessToken...
262 >>> credentials.access_token.key is not None
263 True
264 >>> credentials.access_token.secret is not None
265 True
266 >>> credentials.access_token.context
267 'firefox'
268
269# [XXX leonardr 2010-08-17 bug=619446] Commenting out tests that
270# started failing due to assuming Launchpad behavior that no longer
271# exists.
272
273# Authorizing the request token
274# -----------------------------
275
276# There are also two convenience method which do the access token
277# negotiation and log into the web service: get_token_and_login() and
278# login_with(). These convenience methods use the methods documented
279# above to get a request token, and once it has the request token's
280# authorization information, it makes the end-user authorize the request
281# token by entering their Launchpad username and password.
282
283# There are several ways of having the end-user authorize a request
284# token, but the most secure is to open up the user's own web browser
285# (other ways are described in trusted-client.txt). Because we don't
286# want to actually open a web browser during this test, we'll create a
287# fake authorizer that uses the SimulatedLaunchpadBrowser to authorize
288# the request token.
289
290# >>> from launchpadlib.testing.helpers import (
291# ... DummyAuthorizeRequestTokenWithBrowser)
292
293# >>> class AuthorizeAsSalgado(DummyAuthorizeRequestTokenWithBrowser):
294# ... def wait_for_request_token_authorization(self):
295# ... """Simulate the authorizing user with their web browser."""
296# ... username = 'salgado@ubuntu.com'
297# ... password = 'zeca'
298# ... browser = SimulatedLaunchpadBrowser(self.web_root)
299# ... browser.grant_access(username, password, self.request_token,
300# ... 'READ_PUBLIC')
301
302# Here, we're using 'test_dev' as shorthand for the root URL of the web
303# service. Earlier we used 'test_dev' as shorthand for the website URL,
304# and like in that earlier case, launchpadlib will internally
305# dereference 'test_dev' into the service root URL, defined in the
306# 'uris' module as "http://api.launchpad.dev:8085/".
307
308# >>> consumer_name = 'launchpadlib'
309# >>> launchpad = Launchpad.get_token_and_login(
310# ... consumer_name, service_root="test_dev",
311# ... authorizer_class=AuthorizeAsSalgado)
312# [If this were a real application, the end-user's web browser would
313# be opened to http://launchpad.dev:8085/+authorize-token?oauth_token=...]
314# The authorization page:
315# (http://launchpad.dev:8085/+authorize-token?oauth_token=...)
316# should be opening in your browser. After you have authorized
317# this program to access Launchpad on your behalf you should come
318# back here and press <Enter> to finish the authentication process.
319
320# The login_with method will cache an access token once it gets one, so
321# that the end-user doesn't have to authorize a request token every time
322# they run the program.
323
324# >>> import tempfile
325# >>> cache_dir = tempfile.mkdtemp()
326# >>> launchpad = Launchpad.login_with(
327# ... consumer_name, service_root="test_dev",
328# ... launchpadlib_dir=cache_dir,
329# ... authorizer_class=AuthorizeAsSalgado)
330# [If this were a real application...]
331# The authorization page:
332# ...
333# >>> print launchpad.me.name
334# salgado
335
336# Now that the access token is authorized, we can call login_with()
337# again and pass in a null authorizer. If there was no access token,
338# this would fail, because there would be no way to authorize the
339# request token. But since there's an access token cached in the
340# cache directory, login_with() will succeed without even trying to
341# authorize a request token.
342
343# >>> launchpad = Launchpad.login_with(
344# ... consumer_name, service_root="test_dev",
345# ... launchpadlib_dir=cache_dir,
346# ... authorizer_class=None)
347# >>> print launchpad.me.name
348# salgado
349
350# A bit of clean-up: removing the cache directory.
351
352# >>> import shutil
353# >>> shutil.rmtree(cache_dir)
354248
355The dictionary request token249The dictionary request token
356============================250============================
357251
=== removed file 'src/launchpadlib/docs/trusted-client.txt'
--- src/launchpadlib/docs/trusted-client.txt 2009-10-30 19:19:07 +0000
+++ src/launchpadlib/docs/trusted-client.txt 1970-01-01 00:00:00 +0000
@@ -1,224 +0,0 @@
1***********************
2Making a trusted client
3***********************
4
5To authorize a request token, the end-user must type in their
6Launchpad username and password. Obviously, typing your password into
7a random program is a bad idea. The best case is to use a program you
8already trust with your Launchpad password: your web browser.
9
10But if you're writing an application that can't open the end-user's
11web browser, or you just really want a token authorization client that
12has the same UI as the rest of your application, you should use one of
13the trusted clients packaged with launchpadlib, rather than writing
14your own client.
15
16All the trusted clients are based on the same core code and implement
17the same workflow. This test implements a scriptable trusted client
18and uses it to test the behavior of the standard workflow.
19
20 >>> from launchpadlib.testing.helpers import (
21 ... ScriptableRequestTokenAuthorization)
22
23Here we see the normal workflow, in which the user inputs all the
24correct data to authorize a request token.
25
26 >>> auth = ScriptableRequestTokenAuthorization(
27 ... "consumer", "salgado@ubuntu.com", "zeca",
28 ... "WRITE_PRIVATE",
29 ... allow_access_levels = ["WRITE_PUBLIC", "WRITE_PRIVATE"])
30 >>> access_token = auth()
31 An application identified as "consumer" wants to access Launchpad...
32 <BLANKLINE>
33 I'll use your Launchpad password to give "consumer" limited access...
34 What email address do you use on Launchpad?
35 What's your Launchpad password?
36 Now it's time for you to decide how much power to give "consumer" ...
37 ['UNAUTHORIZED', 'WRITE_PUBLIC', 'WRITE_PRIVATE']
38 Okay, I'm telling Launchpad to grant "consumer" access to your account.
39 You're all done! You should now be able to use Launchpad ...
40
41Ordinarily, the third-party program will create a request token and
42pass it into the trusted client. The test class is a little unusual:
43it takes care of creating the request token and, after the end-user
44has authorized it, exchanges the request token for an access
45token. This way we can verify that the entire end-to-end process
46works.
47
48 >>> access_token.key is not None
49 True
50
51Denying access
52==============
53
54It's always possible for the end-user to deny access to the
55application. This will make it impossible to convert the request token
56into an access token.
57
58 >>> auth = ScriptableRequestTokenAuthorization(
59 ... "consumer", "salgado@ubuntu.com", "zeca", "UNAUTHORIZED")
60 >>> access_token = auth()
61 An application identified as "consumer" wants to access Launchpad...
62 What email address do you use on Launchpad?
63 ...
64 Okay, I'm going to cancel the request that "consumer" made...
65 You're all done! "consumer" still doesn't have access...
66
67 >>> access_token is None
68 True
69
70Only one allowable access level
71===============================
72
73When the application being authenticated only allows one access level,
74the authorizer creates a special message for display to the end-user.
75
76 >>> auth = ScriptableRequestTokenAuthorization(
77 ... "consumer", "salgado@ubuntu.com", "zeca",
78 ... "WRITE_PRIVATE", allow_access_levels=["WRITE_PRIVATE"])
79
80 >>> auth()
81 An application identified as "consumer" wants to access Launchpad ...
82 ...
83 "consumer" says it needs the following level of access to your Launchpad
84 account: "Change Anything". It can't work with any other level of access,
85 so denying this level of access means prohibiting "consumer" from
86 using your Launchpad account at all.
87 ...
88
89Error handling
90==============
91
92Things can go wrong in many ways, most of which we can test with our
93scriptable authorizer. Here's a utility method to run the
94authorization process with a badly-scripted authorizer and print the
95resulting exception.
96
97 >>> from launchpadlib.credentials import TokenAuthorizationException
98 >>> def print_error(auth):
99 ... try:
100 ... auth()
101 ... except TokenAuthorizationException, e:
102 ... print str(e)
103
104Authentication failures
105-----------------------
106
107If the user doesn't have a Launchpad account, or refuses to type in
108their email address, the authorizer will open their web browser to the
109login page, and raise an exception.
110
111 >>> auth = ScriptableRequestTokenAuthorization(
112 ... "consumer", None, "zeca", "WRITE_PRIVATE")
113 >>> print_error(auth)
114 An application identified as "consumer" wants to access Launchpad ...
115 [If this were a real application, ... opened to http://launchpad.dev:8085/+login]
116 OK, you'll need to get yourself a Launchpad account before you can ...
117 <BLANKLINE>
118 I'm opening the Launchpad registration page in your web browser ...
119
120If the user enters the wrong username/password combination too many
121times, the authorizer will give up and raise an exception.
122
123 >>> auth = ScriptableRequestTokenAuthorization(
124 ... "consumer", "salgado@ubuntu.com", "baddpassword",
125 ... "WRITE_PRIVATE")
126 >>> print_error(auth)
127 An application identified as "consumer" wants to access Launchpad...
128 ...
129 What email address do you use on Launchpad?
130 What's your Launchpad password?
131 I can't log in with the credentials you gave me. Let's try again.
132 What email address do you use on Launchpad?
133 Cached email address: salgado@ubuntu.com
134 What's your Launchpad password?
135 You've failed the password entry too many times...
136
137The max_failed_attempts argument controls how many attempts the user
138is given to enter their username and password.
139
140 >>> auth = ScriptableRequestTokenAuthorization(
141 ... "consumer", "bad username", "zeca",
142 ... "WRITE_PRIVATE", max_failed_attempts=1)
143 >>> print_error(auth)
144 An application identified as "consumer" wants to access Launchpad ...
145 What email address do you use on Launchpad?
146 What's your Launchpad password?
147 You've failed the password entry too many times...
148
149Approving a token that was already approved
150-------------------------------------------
151
152To set this up, let's approve a request token but not exchange it for
153an access token.
154
155 >>> auth = ScriptableRequestTokenAuthorization(
156 ... "consumer", "salgado@ubuntu.com", "zeca",
157 ... "WRITE_PRIVATE")
158 >>> auth(exchange_for_access_token=False)
159 An application identified as "consumer" wants to access Launchpad ...
160 ...
161
162Now let's try to approve the request token again:
163
164 >>> print_error(auth)
165 An application identified as "consumer" wants to access Launchpad ...
166 ...
167 It looks like you already approved this request...
168
169Once the request token is exchanged for an access token, it's
170deleted. An attempt to approve a request token that's already been
171exchanged for an access token gives an error message.
172
173 >>> auth.credentials.exchange_request_token_for_access_token(
174 ... web_root=auth.web_root)
175
176 >>> print_error(auth)
177 An application identified as "consumer" wants to access Launchpad ...
178 ...
179 Launchpad couldn't find an outstanding request for integration...
180
181An attempt to approve a nonexistent request token gives the same error
182message.
183
184 >>> auth = ScriptableRequestTokenAuthorization(
185 ... "consumer", "salgado@ubuntu.com", "zeca",
186 ... "WRITE_PRIVATE")
187 >>> auth.request_token = "nosuchrequesttoken"
188 >>> print_error(auth)
189 An application identified as "consumer" wants to access Launchpad ...
190 ...
191 Launchpad couldn't find an outstanding request for integration...
192
193Miscellaneous error
194-------------------
195
196Random errors on the server side or (occasionally) the client side
197will result in a generic error message.
198
199 >>> auth.request_token = "this token will confuse launchpad badly"
200 >>> print_error(auth)
201 An application identified as "consumer" wants to access Launchpad ...
202 ...
203 There seems to be something wrong on the Launchpad server side...
204
205Client duplicity
206----------------
207
208If the third-party client gives one consumer name to Launchpad, and a
209different consumer name to the authorizer, the authorizer will detect
210this possible duplicity and print a warning.
211
212 >>> auth = ScriptableRequestTokenAuthorization(
213 ... "consumer1", "salgado@ubuntu.com", "zeca",
214 ... "WRITE_PRIVATE")
215
216We'll simulate this by changing the authorizer's .consumer_name after
217it obtained a request token from Launchpad.
218
219 >>> auth.consumer_name = "consumer2"
220 >>> auth()
221 An application identified as "consumer2" wants to access Launchpad ...
222 ...
223 WARNING: The application you're using told me its name was "consumer2", but it told Launchpad its name was "consumer1"...
224 ...
2250
=== modified file 'src/launchpadlib/testing/helpers.py'
--- src/launchpadlib/testing/helpers.py 2010-02-15 16:34:26 +0000
+++ src/launchpadlib/testing/helpers.py 2010-10-20 13:56:48 +0000
@@ -31,8 +31,9 @@
3131
32from launchpadlib.launchpad import Launchpad32from launchpadlib.launchpad import Launchpad
33from launchpadlib.credentials import (33from launchpadlib.credentials import (
34 AuthorizeRequestTokenWithBrowser, Credentials,34 AuthorizeRequestTokenWithBrowser,
35 RequestTokenAuthorizationEngine, SimulatedLaunchpadBrowser)35 Credentials,
36 )
3637
3738
38class TestableLaunchpad(Launchpad):39class TestableLaunchpad(Launchpad):
@@ -65,106 +66,3 @@
65salgado_with_full_permissions = KnownTokens('salgado-change-anything', 'test')66salgado_with_full_permissions = KnownTokens('salgado-change-anything', 'test')
66salgado_read_nonprivate = KnownTokens('salgado-read-nonprivate', 'secret')67salgado_read_nonprivate = KnownTokens('salgado-read-nonprivate', 'secret')
67nopriv_read_nonprivate = KnownTokens('nopriv-read-nonprivate', 'mystery')68nopriv_read_nonprivate = KnownTokens('nopriv-read-nonprivate', 'mystery')
68
69
70class ScriptableRequestTokenAuthorization(RequestTokenAuthorizationEngine):
71 """A request token process that doesn't need any user input.
72
73 The RequestTokenAuthorizationEngine is supposed to be hooked up to a
74 user interface, but that makes it difficult to test. This subclass
75 is designed to be easy to test.
76 """
77
78 def __init__(self, consumer_name, username, password, choose_access_level,
79 allow_access_levels=[], max_failed_attempts=2,
80 web_root="http://launchpad.dev:8085/"):
81
82 # Get a request token.
83 self.credentials = Credentials(consumer_name)
84 self.credentials.get_request_token(web_root=web_root)
85
86 # Initialize the superclass with the new request token.
87 super(ScriptableRequestTokenAuthorization, self).__init__(
88 web_root, consumer_name, self.credentials._request_token.key,
89 allow_access_levels, max_failed_attempts)
90
91 self.username = username
92 self.password = password
93 self.choose_access_level = choose_access_level
94
95 def __call__(self, exchange_for_access_token=True):
96 super(ScriptableRequestTokenAuthorization, self).__call__()
97
98 # Now verify that it worked by exchanging the authorized
99 # request token for an access token.
100 if (exchange_for_access_token and
101 self.choose_access_level != self.UNAUTHORIZED_ACCESS_LEVEL):
102 self.credentials.exchange_request_token_for_access_token(
103 web_root=self.web_root)
104 return self.credentials.access_token
105 return None
106
107 def open_page_in_user_browser(self, url):
108 """Print a status message."""
109 print ("[If this were a real application, the end-user's web "
110 "browser would be opened to %s]" % url)
111
112 def input_username(self, cached_username, suggested_message):
113 """Collect the Launchpad username from the end-user."""
114 print suggested_message
115 if cached_username is not None:
116 print "Cached email address: " + cached_username
117 return self.username
118
119 def input_password(self, suggested_message):
120 """Collect the Launchpad password from the end-user."""
121 print suggested_message
122 return self.password
123
124 def input_access_level(self, available_levels, suggested_message,
125 only_one_option):
126 """Collect the desired level of access from the end-user."""
127 print suggested_message
128 print [level['value'] for level in available_levels]
129 return self.choose_access_level
130
131 def startup(self, suggested_messages):
132 for message in suggested_messages:
133 print message
134
135
136class DummyAuthorizeRequestTokenWithBrowser(AuthorizeRequestTokenWithBrowser):
137
138 def __init__(self, web_root, consumer_name, request_token, username,
139 password, allow_access_levels=[], max_failed_attempts=3):
140 super(DummyAuthorizeRequestTokenWithBrowser, self).__init__(
141 web_root, consumer_name, request_token, allow_access_levels,
142 max_failed_attempts)
143
144 def open_page_in_user_browser(self, url):
145 """Print a status message."""
146 print ("[If this were a real application, the end-user's web "
147 "browser would be opened to %s]" % url)
148
149
150class UserInput(object):
151 """A class to store fake user input in a readable way.
152
153 An instance of this class can be used as a substitute for the
154 raw_input() function.
155 """
156
157 def __init__(self, inputs):
158 """Initialize with a line of user inputs."""
159 self.stream = iter(inputs)
160
161 def __call__(self, prompt):
162 """Print and return the next line of input."""
163 line = self.readline()
164 print prompt + "[User input: %s]" % line
165 return line
166
167 def readline(self):
168 """Return the next line of input."""
169 next_input = self.stream.next()
170 return str(next_input)

Subscribers

People subscribed via source and target branches