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
1=== added file 'src/launchpadlib/apps.py'
2--- src/launchpadlib/apps.py 1970-01-01 00:00:00 +0000
3+++ src/launchpadlib/apps.py 2009-10-28 20:20:23 +0000
4@@ -0,0 +1,45 @@
5+# Copyright 2009 Canonical Ltd.
6+
7+# This file is part of launchpadlib.
8+#
9+# launchpadlib is free software: you can redistribute it and/or modify it
10+# under the terms of the GNU Lesser General Public License as published by the
11+# Free Software Foundation, version 3 of the License.
12+#
13+# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
14+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
16+# for more details.
17+#
18+# You should have received a copy of the GNU Lesser General Public License
19+# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
20+
21+"""Command-line applications for Launchpadlib.
22+
23+This module contains the code for various applications. The applications
24+themselves are kept in bin/.
25+"""
26+
27+import simplejson
28+
29+from launchpadlib.credentials import Credentials
30+from launchpadlib.uris import lookup_web_root
31+
32+
33+class RequestTokenApp(object):
34+ """An application that creates request tokens."""
35+
36+ def __init__(self, web_root, consumer_name, context):
37+ """Initialize."""
38+ self.web_root = lookup_web_root(web_root)
39+ self.credentials = Credentials(consumer_name)
40+ self.context = context
41+
42+ def run(self):
43+ """Get a request token and return JSON information about it."""
44+ token = self.credentials.get_request_token(
45+ self.context, self.web_root,
46+ token_format=Credentials.DICT_TOKEN_FORMAT)
47+ return simplejson.dumps(token)
48+
49+
50
51=== modified file 'src/launchpadlib/credentials.py'
52--- src/launchpadlib/credentials.py 2009-10-27 15:24:36 +0000
53+++ src/launchpadlib/credentials.py 2009-10-28 20:20:23 +0000
54@@ -19,6 +19,7 @@
55 __metaclass__ = type
56 __all__ = [
57 'AccessToken',
58+ 'AuthorizeRequestTokenProcess',
59 'Consumer',
60 'Credentials',
61 ]
62@@ -238,3 +239,221 @@
63 elif not 'Almost finished' in content:
64 response.status = 500 # Internal Server Error
65 return response, content
66+
67+
68+class AuthorizeRequestTokenProcess(object):
69+
70+ UNAUTHORIZED_ACCESS_LEVEL = "UNAUTHORIZED"
71+
72+ # Suggested messages for clients to display in common situations.
73+
74+ AUTHENTICATION_FAILURE = "I can't log in with the credentials you gave me. Let's try again."
75+
76+ CHOOSE_ACCESS_LEVEL = """Now it's time for you to decide how much power to give "%(app)s" over your Launchpad account."""
77+
78+ CHOOSE_ACCESS_LEVEL_ONE = CHOOSE_ACCESS_LEVEL + """
79+
80+"%(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."""
81+
82+ CHOSE_OTHER_THAN_UNAUTHORIZED = """Okay, I'm telling Launchpad to grant "%(app)s" access to your account."""
83+
84+ 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."""
85+
86+ 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."""
87+
88+ 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."""
89+
90+ INPUT_USERNAME = "What email address do you use on Launchpad?"
91+
92+ INPUT_PASSWORD = "What's your Launchpad password?"
93+
94+ 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."""
95+
96+ 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."
97+
98+ 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.
99+
100+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."""
101+
102+ SUCCESS = """You're all done! You should now be able to use Launchpad integration features of "%(app)s." """
103+
104+ SUCCESS_UNAUTHORIZED = """You're all done! "%(app)s" still doesn't have access to your Launchpad account."""
105+
106+ 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."""
107+
108+ YOU_NEED_A_LAUNCHPAD_ACCOUNT = """OK, you'll need to get yourself a Launchpad account before you can integrate Launchpad into "%(app)s."
109+
110+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."""
111+
112+ def __init__(self, web_root, consumer_name, request_token,
113+ allow_access_levels=[], max_failed_attempts=3):
114+ self.web_root = web_root
115+ self.consumer_name = consumer_name
116+ self.request_token = request_token
117+ self.browser = SimulatedLaunchpadBrowser(self.web_root)
118+ self.max_failed_attempts = max_failed_attempts
119+ self.allow_access_levels = allow_access_levels
120+
121+ def __call__(self):
122+
123+ self.startup(self.message(self.STARTUP_MESSAGE))
124+
125+ # Have the end-user enter their Launchpad username and password.
126+ # Make sure the credentials are valid, and get information
127+ # about the request token as a side effect.
128+ username, password, token_info = self.get_http_credentials()
129+
130+ # Update this object with fresh information about the request token.
131+ self.token_info = token_info
132+ self.allow_access_levels = token_info['access_levels']
133+ self._check_consumer()
134+
135+ # Have the end-user choose an access level from the fresh list.
136+ if len(self.allow_access_levels) == 2:
137+ # There's only one choice: allow access at a certain level
138+ # or don't allow access at all.
139+ message = self.CHOOSE_ACCESS_LEVEL_ONE
140+ level = [level['title'] for level in self.allow_access_levels
141+ if level['value'] != self.UNAUTHORIZED_ACCESS_LEVEL]
142+ extra = {'level' : level[0]}
143+ else:
144+ message = self.CHOOSE_ACCESS_LEVEL
145+ extra = None
146+ access_level = self.input_access_level(
147+ self.allow_access_levels, self.message(message, extra))
148+
149+ # Notify the program of the user's choice.
150+ if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
151+ self.user_chose_unauthorized(
152+ self.message(self.CHOSE_UNAUTHORIZED))
153+ else:
154+ self.user_chose_other_than_unauthorized(
155+ access_level,
156+ self.message(self.CHOSE_OTHER_THAN_UNAUTHORIZED))
157+
158+ # Try to grant the specified level of access to the request token.
159+ response, content = self.browser.grant_access(
160+ username, password, self.request_token, access_level)
161+ if response.status == 409:
162+ raise RequestTokenAlreadyAuthorized(
163+ self.message(self.REQUEST_TOKEN_ALREADY_AUTHORIZED))
164+ elif response.status == 400:
165+ raise ClientError(self.message(self.CLIENT_ERROR))
166+ elif response.status == 500:
167+ raise ServerError(self.message(SERVER_ERROR))
168+ if access_level == self.UNAUTHORIZED_ACCESS_LEVEL:
169+ message = self.SUCCESS_UNAUTHORIZED
170+ else:
171+ message = self.SUCCESS
172+ self.success(self.message(message))
173+
174+ def get_http_credentials(self, cached_username=None, failed_attempts=0):
175+ username = self.input_username(
176+ cached_username, self.message(self.INPUT_USERNAME))
177+ if username is None:
178+ self.open_login_page_in_user_browser(
179+ urljoin(self.web_root, "+login"))
180+ raise NoLaunchpadAccount(
181+ self.message(self.YOU_NEED_A_LAUNCHPAD_ACCOUNT))
182+ password = self.input_password(self.message(self.INPUT_PASSWORD))
183+ response, content = self.browser.get_token_info(
184+ username, password, self.request_token, self.allow_access_levels)
185+ if response.status == 500:
186+ raise ServerError(self.message(SERVER_ERROR))
187+ elif response.status == 401:
188+ failed_attempts += 1
189+ if failed_attempts == self.max_failed_attempts:
190+ raise TooManyAuthenticationFailures(
191+ self.message(self.TOO_MANY_AUTHENTICATION_FAILURES))
192+ else:
193+ self.authentication_failure(
194+ self.message(self.AUTHENTICATION_FAILURE))
195+ return self.get_http_credentials(username, failed_attempts)
196+ token_info = simplejson.loads(content)
197+ return username, password, token_info
198+
199+ def _check_consumer(self):
200+ """Sanity-check the server consumer against the client consumer."""
201+ real_consumer = self.token_info['oauth_token_consumer']
202+ if real_consumer != self.consumer_name:
203+ message = self.message(
204+ self.CONSUMER_MISMATCH, { 'old_consumer' : self.consumer_name,
205+ 'real_consumer' : real_consumer })
206+ self.server_consumer_differs_from_client_consumer(
207+ self.consumer_name, real_consumer, message)
208+ self.consumer_name = real_consumer
209+
210+ def message(self, raw_message, extra_variables=None):
211+ """Prepare a message by plugging in the app name."""
212+ variables = { 'app' : self.consumer_name }
213+ if extra_variables is not None:
214+ variables.update(extra_variables)
215+ return raw_message % variables
216+
217+ def open_login_page_in_user_browser(self, url):
218+ """Open the Launchpad login page in the user's web browser."""
219+ webbrowser.open(url)
220+
221+ # You should define these methods in your subclass.
222+
223+ def startup(self, suggested_message):
224+ """Hook method called on startup."""
225+
226+ def input_username(self, cached_username, suggested_message):
227+ """Collect the Launchpad username from the end-user.
228+
229+ :param cached_username: A username from a previous entry attempt,
230+ to be presented as the default.
231+ """
232+ raise NotImplementedError()
233+
234+ def input_password(self, suggested_message):
235+ """Collect the Launchpad password from the end-user."""
236+ raise NotImplementedError()
237+
238+ def input_access_level(self, available_levels, suggested_message):
239+ """Collect the desired level of access from the end-user."""
240+ raise NotImplementedError()
241+
242+ def authentication_failure(self, suggested_message):
243+ """The user entered invalid credentials."""
244+
245+ def user_chose_unauthorized(self):
246+ """The user refused to authorize a request token."""
247+
248+ def user_chose_other_than_unauthorized(self, access_level):
249+ """The user authorized a request token with some access level."""
250+
251+ def server_consumer_differs_from_client_consumer(
252+ self, client_name, real_name, suggested_message):
253+ """The client seems to be lying or mistaken about its name.
254+
255+ When requesting a request token, the client told Launchpad
256+ that its consumer name was "foo". Now the client is telling the
257+ end-user that its name is "bar". Something is fishy and at the very
258+ least the end-user should be warned about this.
259+ """
260+ pass
261+
262+ def success(self, suggested_message):
263+ """The token was successfully authorized."""
264+ pass
265+
266+
267+class TokenAuthorizationException(Exception):
268+ pass
269+
270+class RequestTokenAlreadyAuthorized(TokenAuthorizationException):
271+ pass
272+
273+class ClientError(TokenAuthorizationException):
274+ pass
275+
276+class ServerError(TokenAuthorizationException):
277+ pass
278+
279+class NoLaunchpadAccount(TokenAuthorizationException):
280+ pass
281+
282+class TooManyAuthenticationFailures(TokenAuthorizationException):
283+ pass
284
285=== modified file 'src/launchpadlib/docs/browser.txt'
286--- src/launchpadlib/docs/browser.txt 2009-10-27 15:24:36 +0000
287+++ src/launchpadlib/docs/browser.txt 2009-10-28 20:20:23 +0000
288@@ -51,7 +51,11 @@
289 ... username, password, request_token)
290 >>> print response['content-type']
291 application/json
292- >>> response['content-location'] == validate_url
293+
294+ # XXX leonardr 2009-10-28 bug=462773 These two URLs should be
295+ # exactly the same, but the Content-Location is missing the
296+ # lp.context.
297+ >>> validate_url.startswith(response['content-location'])
298 True
299
300 >>> import simplejson
301
302=== added file 'src/launchpadlib/docs/trusted-client.txt'
303--- src/launchpadlib/docs/trusted-client.txt 1970-01-01 00:00:00 +0000
304+++ src/launchpadlib/docs/trusted-client.txt 2009-10-28 20:20:23 +0000
305@@ -0,0 +1,160 @@
306+***********************
307+Making a trusted client
308+***********************
309+
310+To authorize a request token, the end-user must type in their
311+Launchpad username and password. Obviously, typing your password into
312+a random program is a bad idea. The best case is to use a program you
313+already trust with your Launchpad password: your web browser.
314+
315+But if you're writing an application that can't open the end-user's
316+web browser, or you just really want a token authorization client that
317+has the same UI as the rest of your application, you should use one of
318+the trusted clients packaged with launchpadlib, rather than writing
319+your own client.
320+
321+All the trusted clients are based on the same core code and implement
322+the same workflow. This test implements a scriptable trusted client
323+and uses it to test the behavior of the standard workflow.
324+
325+ >>> from launchpadlib.testing.helpers import (
326+ ... ScriptableRequestTokenAuthorization)
327+
328+Here we see the normal workflow, in which the user inputs all the
329+correct data to authorize a request token.
330+
331+ >>> auth = ScriptableRequestTokenAuthorization(
332+ ... "consumer", "salgado@ubuntu.com", "zeca",
333+ ... "WRITE_PRIVATE",
334+ ... allow_access_levels = ["WRITE_PUBLIC", "WRITE_PRIVATE"])
335+ >>> access_token = auth()
336+ What email address do you use on Launchpad?
337+ What's your Launchpad password?
338+ Now it's time for you to decide how much power to give "consumer" ...
339+ ['UNAUTHORIZED', 'WRITE_PUBLIC', 'WRITE_PRIVATE']
340+ Okay, I'm telling Launchpad to grant "consumer" access to your account.
341+ You're all done! You should now be able to use Launchpad ...
342+
343+Ordinarily, the third-party program will create a request token and
344+pass it into the trusted client. The test class is a little unusual:
345+it takes care of creating the request token and, after the end-user
346+has authorized it, exchanges the request token for an access
347+token. This way we can verify that the entire end-to-end process
348+works.
349+
350+ >>> access_token.key is not None
351+ True
352+
353+Denying access
354+==============
355+
356+It's always possible for the end-user to deny access to the
357+application. This will make it impossible to convert the request token
358+into an access token.
359+
360+ >>> auth = ScriptableRequestTokenAuthorization(
361+ ... "consumer", "salgado@ubuntu.com", "zeca", "UNAUTHORIZED")
362+ >>> access_token = auth()
363+ What email address do you use on Launchpad?
364+ ...
365+ Okay, I'm going to cancel the request that "consumer" made...
366+ You're all done! "consumer" still doesn't have access...
367+ >>> access_token is None
368+ True
369+
370+Only one allowable access level
371+===============================
372+
373+When the application being authenticated only allows one access level,
374+the authorizer creates a special message for display to the end-user.
375+
376+ >>> auth = ScriptableRequestTokenAuthorization(
377+ ... "consumer", "salgado@ubuntu.com", "zeca",
378+ ... "WRITE_PRIVATE", allow_access_levels=["WRITE_PRIVATE"])
379+
380+ >>> auth()
381+ What email address do you use on Launchpad?
382+ ...
383+ "consumer" says it needs the following level of access to your Launchpad
384+ account: "Change Anything". It can't work with any other level of access,
385+ so denying this level of access means prohibiting "consumer" from
386+ using your Launchpad account at all.
387+ ...
388+
389+Error handling
390+==============
391+
392+Things can go wrong in many ways, most of which we can test with our
393+scriptable authorizer. Here's a utility method to run the
394+authorization process with a badly-scripted authorizer and print the
395+resulting exception.
396+
397+ >>> from launchpadlib.credentials import TokenAuthorizationException
398+ >>> def print_error(auth):
399+ ... try:
400+ ... auth()
401+ ... except TokenAuthorizationException, e:
402+ ... print str(e)
403+
404+Authentication failures
405+-----------------------
406+
407+If the user doesn't have a Launchpad account, or refuses to type in
408+their email address, the authorizer will open their web browser to the
409+login page, and raise an exception.
410+
411+ >>> auth = ScriptableRequestTokenAuthorization(
412+ ... "consumer", None, "zeca", "WRITE_PRIVATE")
413+ >>> print_error(auth)
414+ What email address do you use on Launchpad?
415+ [If this were a real application, ... opened to http://launchpad.dev:8085/+login]
416+ OK, you'll need to get yourself a Launchpad account before you can ...
417+ <BLANKLINE>
418+ I'm opening the Launchpad registration page in your web browser ...
419+
420+If the user enters the wrong username/password combination too many
421+times, the authorizer will give up and raise an exception.
422+
423+ >>> auth = ScriptableRequestTokenAuthorization(
424+ ... "consumer", "salgado@ubuntu.com", "baddpassword",
425+ ... "WRITE_PRIVATE")
426+ >>> print_error(auth)
427+ What email address do you use on Launchpad?
428+ What's your Launchpad password?
429+ I can't log in with the credentials you gave me. Let's try again.
430+ What email address do you use on Launchpad?
431+ Cached username: salgado@ubuntu.com
432+ What's your Launchpad password?
433+ You've failed the password entry too many times...
434+
435+The max_failed_attempts argument controls how many attempts the user
436+is given to enter their username and password.
437+
438+ >>> auth = ScriptableRequestTokenAuthorization(
439+ ... "consumer", "bad username", "zeca",
440+ ... "WRITE_PRIVATE", max_failed_attempts=1)
441+ >>> print_error(auth)
442+ What email address do you use on Launchpad?
443+ What's your Launchpad password?
444+ You've failed the password entry too many times...
445+
446+Client duplicity
447+----------------
448+
449+If the third-party client gives one consumer name to Launchpad, and a
450+different consumer name to the authorizer, the authorizer will detect
451+this possible duplicity and print a warning.
452+
453+ >>> auth = ScriptableRequestTokenAuthorization(
454+ ... "consumer1", "salgado@ubuntu.com", "zeca",
455+ ... "WRITE_PRIVATE")
456+
457+We'll simulate this by changing the authorizer's .consumer_name after
458+it obtained a request token from Launchpad.
459+
460+ >>> auth.consumer_name = "consumer2"
461+ >>> auth()
462+ What email address do you use on Launchpad?
463+ ...
464+ WARNING: The application you're using told me its name was "consumer2", but it told Launchpad its name was "consumer1"...
465+ ...
466
467=== modified file 'src/launchpadlib/testing/helpers.py'
468--- src/launchpadlib/testing/helpers.py 2009-10-27 12:00:13 +0000
469+++ src/launchpadlib/testing/helpers.py 2009-10-28 20:20:23 +0000
470@@ -29,6 +29,8 @@
471
472
473 from launchpadlib.launchpad import Launchpad
474+from launchpadlib.credentials import (
475+ AuthorizeRequestTokenProcess, Credentials)
476
477
478 class TestableLaunchpad(Launchpad):
479@@ -61,3 +63,90 @@
480 salgado_with_full_permissions = KnownTokens('salgado-change-anything', 'test')
481 salgado_read_nonprivate = KnownTokens('salgado-read-nonprivate', 'secret')
482 nopriv_read_nonprivate = KnownTokens('nopriv-read-nonprivate', 'mystery')
483+
484+
485+class ScriptableRequestTokenAuthorization(AuthorizeRequestTokenProcess):
486+ """A request token process that doesn't need any user input.
487+
488+ The AuthorizeRequestTokenProcess is supposed to be hooked up to a
489+ user interface, but that makes it difficult to test. This subclass
490+ is designed to be easy to test.
491+ """
492+
493+ def __init__(self, consumer_name, username, password, choose_access_level,
494+ allow_access_levels=[], max_failed_attempts=2,
495+ web_root="http://launchpad.dev:8085/"):
496+
497+ # Get a request token.
498+ self.credentials = Credentials(consumer_name)
499+ self.credentials.get_request_token(web_root=web_root)
500+
501+ # Initialize the superclass with the new request token.
502+ super(ScriptableRequestTokenAuthorization, self).__init__(
503+ web_root, consumer_name, self.credentials._request_token.key,
504+ allow_access_levels, max_failed_attempts)
505+
506+ self.username = username
507+ self.password = password
508+ self.choose_access_level = choose_access_level
509+
510+ def __call__(self):
511+ super(ScriptableRequestTokenAuthorization, self).__call__()
512+
513+ # Now verify that it worked by exchanging the authorized
514+ # request token for an access token.
515+ if self.choose_access_level != self.UNAUTHORIZED_ACCESS_LEVEL:
516+ self.credentials.exchange_request_token_for_access_token(
517+ web_root=self.web_root)
518+ return self.credentials.access_token
519+ return None
520+
521+ def open_login_page_in_user_browser(self, url):
522+ """Print a status message."""
523+ print ("[If this were a real application, the end-user's web "
524+ "browser would be opened to %s]" % url)
525+
526+ def setup(self, suggested_message):
527+ print suggested_message
528+
529+ def input_username(self, cached_username, suggested_message):
530+ """Collect the Launchpad username from the end-user."""
531+ print suggested_message
532+ if cached_username is not None:
533+ print "Cached username: " + cached_username
534+ return self.username
535+
536+ def input_password(self, suggested_message):
537+ """Collect the Launchpad password from the end-user."""
538+ print suggested_message
539+ return self.password
540+
541+ def input_access_level(self, available_levels, suggested_message):
542+ """Collect the desired level of access from the end-user."""
543+ print suggested_message
544+ print [level['value'] for level in available_levels]
545+ return self.choose_access_level
546+
547+ def authentication_failure(self, suggested_message):
548+ """The user entered invalid credentials."""
549+ print suggested_message
550+
551+ def user_chose_unauthorized(self, suggested_message):
552+ """The user refused to authorize a request token."""
553+ print suggested_message
554+
555+ def user_chose_other_than_unauthorized(self, access_level,
556+ suggested_message):
557+ """The user authorized a request token with some access level."""
558+ print suggested_message
559+
560+ def server_consumer_differs_from_client_consumer(
561+ self, client_name, real_name, suggested_message):
562+ """The client seems to be lying or mistaken about its name."""
563+ print suggested_message
564+
565+ def success(self, suggested_message):
566+ """The token was successfully authorized."""
567+ print suggested_message
568+
569+

Subscribers

People subscribed via source and target branches