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
1=== modified file 'src/launchpadlib/NEWS.txt'
2--- src/launchpadlib/NEWS.txt 2010-08-23 19:51:55 +0000
3+++ src/launchpadlib/NEWS.txt 2010-10-20 13:56:48 +0000
4@@ -2,6 +2,12 @@
5 NEWS for launchpadlib
6 =====================
7
8+1.7.0 (Unreleased)
9+==================
10+
11+- Removed "fake Launchpad browser" code that didn't work and was
12+ misleading developers.
13+
14 1.6.5 (2010-08-23)
15 ==================
16
17
18=== modified file 'src/launchpadlib/__init__.py'
19--- src/launchpadlib/__init__.py 2010-08-23 19:51:55 +0000
20+++ src/launchpadlib/__init__.py 2010-10-20 13:56:48 +0000
21@@ -14,4 +14,4 @@
22 # You should have received a copy of the GNU Lesser General Public License
23 # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
24
25-__version__ = '1.6.5'
26+__version__ = '1.7.0'
27
28=== modified file 'src/launchpadlib/apps.py'
29--- src/launchpadlib/apps.py 2009-11-02 12:20:47 +0000
30+++ src/launchpadlib/apps.py 2010-10-20 13:56:48 +0000
31@@ -52,122 +52,3 @@
32 return simplejson.dumps(token)
33
34
35-class TrustedTokenAuthorizationConsoleApp(RequestTokenAuthorizationEngine):
36- """An application that authorizes request tokens."""
37-
38- def __init__(self, web_root, consumer_name, request_token,
39- access_levels='', input_method=raw_input):
40- """Constructor.
41-
42- :param access_levels: A string of comma-separated access level
43- values. To get an up-to-date list of access levels, pass
44- token_format=Credentials.DICT_TOKEN_FORMAT into
45- Credentials.get_request_token, load the dict as JSON, and look
46- in 'access_levels'.
47- """
48- access_levels = [level.strip() for level in access_levels.split(',')]
49- super(TrustedTokenAuthorizationConsoleApp, self).__init__(
50- web_root, consumer_name, request_token, access_levels)
51-
52- self.input_method = input_method
53-
54- def run(self):
55- """Try to authorize a request token from user input."""
56- self.error_code = -1 # Start off assuming failure.
57- start = "Launchpad credential client (console)"
58- self.output(start)
59- self.output("-" * len(start))
60-
61- try:
62- self()
63- except TokenAuthorizationException, e:
64- print str(e)
65- self.error_code = -1
66- return self.press_enter_to_exit()
67-
68- def exit_with(self, code):
69- """Exit the app with the specified error code."""
70- sys.exit(code)
71-
72- def get_single_char_input(self, prompt, valid):
73- """Retrieve a single-character line from the input stream."""
74- valid = valid.upper()
75- input = None
76- while input is None:
77- input = self.input_method(prompt).upper()
78- if len(input) != 1 or input not in valid:
79- input = None
80- return input
81-
82- def press_enter_to_exit(self):
83- """Make the user hit enter, and then exit with an error code."""
84- prompt = '\nPress enter to go back to "%s". ' % self.consumer_name
85- self.input_method(prompt)
86- self.exit_with(self.error_code)
87-
88- def input_username(self, cached_username, suggested_message):
89- """Collect the Launchpad username from the end-user.
90-
91- :param cached_username: A username from a previous entry attempt,
92- to be presented as the default.
93- """
94- if cached_username is not None:
95- extra = " [%s] " % cached_username
96- else:
97- extra = "\n(No Launchpad account? Just hit enter.) "
98- username = self.input_method(suggested_message + extra)
99- if username == '':
100- return cached_username
101- return username
102-
103- def input_password(self, suggested_message):
104- """Collect the Launchpad password from the end-user."""
105- if self.input_method is raw_input:
106- password = getpass.getpass(suggested_message + " ")
107- else:
108- password = self.input_method(suggested_message)
109- return password
110-
111- def input_access_level(self, available_levels, suggested_message,
112- only_one_option=None):
113- """Collect the desired level of access from the end-user."""
114- if only_one_option is not None:
115- self.output(suggested_message)
116- prompt = self.message(
117- 'Do you want to give "%(app)s" this level of access? [YN] ')
118- allow = self.get_single_char_input(prompt, "YN")
119- if allow == "Y":
120- return only_one_option['value']
121- else:
122- return self.UNAUTHORIZED_ACCESS_LEVEL
123- else:
124- levels_except_unauthorized = [
125- level for level in available_levels
126- if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL]
127- options = []
128- for i in range(0, len(levels_except_unauthorized)):
129- options.append(
130- "%d: %s" % (i+1, levels_except_unauthorized[i]['title']))
131- self.output(suggested_message)
132- for option in options:
133- self.output(option)
134- allowed = ("".join(map(str, range(1, i+2)))) + "Q"
135- prompt = self.message(
136- 'What should "%(app)s" be allowed to do using your '
137- 'Launchpad account? [1-%(max)d or Q] ',
138- extra_variables = {'max' : i+1})
139- allow = self.get_single_char_input(prompt, allowed)
140- if allow == "Q":
141- return self.UNAUTHORIZED_ACCESS_LEVEL
142- else:
143- return levels_except_unauthorized[int(allow)-1]['value']
144-
145- def user_refused_to_authorize(self, suggested_message):
146- """The user refused to authorize a request token."""
147- self.output(suggested_message)
148- self.error_code = -2
149-
150- def user_authorized(self, access_level, suggested_message):
151- """The user authorized a request token with some access level."""
152- self.output(suggested_message)
153- self.error_code = 0
154
155=== removed file 'src/launchpadlib/bin/launchpad-credentials-console'
156--- src/launchpadlib/bin/launchpad-credentials-console 2009-10-30 17:28:51 +0000
157+++ src/launchpadlib/bin/launchpad-credentials-console 1970-01-01 00:00:00 +0000
158@@ -1,54 +0,0 @@
159-#!/usr/bin/python
160-
161-# Copyright 2009 Canonical Ltd.
162-
163-# This file is part of launchpadlib.
164-#
165-# launchpadlib is free software: you can redistribute it and/or modify it
166-# under the terms of the GNU Lesser General Public License as published by the
167-# Free Software Foundation, version 3 of the License.
168-#
169-# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
170-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
171-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
172-# for more details.
173-#
174-# You should have received a copy of the GNU Lesser General Public License
175-# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
176-
177-"""Take the user's Launchpad credentials and authorize a request token.
178-
179-This script will guide the user through the process of authorizing a
180-request token with their username and password. It's not as secure as
181-opening the user's web browser, but if you are writing a console
182-application and want to keep the authorization process console-based,
183-it's better to use this script than write the authorization code yourself.
184-"""
185-
186-__metaclass__ = type
187-
188-from optparse import OptionParser
189-from launchpadlib.apps import (
190- RequestTokenApp, TrustedTokenAuthorizationConsoleApp)
191-
192-parser = OptionParser()
193-parser.usage = "%prog CONSUMER_NAME REQUEST_TOKEN [-r URL] [-a LEVEL,LEVEL,...]"
194-parser.add_option("-s", "--site", dest="web_root",
195- help=("The name of the Launchpad site on which the request "
196- "token can be found (default: %default). This can "
197- "also be the URL to the root of the site."),
198- metavar="URL", default="staging")
199-parser.add_option("-a", "--access-level", dest="access_levels",
200- help="Any restrictions on your clients' access levels",
201- metavar="[LEVEL,LEVEL,...]", default="")
202-
203-if __name__ == '__main__':
204- (options, args) = parser.parse_args()
205- if len(args) < 2:
206- parser.error("You must provide both a consumer name and the ID of "
207- "an OAuth request token on Launchpad.")
208- consumer_name, token_id = args[:2]
209- TrustedTokenAuthorizationConsoleApp(
210- options.web_root, consumer_name, token_id,
211- options.access_levels).run()
212-
213
214=== modified file 'src/launchpadlib/credentials.py'
215--- src/launchpadlib/credentials.py 2010-07-19 21:47:03 +0000
216+++ src/launchpadlib/credentials.py 2010-10-20 13:56:48 +0000
217@@ -184,330 +184,23 @@
218 super(AnonymousAccessToken, self).__init__('','')
219
220
221-class SimulatedLaunchpadBrowser(object):
222- """A programmable substitute for a human-operated web browser.
223-
224- Used by client programs to interact with Launchpad's credential
225- pages, without opening them in the user's actual web browser.
226- """
227-
228- def __init__(self, web_root=uris.STAGING_WEB_ROOT):
229- self.web_root = uris.lookup_web_root(web_root)
230- self.http = httplib2.Http()
231-
232- def _auth_header(self, username, password):
233- """Utility method to generate a Basic auth header."""
234- auth = base64.encodestring("%s:%s" % (username, password))[:-1]
235- return "Basic " + auth
236-
237- def get_token_info(self, username, password, request_token,
238- access_levels=''):
239- """Retrieve a JSON representation of a request token.
240-
241- This is useful for verifying that the end-user gave a valid
242- username and password, and for reconciling the client's
243- allowable access levels with the access levels defined in
244- Launchpad.
245- """
246- if access_levels != '':
247- s = "&allow_permission="
248- access_levels = s + s.join(access_levels)
249- page = "%s?oauth_token=%s%s" % (
250- authorize_token_page, request_token, access_levels)
251- url = urljoin(self.web_root, page)
252- # We can't use httplib2's add_credentials, because Launchpad
253- # doesn't respond to credential-less access with a 401
254- # response code.
255- headers = {'Accept' : 'application/json',
256- 'Referer' : self.web_root}
257- headers['Authorization'] = self._auth_header(username, password)
258- response, content = self.http.request(url, headers=headers)
259- # Detect common error conditions and set the response code
260- # appropriately. This lets code that uses
261- # SimulatedLaunchpadBrowser detect standard response codes
262- # instead of having Launchpad-specific knowledge.
263- location = response.get('content-location')
264- if response.status == 200 and '+login' in location:
265- response.status = 401
266- elif response.get('content-type') != 'application/json':
267- response.status = 500
268- return response, content
269-
270- def grant_access(self, username, password, request_token, access_level,
271- context=None):
272- """Grant a level of access to an application on behalf of a user."""
273- headers = {'Content-type' : 'application/x-www-form-urlencoded',
274- 'Referer' : self.web_root}
275- headers['Authorization'] = self._auth_header(username, password)
276- body = "oauth_token=%s&field.actions.%s=True" % (
277- quote(request_token), quote(access_level))
278- if context is not None:
279- body += "&lp.context=%s" % quote(context)
280- url = urljoin(self.web_root, "+authorize-token")
281- response, content = self.http.request(
282- url, method="POST", headers=headers, body=body)
283- # This would be much less fragile if Launchpad gave us an
284- # error code to work with.
285- if "Unauthenticated user POSTing to page" in content:
286- response.status = 401 # Unauthorized
287- elif 'Request already reviewed' in content:
288- response.status = 409 # Conflict
289- elif 'What level of access' in content:
290- response.status = 400 # Bad Request
291- elif 'Unable to identify application' in content:
292- response.status = 400 # Bad Request
293- elif not 'Almost finished' in content:
294- response.status = 500 # Internal Server Error
295- return response, content
296-
297-
298 class RequestTokenAuthorizationEngine(object):
299
300 UNAUTHORIZED_ACCESS_LEVEL = "UNAUTHORIZED"
301
302- # Suggested messages for clients to display in common situations.
303-
304- AUTHENTICATION_FAILURE = "I can't log in with the credentials you gave me. Let's try again."
305-
306- CHOOSE_ACCESS_LEVEL = """Now it's time for you to decide how much power to give "%(app)s" over your Launchpad account."""
307-
308- CHOOSE_ACCESS_LEVEL_ONE = CHOOSE_ACCESS_LEVEL + """
309-
310-"%(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."""
311-
312- USER_AUTHORIZED = """Okay, I'm telling Launchpad to grant "%(app)s" access to your account."""
313-
314- 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."""
315-
316- 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."""
317-
318- 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."""
319-
320- INPUT_USERNAME = "What email address do you use on Launchpad?"
321-
322- INPUT_PASSWORD = "What's your Launchpad password? "
323-
324- 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."""
325-
326- 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."""
327-
328- 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"."""
329-
330- 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."""
331-
332- 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."""
333-
334- SUCCESS = """You're all done! You should now be able to use Launchpad integration features of "%(app)s." """
335-
336- SUCCESS_UNAUTHORIZED = """You're all done! "%(app)s" still doesn't have access to your Launchpad account."""
337-
338- 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."""
339-
340- YOU_NEED_A_LAUNCHPAD_ACCOUNT = """OK, you'll need to get yourself a Launchpad account before you can integrate Launchpad into "%(app)s."
341-
342-I'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."""
343-
344 def __init__(self, web_root, consumer_name, request_token,
345- allow_access_levels=[], max_failed_attempts=3):
346+ allow_access_levels=[]):
347 self.web_root = uris.lookup_web_root(web_root)
348 self.consumer_name = consumer_name
349 self.request_token = request_token
350- self.browser = SimulatedLaunchpadBrowser(self.web_root)
351- self.max_failed_attempts = max_failed_attempts
352 self.allow_access_levels = allow_access_levels
353- self.text_wrapper = textwrap.TextWrapper(
354- replace_whitespace=False, width=78)
355
356 def __call__(self):
357-
358- self.startup(
359- [self.message(self.STARTUP_MESSAGE),
360- self.message(self.STARTUP_MESSAGE_2)])
361-
362- # Have the end-user enter their Launchpad username and password.
363- # Make sure the credentials are valid, and get information
364- # about the request token as a side effect.
365- username, password, token_info = self.get_http_credentials()
366-
367- # Update this object with fresh information about the request token.
368- self.token_info = token_info
369- self.reconciled_access_levels = token_info['access_levels']
370- self._check_consumer()
371-
372- # Have the end-user choose an access level from the fresh list.
373- if len(self.reconciled_access_levels) == 2:
374- # There's only one choice: allow access at a certain level
375- # or don't allow access at all.
376- message = self.CHOOSE_ACCESS_LEVEL_ONE
377- level = [level for level in self.reconciled_access_levels
378- if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL][0]
379- extra = {'level' : level['title']}
380- only_one_option = level
381- else:
382- message = self.CHOOSE_ACCESS_LEVEL
383- extra = None
384- only_one_option = None
385- access_level = self.input_access_level(
386- self.reconciled_access_levels, self.message(message, extra),
387- only_one_option)
388-
389- # Notify the program of the user's choice.
390- if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
391- self.user_refused_to_authorize(
392- self.message(self.USER_REFUSED_TO_AUTHORIZE))
393- else:
394- self.user_authorized(
395- access_level, self.message(self.USER_AUTHORIZED))
396-
397- # Try to grant the specified level of access to the request token.
398- response, content = self.browser.grant_access(
399- username, password, self.request_token, access_level)
400- if response.status == 409:
401- raise RequestTokenAlreadyAuthorized(
402- self.message(self.REQUEST_TOKEN_ALREADY_AUTHORIZED))
403- elif response.status == 400:
404- raise ClientError(self.message(self.CLIENT_ERROR))
405- elif response.status == 500:
406- raise ServerError(self.message(self.SERVER_ERROR))
407- if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
408- message = self.SUCCESS_UNAUTHORIZED
409- else:
410- message = self.SUCCESS
411- self.success(self.message(message))
412-
413- def get_http_credentials(self, cached_username=None, failed_attempts=0):
414- """Authenticate the user to Launchpad, or raise an exception trying.
415-
416- :return: A 3-tuple (username, password,
417- token_info). 'username' and 'password' are the validated
418- Launchpad username and password. 'token_info' is a dict of
419- validated information about the request token, including
420- Launchpad's reconciled list of its available access levels
421- with the access levels the third-party client will accept.
422-
423- :param cached_username: If the user has tried to enter their
424- credentials before and failed, this variable will contain the
425- username they entered the first time. This can be presented as
426- a default, since users are more likely to enter the wrong
427- password than the wrong username.
428-
429- :param failed_attempts: This method calls itself recursively
430- until failed_attempts equals self.max_failed_attempts.
431- """
432- username = self.input_username(
433- cached_username, self.message(self.INPUT_USERNAME))
434- if username is None:
435- self.open_page_in_user_browser(
436- urljoin(self.web_root, "+login"))
437- raise NoLaunchpadAccount(
438- self.message(self.YOU_NEED_A_LAUNCHPAD_ACCOUNT))
439- password = self.input_password(self.message(self.INPUT_PASSWORD))
440- response, content = self.browser.get_token_info(
441- username, password, self.request_token, self.allow_access_levels)
442- if response.status == 500:
443- raise ServerError(self.message(self.SERVER_ERROR))
444- elif response.status == 401:
445- failed_attempts += 1
446- if failed_attempts == self.max_failed_attempts:
447- raise TooManyAuthenticationFailures(
448- self.message(self.TOO_MANY_AUTHENTICATION_FAILURES))
449- else:
450- self.authentication_failure(
451- self.message(self.AUTHENTICATION_FAILURE))
452- return self.get_http_credentials(username, failed_attempts)
453- token_info = simplejson.loads(content)
454- # If Launchpad provides no information about the request token,
455- # that means the request token doesn't exist.
456- if 'oauth_token' not in token_info:
457- raise RequestTokenAlreadyAuthorized(
458- self.message(self.NONEXISTENT_REQUEST_TOKEN))
459- return username, password, token_info
460-
461- def _check_consumer(self):
462- """Sanity-check the server consumer against the client consumer."""
463- real_consumer = self.token_info['oauth_token_consumer']
464- if real_consumer != self.consumer_name:
465- message = self.message(
466- self.CONSUMER_MISMATCH, { 'old_consumer' : self.consumer_name,
467- 'real_consumer' : real_consumer })
468- self.server_consumer_differs_from_client_consumer(
469- self.consumer_name, real_consumer, message)
470- self.consumer_name = real_consumer
471-
472- def message(self, raw_message, extra_variables=None):
473- """Prepare a message by plugging in the app name."""
474- variables = { 'app' : self.consumer_name }
475- if extra_variables is not None:
476- variables.update(extra_variables)
477- return raw_message % variables
478-
479- def open_page_in_user_browser(self, url):
480- """Open a web page in the user's web browser."""
481- webbrowser.open(url)
482-
483- # You should define these methods in your subclass.
484-
485- def output(self, message):
486- print self.text_wrapper.fill(message)
487-
488- def input_username(self, cached_username, suggested_message):
489- """Collect the Launchpad username from the end-user.
490-
491- :param cached_username: A username from a previous entry attempt,
492- to be presented as the default.
493- """
494- raise NotImplementedError()
495-
496- def input_password(self, suggested_message):
497- """Collect the Launchpad password from the end-user."""
498- raise NotImplementedError()
499-
500- def input_access_level(self, available_levels, suggested_message,
501- only_one_option=None):
502- """Collect the desired level of access from the end-user."""
503- raise NotImplementedError()
504-
505- def startup(self, suggested_messages):
506- """Hook method called on startup."""
507- for message in suggested_messages:
508- self.output(message)
509- self.output("\n")
510-
511- def authentication_failure(self, suggested_message):
512- """The user entered invalid credentials."""
513- self.output(suggested_message)
514- self.output("\n")
515-
516- def user_refused_to_authorize(self, suggested_message):
517- """The user refused to authorize a request token."""
518- self.output(suggested_message)
519- self.output("\n")
520-
521- def user_authorized(self, access_level, suggested_message):
522- """The user authorized a request token with some access level."""
523- self.output(suggested_message)
524- self.output("\n")
525-
526- def server_consumer_differs_from_client_consumer(
527- self, client_name, real_name, suggested_message):
528- """The client seems to be lying or mistaken about its name.
529-
530- When requesting a request token, the client told Launchpad
531- that its consumer name was "foo". Now the client is telling the
532- end-user that its name is "bar". Something is fishy and at the very
533- least the end-user should be warned about this.
534- """
535- self.output("\n")
536- self.output(suggested_message)
537- self.output("\n")
538-
539- def success(self, suggested_message):
540- """The token was successfully authorized."""
541- self.output(suggested_message)
542+ raise NotImplementedError()
543
544
545 class AuthorizeRequestTokenWithBrowser(RequestTokenAuthorizationEngine):
546- """The simplest and most secure request token authorizer.
547+ """The simplest (and, right now, only) request token authorizer.
548
549 This authorizer simply opens up the end-user's web browser to a
550 Launchpad URL and lets the end-user authorize the request token
551
552=== removed file 'src/launchpadlib/docs/browser.txt'
553--- src/launchpadlib/docs/browser.txt 2009-10-28 18:04:29 +0000
554+++ src/launchpadlib/docs/browser.txt 1970-01-01 00:00:00 +0000
555@@ -1,151 +0,0 @@
556-*******************************
557-The simulated Launchpad browser
558-*******************************
559-
560-The SimulatedLaunchpadBrowser class is a scriptable browser-like class
561-that can be trusted with the end-user's username and password. It
562-fulfils the same function as the user's web browser, but because it's
563-scriptable can be used to create non-browser trusted clients.
564-
565- >>> username = 'salgado@ubuntu.com'
566- >>> password = 'zeca'
567- >>> web_root = 'http://launchpad.dev:8085/'
568-
569-Before showing how SimulatedLaunchpadBrowser can authorize a request
570-token, let's create a request token to authorize.
571-
572- >>> from launchpadlib.credentials import Credentials
573- >>> credentials = Credentials("doctest consumer")
574- >>> context="firefox"
575- >>> validate_url = credentials.get_request_token(
576- ... web_root=web_root, context=context)
577- >>> request_token = credentials._request_token.key
578-
579-get_token_info()
580-================
581-
582-If you have the end-user's username and password, you can use
583-get_token_info() to get information about one of the user's request
584-tokens. It's useful for confirming that the end-user gave the correct
585-username and password, and for reconciling the list of access levels a
586-client will accept with Launchpad's master list.
587-
588- >>> from launchpadlib.credentials import SimulatedLaunchpadBrowser
589- >>> from launchpadlib.testing.helpers import TestableLaunchpad
590-
591- >>> browser = SimulatedLaunchpadBrowser(web_root)
592-
593-If you make an unauthorized request, you'll get a 401 error.
594-(Launchpad returns 200, but SimulatedLaunchpadBrowser sniffs it and
595-changes it to a 401.)
596-
597- >>> response, content = browser.get_token_info(
598- ... "baduser", "badpasword", request_token)
599- >>> print response.status
600- 401
601-
602-If you provide the right authorization, you'll get back information
603-about your request token.
604-
605- >>> response, content = browser.get_token_info(
606- ... username, password, request_token)
607- >>> print response['content-type']
608- application/json
609-
610- # XXX leonardr 2009-10-28 bug=462773 These two URLs should be
611- # exactly the same, but the Content-Location is missing the
612- # lp.context.
613- >>> validate_url.startswith(response['content-location'])
614- True
615-
616- >>> import simplejson
617- >>> json = simplejson.loads(content)
618- >>> json['oauth_token'] == request_token
619- True
620-
621- >>> print json['oauth_token_consumer']
622- doctest consumer
623-
624-You'll also get information about the available access
625-levels.
626-
627- >>> print sorted([level['value'] for level in json['access_levels']])
628- ['READ_PRIVATE', ... 'UNAUTHORIZED', ...]
629-
630-If you provide a list of possible access levels, you'll
631-get back a list that reconciles the list you gave with
632-Launchpad's access levels.
633-
634- >>> response, content = browser.get_token_info(
635- ... username, password, request_token,
636- ... ["READ_PUBLIC", "READ_PRIVATE", "NO_SUCH_ACCESS_LEVEL"])
637-
638- >>> print response['content-type']
639- application/json
640-
641- >>> json = simplejson.loads(content)
642- >>> print sorted(
643- ... [level['value'] for level in json['access_levels']])
644- ['READ_PRIVATE', 'READ_PUBLIC', 'UNAUTHORIZED']
645-
646-Note that the nonexistent access level has been removed from the
647-reconciled list, and the "Unauthorized" access level (which must
648-always be an option) has been added.
649-
650-grant_access()
651-==============
652-
653-If you have the end-user's username and password, you can use
654-grant_access() to authorize a request token.
655-
656-If you make an unauthorized request, you'll get a 401 error. (As with
657-get_token_info(), Launchpad returns 200, but SimulatedLaunchpadBrowser
658-sniffs it and changes it to a 401.)
659-
660- >>> access_level = "READ_PRIVATE"
661-
662- >>> response, content = browser.grant_access(
663- ... "baduser", "badpasword", request_token, access_level, context)
664- >>> print response.status
665- 401
666-
667-If you try to grant an invalid level of access, you'll get a
668-400 error.
669-
670- >>> response, content = browser.grant_access(
671- ... username, password, request_token,
672- ... "NO_SUCH_ACCESS_LEVEL")
673- >>> print response.status
674- 400
675-
676-If you provide all the necessary information, you'll get a 200
677-response code and the request token will be authorized.
678-
679- >>> response, content = browser.grant_access(
680- ... username, password, request_token, access_level)
681- >>> print response.status
682- 200
683-
684-If you try to grant access to a request token that's already
685-been authorized, you'll get a 409 error.
686-
687- >>> response, content = browser.grant_access(
688- ... username, password, request_token, access_level)
689- >>> print response.status
690- 409
691-
692-Now that the request token is authorized, we can exchange it for an
693-access token.
694-
695- >>> credentials.exchange_request_token_for_access_token(
696- ... web_root=web_root)
697- >>> credentials.access_token.key is None
698- False
699-
700-If you try to grant access to a request token that's already been
701-exchanged for an access token, you'll get a 400 error.
702-
703- >>> response, content = browser.grant_access(
704- ... username, password, request_token, access_level)
705- >>> print response.status
706- 400
707
708=== modified file 'src/launchpadlib/docs/command-line.txt'
709--- src/launchpadlib/docs/command-line.txt 2009-11-03 13:50:27 +0000
710+++ src/launchpadlib/docs/command-line.txt 2010-10-20 13:56:48 +0000
711@@ -2,12 +2,11 @@
712 Command-line scripts
713 ********************
714
715-Launchpad includes some command-line scripts to make Launchpad
716+Launchpad includes one command-line script to make Launchpad
717 integration easier for third-party libraries that aren't written in
718-Python or that can't do token authorization by opening a user's web
719-browser.
720+Python.
721
722-This file tests the workflow underlying the command-line scripts as
723+This file tests the workflow underlying the command-line script as
724 best it can.
725
726 RequestTokenApp
727@@ -36,170 +35,3 @@
728 >>> print json['oauth_token_consumer']
729 consumer
730
731-TrustedTokenAuthorizationConsoleApp
732-===================================
733-
734-This class is called by the command-line script
735-launchpad-credentials-console. It asks for the user's Launchpad
736-username and password, and authorizes a request token on their
737-behalf.
738-
739- >>> from launchpadlib.apps import TrustedTokenAuthorizationConsoleApp
740- >>> from launchpadlib.testing.helpers import UserInput
741-
742-This class does not create the request token, or exchange it for the
743-access token--that's the job of the program that calls
744-launchpad-credentials-console. So we'll use the request token created
745-earlier by RequestTokenApp.
746-
747- >>> request_token = json['oauth_token']
748-
749-Since this is a test, we don't want the application to call sys.exit()
750-or try to open up pages in a web browser. This subclass of
751-TrustedTokenAuthorizationConsoleApp will print messages instead of
752-performing such un-doctest-like actions.
753-
754- >>> class ConsoleApp(TrustedTokenAuthorizationConsoleApp):
755- ... def open_page_in_user_browser(self, url):
756- ... """Print a status message."""
757- ... self.output("[If this were a real application, the "
758- ... "end-user's web browser would be opened "
759- ... "to %s]" % url)
760- ...
761- ... def exit_with(self, code):
762- ... print "Application exited with code %d" % code
763-
764-We'll use a UserInput object to simulate a user typing things in at
765-the prompt and hitting enter. This UserInput runs the program
766-correctly, entering the Launchpad username and password, choosing an
767-access level, and hitting Enter to exit.
768-
769- >>> username = "salgado@ubuntu.com"
770- >>> password = "zeca"
771- >>> fake_input = UserInput([username, password, "1", ""])
772-
773-Here's a successful run of the application. When the request token is
774-authorized, the script's response code is 0.
775-
776- >>> app = ConsoleApp(
777- ... web_root, consumer_name, request_token,
778- ... 'READ_PRIVATE, READ_PUBLIC', input_method=fake_input)
779- >>> app.run()
780- Launchpad credential client (console)
781- -------------------------------------
782- An application identified as "consumer" wants to access Launchpad...
783- What email address do you use on Launchpad?
784- (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com]
785- What's your Launchpad password? [User input: zeca]
786- Now it's time for you to decide how much power to give "consumer"...
787- 1: Read Non-Private Data
788- 2: Read Anything
789- What should "consumer" be allowed to do...? [1-2 or Q] [User input: 1]
790- Okay, I'm telling Launchpad to grant "consumer" access to your account.
791- You're all done!...
792- Press enter to go back to "consumer". [User input: ]
793- Application exited with code 0
794-
795-Now that the request token has been authorized, we'll need to create
796-another one to continue the test.
797-
798- >>> json = simplejson.loads(token_app.run())
799- >>> request_token = json['oauth_token']
800- >>> app.request_token = request_token
801-
802-Invalid input is ignored. The user may enter 'Q' instead of a number
803-to refuse to authorize the request token. When the user denies access,
804-the exit code is -2.
805-
806- >>> fake_input = UserInput([username, password, "A", "99", "Q", ""])
807- >>> app.input_method = fake_input
808- >>> app.run()
809- Launchpad credential client (console)
810- -------------------------------------
811- An application identified as "consumer"...
812- What should "consumer" be allowed to do...? [1-2 or Q] [User input: A]
813- What should "consumer" be allowed to do...? [1-2 or Q] [User input: 99]
814- What should "consumer" be allowed to do...? [1-2 or Q] [User input: Q]
815- Okay, I'm going to cancel the request...
816- You're all done! "consumer" still doesn't have access...
817- <BLANKLINE>
818- Press enter to go back to "consumer". [User input: ]
819- Application exited with code -2
820-
821-When the third-party application will allow only one level of access,
822-the end-user is presented with a yes-or-no choice instead of a list to
823-choose from. Again, invalid input is ignored.
824-
825- >>> json = simplejson.loads(token_app.run())
826- >>> request_token = json['oauth_token']
827- >>> fake_input = UserInput([username, password, "1", "Q", "Y", ""])
828-
829- >>> app = ConsoleApp(
830- ... web_root, consumer_name, request_token,
831- ... 'READ_PRIVATE', input_method=fake_input)
832-
833- >>> app.run()
834- Launchpad credential client (console)
835- -------------------------------------
836- An application identified as "consumer"...
837- Do you want to give "consumer" this level of access? [YN] [User input: 1]
838- Do you want to give "consumer" this level of access? [YN] [User input: Q]
839- Do you want to give "consumer" this level of access? [YN] [User input: Y]
840- ...
841- Application exited with code 0
842-
843-
844-Error handling
845---------------
846-
847-When the end-user refuses to authorize the request token, the app
848-exits with a return code of -2, as seen above. When any other error
849-gets in the way of the authorization of the request token, the app's
850-return code is -1.
851-
852-If the user hits enter when asked for their email address, indicating
853-that they don't have a Launchpad account, the app opens their browser
854-to the Launchpad login page.
855-
856- >>> json = simplejson.loads(token_app.run())
857- >>> app.request_token = json['oauth_token']
858-
859- >>> input_nothing = UserInput(["", ""])
860- >>> app.input_method = input_nothing
861-
862- >>> app.run()
863- Launchpad credential client (console)
864- -------------------------------------
865- An application identified as "consumer"...
866- [If this were a real application, the end-user's web browser...]
867- OK, you'll need to get yourself a Launchpad account before...
868- <BLANKLINE>
869- I'm opening the Launchpad registration page in your web browser...
870- Press enter to go back to "consumer". [User input: ]
871- Application exited with code -1
872-
873-If the user keeps entering bad passwords, the app eventually gives up.
874-
875- >>> input_bad_password = UserInput(
876- ... [username, "badpw", "", "badpw", "", "badpw", ""])
877- >>> json = simplejson.loads(token_app.run())
878- >>> request_token = json['oauth_token']
879- >>> app.request_token = request_token
880- >>> app.input_method = input_bad_password
881- >>> app.run()
882- Launchpad credential client (console)
883- -------------------------------------
884- An application identified as "consumer"...
885- What email address do you use on Launchpad?
886- (No Launchpad account? Just hit enter.) [User input: salgado@ubuntu.com]
887- What's your Launchpad password? [User input: badpw]
888- I can't log in with the credentials you gave me. Let's try again.
889- What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ]
890- What's your Launchpad password? [User input: badpw]
891- I can't log in with the credentials you gave me. Let's try again.
892- What email address do you use on Launchpad? [salgado@ubuntu.com] [User input: ]
893- What's your Launchpad password? [User input: badpw]
894- You've failed the password entry too many times...
895- Press enter to go back to "consumer". [User input: ]
896- Application exited with code -1
897-
898
899=== modified file 'src/launchpadlib/docs/introduction.txt'
900--- src/launchpadlib/docs/introduction.txt 2010-08-17 20:21:12 +0000
901+++ src/launchpadlib/docs/introduction.txt 2010-10-20 13:56:48 +0000
902@@ -211,12 +211,12 @@
903 salgado
904
905 Otherwise, the application should obtain authorization from the user
906-and get a new set of credentials directly from Launchpad.
907+and get a new set of credentials directly from
908+Launchpad.
909
910-First we must get a request token. We use 'test_dev' as a shorthand
911-for the root URL of the Launchpad installation. It's defined in the
912-'uris' module as 'http://launchpad.dev:8085/', and the launchpadlib
913-code knows how to dereference it before using it as a URL.
914+Unfortunately, we can't test this entire process because it requires
915+opening up a web browser, but we can test the first step, which is to
916+get a request token.
917
918 >>> import launchpadlib.credentials
919 >>> credentials = Credentials('consumer')
920@@ -226,6 +226,11 @@
921 >>> authorization_url
922 'http://launchpad.dev:8085/+authorize-token?oauth_token=...&lp.context=firefox'
923
924+We use 'test_dev' as a shorthand for the root URL of the Launchpad
925+installation. It's defined in the 'uris' module as
926+'http://launchpad.dev:8085/', and the launchpadlib code knows how to
927+dereference it before using it as a URL.
928+
929 Information about the request token is kept in the _request_token
930 attribute of the Credentials object.
931
932@@ -236,121 +241,10 @@
933 >>> print credentials._request_token.context
934 firefox
935
936-Now the user must authorize that token, so we'll use the
937-SimulatedLaunchpadBrowser to pretend the user is authorizing it.
938-
939- >>> from launchpadlib.credentials import SimulatedLaunchpadBrowser
940- >>> browser = SimulatedLaunchpadBrowser(web_root='test_dev')
941- >>> response, content = browser.grant_access(
942- ... "foo.bar@canonical.com", "test",
943- ... credentials._request_token.key, "WRITE_PRIVATE",
944- ... credentials._request_token.context)
945- >>> response['status']
946- '200'
947-
948-After that we can exchange that request token for an access token.
949-
950- >>> credentials.exchange_request_token_for_access_token(
951- ... web_root='test_dev')
952-
953-Once that's done, our credentials will be complete and ready to use.
954-
955- >>> credentials.consumer.key
956- 'consumer'
957- >>> credentials.access_token
958- <launchpadlib.credentials.AccessToken...
959- >>> credentials.access_token.key is not None
960- True
961- >>> credentials.access_token.secret is not None
962- True
963- >>> credentials.access_token.context
964- 'firefox'
965-
966-# [XXX leonardr 2010-08-17 bug=619446] Commenting out tests that
967-# started failing due to assuming Launchpad behavior that no longer
968-# exists.
969-
970-# Authorizing the request token
971-# -----------------------------
972-
973-# There are also two convenience method which do the access token
974-# negotiation and log into the web service: get_token_and_login() and
975-# login_with(). These convenience methods use the methods documented
976-# above to get a request token, and once it has the request token's
977-# authorization information, it makes the end-user authorize the request
978-# token by entering their Launchpad username and password.
979-
980-# There are several ways of having the end-user authorize a request
981-# token, but the most secure is to open up the user's own web browser
982-# (other ways are described in trusted-client.txt). Because we don't
983-# want to actually open a web browser during this test, we'll create a
984-# fake authorizer that uses the SimulatedLaunchpadBrowser to authorize
985-# the request token.
986-
987-# >>> from launchpadlib.testing.helpers import (
988-# ... DummyAuthorizeRequestTokenWithBrowser)
989-
990-# >>> class AuthorizeAsSalgado(DummyAuthorizeRequestTokenWithBrowser):
991-# ... def wait_for_request_token_authorization(self):
992-# ... """Simulate the authorizing user with their web browser."""
993-# ... username = 'salgado@ubuntu.com'
994-# ... password = 'zeca'
995-# ... browser = SimulatedLaunchpadBrowser(self.web_root)
996-# ... browser.grant_access(username, password, self.request_token,
997-# ... 'READ_PUBLIC')
998-
999-# Here, we're using 'test_dev' as shorthand for the root URL of the web
1000-# service. Earlier we used 'test_dev' as shorthand for the website URL,
1001-# and like in that earlier case, launchpadlib will internally
1002-# dereference 'test_dev' into the service root URL, defined in the
1003-# 'uris' module as "http://api.launchpad.dev:8085/".
1004-
1005-# >>> consumer_name = 'launchpadlib'
1006-# >>> launchpad = Launchpad.get_token_and_login(
1007-# ... consumer_name, service_root="test_dev",
1008-# ... authorizer_class=AuthorizeAsSalgado)
1009-# [If this were a real application, the end-user's web browser would
1010-# be opened to http://launchpad.dev:8085/+authorize-token?oauth_token=...]
1011-# The authorization page:
1012-# (http://launchpad.dev:8085/+authorize-token?oauth_token=...)
1013-# should be opening in your browser. After you have authorized
1014-# this program to access Launchpad on your behalf you should come
1015-# back here and press <Enter> to finish the authentication process.
1016-
1017-# The login_with method will cache an access token once it gets one, so
1018-# that the end-user doesn't have to authorize a request token every time
1019-# they run the program.
1020-
1021-# >>> import tempfile
1022-# >>> cache_dir = tempfile.mkdtemp()
1023-# >>> launchpad = Launchpad.login_with(
1024-# ... consumer_name, service_root="test_dev",
1025-# ... launchpadlib_dir=cache_dir,
1026-# ... authorizer_class=AuthorizeAsSalgado)
1027-# [If this were a real application...]
1028-# The authorization page:
1029-# ...
1030-# >>> print launchpad.me.name
1031-# salgado
1032-
1033-# Now that the access token is authorized, we can call login_with()
1034-# again and pass in a null authorizer. If there was no access token,
1035-# this would fail, because there would be no way to authorize the
1036-# request token. But since there's an access token cached in the
1037-# cache directory, login_with() will succeed without even trying to
1038-# authorize a request token.
1039-
1040-# >>> launchpad = Launchpad.login_with(
1041-# ... consumer_name, service_root="test_dev",
1042-# ... launchpadlib_dir=cache_dir,
1043-# ... authorizer_class=None)
1044-# >>> print launchpad.me.name
1045-# salgado
1046-
1047-# A bit of clean-up: removing the cache directory.
1048-
1049-# >>> import shutil
1050-# >>> shutil.rmtree(cache_dir)
1051+Now the user must authorize that token, and this is the part we can't
1052+test--it requires opening a web browser. Once the token is authorized
1053+on the server side, we can call exchange_request_token_for_access_token()
1054+on our Credentials object, which will then be ready to use.
1055
1056 The dictionary request token
1057 ============================
1058
1059=== removed file 'src/launchpadlib/docs/trusted-client.txt'
1060--- src/launchpadlib/docs/trusted-client.txt 2009-10-30 19:19:07 +0000
1061+++ src/launchpadlib/docs/trusted-client.txt 1970-01-01 00:00:00 +0000
1062@@ -1,224 +0,0 @@
1063-***********************
1064-Making a trusted client
1065-***********************
1066-
1067-To authorize a request token, the end-user must type in their
1068-Launchpad username and password. Obviously, typing your password into
1069-a random program is a bad idea. The best case is to use a program you
1070-already trust with your Launchpad password: your web browser.
1071-
1072-But if you're writing an application that can't open the end-user's
1073-web browser, or you just really want a token authorization client that
1074-has the same UI as the rest of your application, you should use one of
1075-the trusted clients packaged with launchpadlib, rather than writing
1076-your own client.
1077-
1078-All the trusted clients are based on the same core code and implement
1079-the same workflow. This test implements a scriptable trusted client
1080-and uses it to test the behavior of the standard workflow.
1081-
1082- >>> from launchpadlib.testing.helpers import (
1083- ... ScriptableRequestTokenAuthorization)
1084-
1085-Here we see the normal workflow, in which the user inputs all the
1086-correct data to authorize a request token.
1087-
1088- >>> auth = ScriptableRequestTokenAuthorization(
1089- ... "consumer", "salgado@ubuntu.com", "zeca",
1090- ... "WRITE_PRIVATE",
1091- ... allow_access_levels = ["WRITE_PUBLIC", "WRITE_PRIVATE"])
1092- >>> access_token = auth()
1093- An application identified as "consumer" wants to access Launchpad...
1094- <BLANKLINE>
1095- I'll use your Launchpad password to give "consumer" limited access...
1096- What email address do you use on Launchpad?
1097- What's your Launchpad password?
1098- Now it's time for you to decide how much power to give "consumer" ...
1099- ['UNAUTHORIZED', 'WRITE_PUBLIC', 'WRITE_PRIVATE']
1100- Okay, I'm telling Launchpad to grant "consumer" access to your account.
1101- You're all done! You should now be able to use Launchpad ...
1102-
1103-Ordinarily, the third-party program will create a request token and
1104-pass it into the trusted client. The test class is a little unusual:
1105-it takes care of creating the request token and, after the end-user
1106-has authorized it, exchanges the request token for an access
1107-token. This way we can verify that the entire end-to-end process
1108-works.
1109-
1110- >>> access_token.key is not None
1111- True
1112-
1113-Denying access
1114-==============
1115-
1116-It's always possible for the end-user to deny access to the
1117-application. This will make it impossible to convert the request token
1118-into an access token.
1119-
1120- >>> auth = ScriptableRequestTokenAuthorization(
1121- ... "consumer", "salgado@ubuntu.com", "zeca", "UNAUTHORIZED")
1122- >>> access_token = auth()
1123- An application identified as "consumer" wants to access Launchpad...
1124- What email address do you use on Launchpad?
1125- ...
1126- Okay, I'm going to cancel the request that "consumer" made...
1127- You're all done! "consumer" still doesn't have access...
1128-
1129- >>> access_token is None
1130- True
1131-
1132-Only one allowable access level
1133-===============================
1134-
1135-When the application being authenticated only allows one access level,
1136-the authorizer creates a special message for display to the end-user.
1137-
1138- >>> auth = ScriptableRequestTokenAuthorization(
1139- ... "consumer", "salgado@ubuntu.com", "zeca",
1140- ... "WRITE_PRIVATE", allow_access_levels=["WRITE_PRIVATE"])
1141-
1142- >>> auth()
1143- An application identified as "consumer" wants to access Launchpad ...
1144- ...
1145- "consumer" says it needs the following level of access to your Launchpad
1146- account: "Change Anything". It can't work with any other level of access,
1147- so denying this level of access means prohibiting "consumer" from
1148- using your Launchpad account at all.
1149- ...
1150-
1151-Error handling
1152-==============
1153-
1154-Things can go wrong in many ways, most of which we can test with our
1155-scriptable authorizer. Here's a utility method to run the
1156-authorization process with a badly-scripted authorizer and print the
1157-resulting exception.
1158-
1159- >>> from launchpadlib.credentials import TokenAuthorizationException
1160- >>> def print_error(auth):
1161- ... try:
1162- ... auth()
1163- ... except TokenAuthorizationException, e:
1164- ... print str(e)
1165-
1166-Authentication failures
1167------------------------
1168-
1169-If the user doesn't have a Launchpad account, or refuses to type in
1170-their email address, the authorizer will open their web browser to the
1171-login page, and raise an exception.
1172-
1173- >>> auth = ScriptableRequestTokenAuthorization(
1174- ... "consumer", None, "zeca", "WRITE_PRIVATE")
1175- >>> print_error(auth)
1176- An application identified as "consumer" wants to access Launchpad ...
1177- [If this were a real application, ... opened to http://launchpad.dev:8085/+login]
1178- OK, you'll need to get yourself a Launchpad account before you can ...
1179- <BLANKLINE>
1180- I'm opening the Launchpad registration page in your web browser ...
1181-
1182-If the user enters the wrong username/password combination too many
1183-times, the authorizer will give up and raise an exception.
1184-
1185- >>> auth = ScriptableRequestTokenAuthorization(
1186- ... "consumer", "salgado@ubuntu.com", "baddpassword",
1187- ... "WRITE_PRIVATE")
1188- >>> print_error(auth)
1189- An application identified as "consumer" wants to access Launchpad...
1190- ...
1191- What email address do you use on Launchpad?
1192- What's your Launchpad password?
1193- I can't log in with the credentials you gave me. Let's try again.
1194- What email address do you use on Launchpad?
1195- Cached email address: salgado@ubuntu.com
1196- What's your Launchpad password?
1197- You've failed the password entry too many times...
1198-
1199-The max_failed_attempts argument controls how many attempts the user
1200-is given to enter their username and password.
1201-
1202- >>> auth = ScriptableRequestTokenAuthorization(
1203- ... "consumer", "bad username", "zeca",
1204- ... "WRITE_PRIVATE", max_failed_attempts=1)
1205- >>> print_error(auth)
1206- An application identified as "consumer" wants to access Launchpad ...
1207- What email address do you use on Launchpad?
1208- What's your Launchpad password?
1209- You've failed the password entry too many times...
1210-
1211-Approving a token that was already approved
1212--------------------------------------------
1213-
1214-To set this up, let's approve a request token but not exchange it for
1215-an access token.
1216-
1217- >>> auth = ScriptableRequestTokenAuthorization(
1218- ... "consumer", "salgado@ubuntu.com", "zeca",
1219- ... "WRITE_PRIVATE")
1220- >>> auth(exchange_for_access_token=False)
1221- An application identified as "consumer" wants to access Launchpad ...
1222- ...
1223-
1224-Now let's try to approve the request token again:
1225-
1226- >>> print_error(auth)
1227- An application identified as "consumer" wants to access Launchpad ...
1228- ...
1229- It looks like you already approved this request...
1230-
1231-Once the request token is exchanged for an access token, it's
1232-deleted. An attempt to approve a request token that's already been
1233-exchanged for an access token gives an error message.
1234-
1235- >>> auth.credentials.exchange_request_token_for_access_token(
1236- ... web_root=auth.web_root)
1237-
1238- >>> print_error(auth)
1239- An application identified as "consumer" wants to access Launchpad ...
1240- ...
1241- Launchpad couldn't find an outstanding request for integration...
1242-
1243-An attempt to approve a nonexistent request token gives the same error
1244-message.
1245-
1246- >>> auth = ScriptableRequestTokenAuthorization(
1247- ... "consumer", "salgado@ubuntu.com", "zeca",
1248- ... "WRITE_PRIVATE")
1249- >>> auth.request_token = "nosuchrequesttoken"
1250- >>> print_error(auth)
1251- An application identified as "consumer" wants to access Launchpad ...
1252- ...
1253- Launchpad couldn't find an outstanding request for integration...
1254-
1255-Miscellaneous error
1256--------------------
1257-
1258-Random errors on the server side or (occasionally) the client side
1259-will result in a generic error message.
1260-
1261- >>> auth.request_token = "this token will confuse launchpad badly"
1262- >>> print_error(auth)
1263- An application identified as "consumer" wants to access Launchpad ...
1264- ...
1265- There seems to be something wrong on the Launchpad server side...
1266-
1267-Client duplicity
1268-----------------
1269-
1270-If the third-party client gives one consumer name to Launchpad, and a
1271-different consumer name to the authorizer, the authorizer will detect
1272-this possible duplicity and print a warning.
1273-
1274- >>> auth = ScriptableRequestTokenAuthorization(
1275- ... "consumer1", "salgado@ubuntu.com", "zeca",
1276- ... "WRITE_PRIVATE")
1277-
1278-We'll simulate this by changing the authorizer's .consumer_name after
1279-it obtained a request token from Launchpad.
1280-
1281- >>> auth.consumer_name = "consumer2"
1282- >>> auth()
1283- An application identified as "consumer2" wants to access Launchpad ...
1284- ...
1285- WARNING: The application you're using told me its name was "consumer2", but it told Launchpad its name was "consumer1"...
1286- ...
1287
1288=== modified file 'src/launchpadlib/testing/helpers.py'
1289--- src/launchpadlib/testing/helpers.py 2010-02-15 16:34:26 +0000
1290+++ src/launchpadlib/testing/helpers.py 2010-10-20 13:56:48 +0000
1291@@ -31,8 +31,9 @@
1292
1293 from launchpadlib.launchpad import Launchpad
1294 from launchpadlib.credentials import (
1295- AuthorizeRequestTokenWithBrowser, Credentials,
1296- RequestTokenAuthorizationEngine, SimulatedLaunchpadBrowser)
1297+ AuthorizeRequestTokenWithBrowser,
1298+ Credentials,
1299+ )
1300
1301
1302 class TestableLaunchpad(Launchpad):
1303@@ -65,106 +66,3 @@
1304 salgado_with_full_permissions = KnownTokens('salgado-change-anything', 'test')
1305 salgado_read_nonprivate = KnownTokens('salgado-read-nonprivate', 'secret')
1306 nopriv_read_nonprivate = KnownTokens('nopriv-read-nonprivate', 'mystery')
1307-
1308-
1309-class ScriptableRequestTokenAuthorization(RequestTokenAuthorizationEngine):
1310- """A request token process that doesn't need any user input.
1311-
1312- The RequestTokenAuthorizationEngine is supposed to be hooked up to a
1313- user interface, but that makes it difficult to test. This subclass
1314- is designed to be easy to test.
1315- """
1316-
1317- def __init__(self, consumer_name, username, password, choose_access_level,
1318- allow_access_levels=[], max_failed_attempts=2,
1319- web_root="http://launchpad.dev:8085/"):
1320-
1321- # Get a request token.
1322- self.credentials = Credentials(consumer_name)
1323- self.credentials.get_request_token(web_root=web_root)
1324-
1325- # Initialize the superclass with the new request token.
1326- super(ScriptableRequestTokenAuthorization, self).__init__(
1327- web_root, consumer_name, self.credentials._request_token.key,
1328- allow_access_levels, max_failed_attempts)
1329-
1330- self.username = username
1331- self.password = password
1332- self.choose_access_level = choose_access_level
1333-
1334- def __call__(self, exchange_for_access_token=True):
1335- super(ScriptableRequestTokenAuthorization, self).__call__()
1336-
1337- # Now verify that it worked by exchanging the authorized
1338- # request token for an access token.
1339- if (exchange_for_access_token and
1340- self.choose_access_level != self.UNAUTHORIZED_ACCESS_LEVEL):
1341- self.credentials.exchange_request_token_for_access_token(
1342- web_root=self.web_root)
1343- return self.credentials.access_token
1344- return None
1345-
1346- def open_page_in_user_browser(self, url):
1347- """Print a status message."""
1348- print ("[If this were a real application, the end-user's web "
1349- "browser would be opened to %s]" % url)
1350-
1351- def input_username(self, cached_username, suggested_message):
1352- """Collect the Launchpad username from the end-user."""
1353- print suggested_message
1354- if cached_username is not None:
1355- print "Cached email address: " + cached_username
1356- return self.username
1357-
1358- def input_password(self, suggested_message):
1359- """Collect the Launchpad password from the end-user."""
1360- print suggested_message
1361- return self.password
1362-
1363- def input_access_level(self, available_levels, suggested_message,
1364- only_one_option):
1365- """Collect the desired level of access from the end-user."""
1366- print suggested_message
1367- print [level['value'] for level in available_levels]
1368- return self.choose_access_level
1369-
1370- def startup(self, suggested_messages):
1371- for message in suggested_messages:
1372- print message
1373-
1374-
1375-class DummyAuthorizeRequestTokenWithBrowser(AuthorizeRequestTokenWithBrowser):
1376-
1377- def __init__(self, web_root, consumer_name, request_token, username,
1378- password, allow_access_levels=[], max_failed_attempts=3):
1379- super(DummyAuthorizeRequestTokenWithBrowser, self).__init__(
1380- web_root, consumer_name, request_token, allow_access_levels,
1381- max_failed_attempts)
1382-
1383- def open_page_in_user_browser(self, url):
1384- """Print a status message."""
1385- print ("[If this were a real application, the end-user's web "
1386- "browser would be opened to %s]" % url)
1387-
1388-
1389-class UserInput(object):
1390- """A class to store fake user input in a readable way.
1391-
1392- An instance of this class can be used as a substitute for the
1393- raw_input() function.
1394- """
1395-
1396- def __init__(self, inputs):
1397- """Initialize with a line of user inputs."""
1398- self.stream = iter(inputs)
1399-
1400- def __call__(self, prompt):
1401- """Print and return the next line of input."""
1402- line = self.readline()
1403- print prompt + "[User input: %s]" % line
1404- return line
1405-
1406- def readline(self):
1407- """Return the next line of input."""
1408- next_input = self.stream.next()
1409- return str(next_input)

Subscribers

People subscribed via source and target branches