Merge lp:~leonardr/launchpadlib/remove-broken-code into lp:launchpadlib
- remove-broken-code
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij (community) | code | Approve | |
Review via email: mp+38935@code.launchpad.net |
Commit message
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*. RequestTokenAut
The SimulatedLaunch
This is really bad. I've seen at least one developer copy SimulatedLaunch
I'm leaving RequestTokenAut
- 98. By Leonard Richardson
-
Removed the no-longer-used DummyAuthorizeR
equestTokenWith Browser class.
Jelmer Vernooij (jelmer) : | # |
Preview Diff
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) |