Merge lp:~leonardr/launchpadlib/trusted-workflow into lp:launchpadlib

Proposed by Leonard Richardson
Status: Merged
Merged at revision: not available
Proposed branch: lp:~leonardr/launchpadlib/trusted-workflow
Merge into: lp:launchpadlib
Diff against target: 569 lines
5 files modified
src/launchpadlib/apps.py (+45/-0)
src/launchpadlib/credentials.py (+219/-0)
src/launchpadlib/docs/browser.txt (+5/-1)
src/launchpadlib/docs/trusted-client.txt (+160/-0)
src/launchpadlib/testing/helpers.py (+89/-0)
To merge this branch: bzr merge lp:~leonardr/launchpadlib/trusted-workflow
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code Approve
Review via email: mp+14107@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch introduces a generic engine for a trusted request-token-authorization client. I'm doing this because by and large, third-party developers aren't satisfied with opening the user's web browser and having them authorize a request token through the browser. They want a client that fits in with the rest of the UI. This generic engine can be used by command-line and GUI clients, and I've tested it by writing a scriptable client that implements stubs for all the plugin methods.

I have not written all the tests I'm going to write for this client, but I've written a representative sample, and I want to get what I have landed because this branch is getting kind of large.

There are two unrelated changes in this branch that I should have caught in my earlier branch (json-token-format):

1. I added launchpadlib/apps.py, which was written for the previous branch but I forgot to bzr add it. This contains the class tested by doc/command-line.txt.

2. A test in browser.txt was failing because of a bug in Launchpad. I filed a bug, hacked the test, and added a XXX.

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi Leonard,

This branch looks good. Just one comment below.

merge-approved

-Edwin

>=== modified file 'src/launchpadlib/credentials.py'
>--- src/launchpadlib/credentials.py 2009-10-27 15:24:36 +0000
>+++ src/launchpadlib/credentials.py 2009-10-28 20:22:45 +0000
>+
>+ def get_http_credentials(self, cached_username=None, failed_attempts=0):

Missing docstring.

>+ username = self.input_username(
>+ cached_username, self.message(self.INPUT_USERNAME))
>+ if username is None:
>+ self.open_login_page_in_user_browser(
>+ urljoin(self.web_root, "+login"))
>+ raise NoLaunchpadAccount(
>+ self.message(self.YOU_NEED_A_LAUNCHPAD_ACCOUNT))
>+ password = self.input_password(self.message(self.INPUT_PASSWORD))
>+ response, content = self.browser.get_token_info(
>+ username, password, self.request_token, self.allow_access_levels)
>+ if response.status == 500:
>+ raise ServerError(self.message(SERVER_ERROR))
>+ elif response.status == 401:
>+ failed_attempts += 1
>+ if failed_attempts == self.max_failed_attempts:
>+ raise TooManyAuthenticationFailures(
>+ self.message(self.TOO_MANY_AUTHENTICATION_FAILURES))
>+ else:
>+ self.authentication_failure(
>+ self.message(self.AUTHENTICATION_FAILURE))
>+ return self.get_http_credentials(username, failed_attempts)
>+ token_info = simplejson.loads(content)
>+ return username, password, token_info

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'src/launchpadlib/apps.py'
--- src/launchpadlib/apps.py 1970-01-01 00:00:00 +0000
+++ src/launchpadlib/apps.py 2009-10-28 20:20:23 +0000
@@ -0,0 +1,45 @@
1# Copyright 2009 Canonical Ltd.
2
3# This file is part of launchpadlib.
4#
5# launchpadlib is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by the
7# Free Software Foundation, version 3 of the License.
8#
9# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12# for more details.
13#
14# 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/>.
16
17"""Command-line applications for Launchpadlib.
18
19This module contains the code for various applications. The applications
20themselves are kept in bin/.
21"""
22
23import simplejson
24
25from launchpadlib.credentials import Credentials
26from launchpadlib.uris import lookup_web_root
27
28
29class RequestTokenApp(object):
30 """An application that creates request tokens."""
31
32 def __init__(self, web_root, consumer_name, context):
33 """Initialize."""
34 self.web_root = lookup_web_root(web_root)
35 self.credentials = Credentials(consumer_name)
36 self.context = context
37
38 def run(self):
39 """Get a request token and return JSON information about it."""
40 token = self.credentials.get_request_token(
41 self.context, self.web_root,
42 token_format=Credentials.DICT_TOKEN_FORMAT)
43 return simplejson.dumps(token)
44
45
046
=== modified file 'src/launchpadlib/credentials.py'
--- src/launchpadlib/credentials.py 2009-10-27 15:24:36 +0000
+++ src/launchpadlib/credentials.py 2009-10-28 20:20:23 +0000
@@ -19,6 +19,7 @@
19__metaclass__ = type19__metaclass__ = type
20__all__ = [20__all__ = [
21 'AccessToken',21 'AccessToken',
22 'AuthorizeRequestTokenProcess',
22 'Consumer',23 'Consumer',
23 'Credentials',24 'Credentials',
24 ]25 ]
@@ -238,3 +239,221 @@
238 elif not 'Almost finished' in content:239 elif not 'Almost finished' in content:
239 response.status = 500 # Internal Server Error240 response.status = 500 # Internal Server Error
240 return response, content241 return response, content
242
243
244class AuthorizeRequestTokenProcess(object):
245
246 UNAUTHORIZED_ACCESS_LEVEL = "UNAUTHORIZED"
247
248 # Suggested messages for clients to display in common situations.
249
250 AUTHENTICATION_FAILURE = "I can't log in with the credentials you gave me. Let's try again."
251
252 CHOOSE_ACCESS_LEVEL = """Now it's time for you to decide how much power to give "%(app)s" over your Launchpad account."""
253
254 CHOOSE_ACCESS_LEVEL_ONE = CHOOSE_ACCESS_LEVEL + """
255
256"%(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."""
257
258 CHOSE_OTHER_THAN_UNAUTHORIZED = """Okay, I'm telling Launchpad to grant "%(app)s" access to your account."""
259
260 CHOSE_UNAUTHORIZED = """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."""
261
262 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."""
263
264 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."""
265
266 INPUT_USERNAME = "What email address do you use on Launchpad?"
267
268 INPUT_PASSWORD = "What's your Launchpad password?"
269
270 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."""
271
272 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."
273
274 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.
275
276I'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."""
277
278 SUCCESS = """You're all done! You should now be able to use Launchpad integration features of "%(app)s." """
279
280 SUCCESS_UNAUTHORIZED = """You're all done! "%(app)s" still doesn't have access to your Launchpad account."""
281
282 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."""
283
284 YOU_NEED_A_LAUNCHPAD_ACCOUNT = """OK, you'll need to get yourself a Launchpad account before you can integrate Launchpad into "%(app)s."
285
286I'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."""
287
288 def __init__(self, web_root, consumer_name, request_token,
289 allow_access_levels=[], max_failed_attempts=3):
290 self.web_root = web_root
291 self.consumer_name = consumer_name
292 self.request_token = request_token
293 self.browser = SimulatedLaunchpadBrowser(self.web_root)
294 self.max_failed_attempts = max_failed_attempts
295 self.allow_access_levels = allow_access_levels
296
297 def __call__(self):
298
299 self.startup(self.message(self.STARTUP_MESSAGE))
300
301 # Have the end-user enter their Launchpad username and password.
302 # Make sure the credentials are valid, and get information
303 # about the request token as a side effect.
304 username, password, token_info = self.get_http_credentials()
305
306 # Update this object with fresh information about the request token.
307 self.token_info = token_info
308 self.allow_access_levels = token_info['access_levels']
309 self._check_consumer()
310
311 # Have the end-user choose an access level from the fresh list.
312 if len(self.allow_access_levels) == 2:
313 # There's only one choice: allow access at a certain level
314 # or don't allow access at all.
315 message = self.CHOOSE_ACCESS_LEVEL_ONE
316 level = [level['title'] for level in self.allow_access_levels
317 if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL]
318 extra = {'level' : level[0]}
319 else:
320 message = self.CHOOSE_ACCESS_LEVEL
321 extra = None
322 access_level = self.input_access_level(
323 self.allow_access_levels, self.message(message, extra))
324
325 # Notify the program of the user's choice.
326 if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
327 self.user_chose_unauthorized(
328 self.message(self.CHOSE_UNAUTHORIZED))
329 else:
330 self.user_chose_other_than_unauthorized(
331 access_level,
332 self.message(self.CHOSE_OTHER_THAN_UNAUTHORIZED))
333
334 # Try to grant the specified level of access to the request token.
335 response, content = self.browser.grant_access(
336 username, password, self.request_token, access_level)
337 if response.status == 409:
338 raise RequestTokenAlreadyAuthorized(
339 self.message(self.REQUEST_TOKEN_ALREADY_AUTHORIZED))
340 elif response.status == 400:
341 raise ClientError(self.message(self.CLIENT_ERROR))
342 elif response.status == 500:
343 raise ServerError(self.message(SERVER_ERROR))
344 if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
345 message = self.SUCCESS_UNAUTHORIZED
346 else:
347 message = self.SUCCESS
348 self.success(self.message(message))
349
350 def get_http_credentials(self, cached_username=None, failed_attempts=0):
351 username = self.input_username(
352 cached_username, self.message(self.INPUT_USERNAME))
353 if username is None:
354 self.open_login_page_in_user_browser(
355 urljoin(self.web_root, "+login"))
356 raise NoLaunchpadAccount(
357 self.message(self.YOU_NEED_A_LAUNCHPAD_ACCOUNT))
358 password = self.input_password(self.message(self.INPUT_PASSWORD))
359 response, content = self.browser.get_token_info(
360 username, password, self.request_token, self.allow_access_levels)
361 if response.status == 500:
362 raise ServerError(self.message(SERVER_ERROR))
363 elif response.status == 401:
364 failed_attempts += 1
365 if failed_attempts == self.max_failed_attempts:
366 raise TooManyAuthenticationFailures(
367 self.message(self.TOO_MANY_AUTHENTICATION_FAILURES))
368 else:
369 self.authentication_failure(
370 self.message(self.AUTHENTICATION_FAILURE))
371 return self.get_http_credentials(username, failed_attempts)
372 token_info = simplejson.loads(content)
373 return username, password, token_info
374
375 def _check_consumer(self):
376 """Sanity-check the server consumer against the client consumer."""
377 real_consumer = self.token_info['oauth_token_consumer']
378 if real_consumer != self.consumer_name:
379 message = self.message(
380 self.CONSUMER_MISMATCH, { 'old_consumer' : self.consumer_name,
381 'real_consumer' : real_consumer })
382 self.server_consumer_differs_from_client_consumer(
383 self.consumer_name, real_consumer, message)
384 self.consumer_name = real_consumer
385
386 def message(self, raw_message, extra_variables=None):
387 """Prepare a message by plugging in the app name."""
388 variables = { 'app' : self.consumer_name }
389 if extra_variables is not None:
390 variables.update(extra_variables)
391 return raw_message % variables
392
393 def open_login_page_in_user_browser(self, url):
394 """Open the Launchpad login page in the user's web browser."""
395 webbrowser.open(url)
396
397 # You should define these methods in your subclass.
398
399 def startup(self, suggested_message):
400 """Hook method called on startup."""
401
402 def input_username(self, cached_username, suggested_message):
403 """Collect the Launchpad username from the end-user.
404
405 :param cached_username: A username from a previous entry attempt,
406 to be presented as the default.
407 """
408 raise NotImplementedError()
409
410 def input_password(self, suggested_message):
411 """Collect the Launchpad password from the end-user."""
412 raise NotImplementedError()
413
414 def input_access_level(self, available_levels, suggested_message):
415 """Collect the desired level of access from the end-user."""
416 raise NotImplementedError()
417
418 def authentication_failure(self, suggested_message):
419 """The user entered invalid credentials."""
420
421 def user_chose_unauthorized(self):
422 """The user refused to authorize a request token."""
423
424 def user_chose_other_than_unauthorized(self, access_level):
425 """The user authorized a request token with some access level."""
426
427 def server_consumer_differs_from_client_consumer(
428 self, client_name, real_name, suggested_message):
429 """The client seems to be lying or mistaken about its name.
430
431 When requesting a request token, the client told Launchpad
432 that its consumer name was "foo". Now the client is telling the
433 end-user that its name is "bar". Something is fishy and at the very
434 least the end-user should be warned about this.
435 """
436 pass
437
438 def success(self, suggested_message):
439 """The token was successfully authorized."""
440 pass
441
442
443class TokenAuthorizationException(Exception):
444 pass
445
446class RequestTokenAlreadyAuthorized(TokenAuthorizationException):
447 pass
448
449class ClientError(TokenAuthorizationException):
450 pass
451
452class ServerError(TokenAuthorizationException):
453 pass
454
455class NoLaunchpadAccount(TokenAuthorizationException):
456 pass
457
458class TooManyAuthenticationFailures(TokenAuthorizationException):
459 pass
241460
=== modified file 'src/launchpadlib/docs/browser.txt'
--- src/launchpadlib/docs/browser.txt 2009-10-27 15:24:36 +0000
+++ src/launchpadlib/docs/browser.txt 2009-10-28 20:20:23 +0000
@@ -51,7 +51,11 @@
51 ... username, password, request_token)51 ... username, password, request_token)
52 >>> print response['content-type']52 >>> print response['content-type']
53 application/json53 application/json
54 >>> response['content-location'] == validate_url54
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'])
55 True59 True
5660
57 >>> import simplejson61 >>> import simplejson
5862
=== added file 'src/launchpadlib/docs/trusted-client.txt'
--- src/launchpadlib/docs/trusted-client.txt 1970-01-01 00:00:00 +0000
+++ src/launchpadlib/docs/trusted-client.txt 2009-10-28 20:20:23 +0000
@@ -0,0 +1,160 @@
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 What email address do you use on Launchpad?
32 What's your Launchpad password?
33 Now it's time for you to decide how much power to give "consumer" ...
34 ['UNAUTHORIZED', 'WRITE_PUBLIC', 'WRITE_PRIVATE']
35 Okay, I'm telling Launchpad to grant "consumer" access to your account.
36 You're all done! You should now be able to use Launchpad ...
37
38Ordinarily, the third-party program will create a request token and
39pass it into the trusted client. The test class is a little unusual:
40it takes care of creating the request token and, after the end-user
41has authorized it, exchanges the request token for an access
42token. This way we can verify that the entire end-to-end process
43works.
44
45 >>> access_token.key is not None
46 True
47
48Denying access
49==============
50
51It's always possible for the end-user to deny access to the
52application. This will make it impossible to convert the request token
53into an access token.
54
55 >>> auth = ScriptableRequestTokenAuthorization(
56 ... "consumer", "salgado@ubuntu.com", "zeca", "UNAUTHORIZED")
57 >>> access_token = auth()
58 What email address do you use on Launchpad?
59 ...
60 Okay, I'm going to cancel the request that "consumer" made...
61 You're all done! "consumer" still doesn't have access...
62 >>> access_token is None
63 True
64
65Only one allowable access level
66===============================
67
68When the application being authenticated only allows one access level,
69the authorizer creates a special message for display to the end-user.
70
71 >>> auth = ScriptableRequestTokenAuthorization(
72 ... "consumer", "salgado@ubuntu.com", "zeca",
73 ... "WRITE_PRIVATE", allow_access_levels=["WRITE_PRIVATE"])
74
75 >>> auth()
76 What email address do you use on Launchpad?
77 ...
78 "consumer" says it needs the following level of access to your Launchpad
79 account: "Change Anything". It can't work with any other level of access,
80 so denying this level of access means prohibiting "consumer" from
81 using your Launchpad account at all.
82 ...
83
84Error handling
85==============
86
87Things can go wrong in many ways, most of which we can test with our
88scriptable authorizer. Here's a utility method to run the
89authorization process with a badly-scripted authorizer and print the
90resulting exception.
91
92 >>> from launchpadlib.credentials import TokenAuthorizationException
93 >>> def print_error(auth):
94 ... try:
95 ... auth()
96 ... except TokenAuthorizationException, e:
97 ... print str(e)
98
99Authentication failures
100-----------------------
101
102If the user doesn't have a Launchpad account, or refuses to type in
103their email address, the authorizer will open their web browser to the
104login page, and raise an exception.
105
106 >>> auth = ScriptableRequestTokenAuthorization(
107 ... "consumer", None, "zeca", "WRITE_PRIVATE")
108 >>> print_error(auth)
109 What email address do you use on Launchpad?
110 [If this were a real application, ... opened to http://launchpad.dev:8085/+login]
111 OK, you'll need to get yourself a Launchpad account before you can ...
112 <BLANKLINE>
113 I'm opening the Launchpad registration page in your web browser ...
114
115If the user enters the wrong username/password combination too many
116times, the authorizer will give up and raise an exception.
117
118 >>> auth = ScriptableRequestTokenAuthorization(
119 ... "consumer", "salgado@ubuntu.com", "baddpassword",
120 ... "WRITE_PRIVATE")
121 >>> print_error(auth)
122 What email address do you use on Launchpad?
123 What's your Launchpad password?
124 I can't log in with the credentials you gave me. Let's try again.
125 What email address do you use on Launchpad?
126 Cached username: salgado@ubuntu.com
127 What's your Launchpad password?
128 You've failed the password entry too many times...
129
130The max_failed_attempts argument controls how many attempts the user
131is given to enter their username and password.
132
133 >>> auth = ScriptableRequestTokenAuthorization(
134 ... "consumer", "bad username", "zeca",
135 ... "WRITE_PRIVATE", max_failed_attempts=1)
136 >>> print_error(auth)
137 What email address do you use on Launchpad?
138 What's your Launchpad password?
139 You've failed the password entry too many times...
140
141Client duplicity
142----------------
143
144If the third-party client gives one consumer name to Launchpad, and a
145different consumer name to the authorizer, the authorizer will detect
146this possible duplicity and print a warning.
147
148 >>> auth = ScriptableRequestTokenAuthorization(
149 ... "consumer1", "salgado@ubuntu.com", "zeca",
150 ... "WRITE_PRIVATE")
151
152We'll simulate this by changing the authorizer's .consumer_name after
153it obtained a request token from Launchpad.
154
155 >>> auth.consumer_name = "consumer2"
156 >>> auth()
157 What email address do you use on Launchpad?
158 ...
159 WARNING: The application you're using told me its name was "consumer2", but it told Launchpad its name was "consumer1"...
160 ...
0161
=== modified file 'src/launchpadlib/testing/helpers.py'
--- src/launchpadlib/testing/helpers.py 2009-10-27 12:00:13 +0000
+++ src/launchpadlib/testing/helpers.py 2009-10-28 20:20:23 +0000
@@ -29,6 +29,8 @@
2929
3030
31from launchpadlib.launchpad import Launchpad31from launchpadlib.launchpad import Launchpad
32from launchpadlib.credentials import (
33 AuthorizeRequestTokenProcess, Credentials)
3234
3335
34class TestableLaunchpad(Launchpad):36class TestableLaunchpad(Launchpad):
@@ -61,3 +63,90 @@
61salgado_with_full_permissions = KnownTokens('salgado-change-anything', 'test')63salgado_with_full_permissions = KnownTokens('salgado-change-anything', 'test')
62salgado_read_nonprivate = KnownTokens('salgado-read-nonprivate', 'secret')64salgado_read_nonprivate = KnownTokens('salgado-read-nonprivate', 'secret')
63nopriv_read_nonprivate = KnownTokens('nopriv-read-nonprivate', 'mystery')65nopriv_read_nonprivate = KnownTokens('nopriv-read-nonprivate', 'mystery')
66
67
68class ScriptableRequestTokenAuthorization(AuthorizeRequestTokenProcess):
69 """A request token process that doesn't need any user input.
70
71 The AuthorizeRequestTokenProcess is supposed to be hooked up to a
72 user interface, but that makes it difficult to test. This subclass
73 is designed to be easy to test.
74 """
75
76 def __init__(self, consumer_name, username, password, choose_access_level,
77 allow_access_levels=[], max_failed_attempts=2,
78 web_root="http://launchpad.dev:8085/"):
79
80 # Get a request token.
81 self.credentials = Credentials(consumer_name)
82 self.credentials.get_request_token(web_root=web_root)
83
84 # Initialize the superclass with the new request token.
85 super(ScriptableRequestTokenAuthorization, self).__init__(
86 web_root, consumer_name, self.credentials._request_token.key,
87 allow_access_levels, max_failed_attempts)
88
89 self.username = username
90 self.password = password
91 self.choose_access_level = choose_access_level
92
93 def __call__(self):
94 super(ScriptableRequestTokenAuthorization, self).__call__()
95
96 # Now verify that it worked by exchanging the authorized
97 # request token for an access token.
98 if self.choose_access_level != self.UNAUTHORIZED_ACCESS_LEVEL:
99 self.credentials.exchange_request_token_for_access_token(
100 web_root=self.web_root)
101 return self.credentials.access_token
102 return None
103
104 def open_login_page_in_user_browser(self, url):
105 """Print a status message."""
106 print ("[If this were a real application, the end-user's web "
107 "browser would be opened to %s]" % url)
108
109 def setup(self, suggested_message):
110 print suggested_message
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 username: " + 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 """Collect the desired level of access from the end-user."""
126 print suggested_message
127 print [level['value'] for level in available_levels]
128 return self.choose_access_level
129
130 def authentication_failure(self, suggested_message):
131 """The user entered invalid credentials."""
132 print suggested_message
133
134 def user_chose_unauthorized(self, suggested_message):
135 """The user refused to authorize a request token."""
136 print suggested_message
137
138 def user_chose_other_than_unauthorized(self, access_level,
139 suggested_message):
140 """The user authorized a request token with some access level."""
141 print suggested_message
142
143 def server_consumer_differs_from_client_consumer(
144 self, client_name, real_name, suggested_message):
145 """The client seems to be lying or mistaken about its name."""
146 print suggested_message
147
148 def success(self, suggested_message):
149 """The token was successfully authorized."""
150 print suggested_message
151
152

Subscribers

People subscribed via source and target branches