Merge lp:~leonardr/launchpadlib/ask-for-desktop-integration into lp:launchpadlib

Proposed by Leonard Richardson on 2010-11-01
Status: Merged
Approved by: Aaron Bentley on 2010-11-01
Approved revision: 108
Merged at revision: 100
Proposed branch: lp:~leonardr/launchpadlib/ask-for-desktop-integration
Merge into: lp:launchpadlib
Prerequisite: lp:~benji/launchpadlib/kwallet
Diff against target: 305 lines (+117/-31)
4 files modified
src/launchpadlib/NEWS.txt (+4/-0)
src/launchpadlib/credentials.py (+5/-1)
src/launchpadlib/launchpad.py (+65/-16)
src/launchpadlib/tests/test_launchpad.py (+43/-14)
To merge this branch: bzr merge lp:~leonardr/launchpadlib/ask-for-desktop-integration
Reviewer Review Type Date Requested Status
Aaron Bentley (community) 2010-11-01 Approve on 2010-11-01
Review via email: mp+39772@code.launchpad.net

Description of the Change

This branch changes the default behavior to make launchpadlib ask Launchpad for a desktop-wide integration token instead of an application-specific token. The former "consumer name" is now an "application name" (concept defined in lazr.restfulclient), which shows up in the User-Agent but has no effect on any of the OAuth stuff.

I also removed some code left over from Benji's keyring branch: we were still creating a 'credentials' directory inside .launchpadlib, even though we don't store credentials in that directory anymore.

To post a comment you must log in.
Aaron Bentley (abentley) wrote :

Looks good. I think there's an unnecessary blank line at 287.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/launchpadlib/NEWS.txt'
2--- src/launchpadlib/NEWS.txt 2010-11-01 20:02:03 +0000
3+++ src/launchpadlib/NEWS.txt 2010-11-01 20:02:03 +0000
4@@ -9,6 +9,10 @@
5 available. The credentials_file parameter of Launchpad.login_with() is now
6 ignored.
7
8+- By default, Launchpad.login_with() now asks Launchpad for
9+ desktop-wide integration. This removes the need for each individual
10+ application to get its own OAuth token.
11+
12 1.7.0 (2010-09-23)
13 ==================
14
15
16=== modified file 'src/launchpadlib/credentials.py'
17--- src/launchpadlib/credentials.py 2010-10-26 17:23:35 +0000
18+++ src/launchpadlib/credentials.py 2010-11-01 20:02:03 +0000
19@@ -39,7 +39,11 @@
20
21 from lazr.restfulclient.errors import HTTPError
22 from lazr.restfulclient.authorize.oauth import (
23- AccessToken as _AccessToken, Consumer, OAuthAuthorizer)
24+ AccessToken as _AccessToken,
25+ Consumer,
26+ OAuthAuthorizer,
27+ SystemWideConsumer # Not used directly, just re-imported into here.
28+ )
29
30 from launchpadlib import uris
31
32
33=== modified file 'src/launchpadlib/launchpad.py'
34--- src/launchpadlib/launchpad.py 2010-11-01 20:02:03 +0000
35+++ src/launchpadlib/launchpad.py 2010-11-01 20:02:03 +0000
36@@ -30,10 +30,19 @@
37 import keyring
38 from lazr.uri import URI
39 from lazr.restfulclient.resource import (
40- CollectionWithKeyBasedLookup, HostedFile, ScalarValue, ServiceRoot)
41+ CollectionWithKeyBasedLookup,
42+ HostedFile,
43+ ScalarValue,
44+ ServiceRoot,
45+ )
46 from launchpadlib.credentials import (
47- AccessToken, AnonymousAccessToken, Credentials,
48- AuthorizeRequestTokenWithBrowser)
49+ AccessToken,
50+ AnonymousAccessToken,
51+ AuthorizeRequestTokenWithBrowser,
52+ Consumer,
53+ Credentials,
54+ SystemWideConsumer,
55+ )
56 from launchpadlib import uris
57
58
59@@ -167,8 +176,7 @@
60 name, the access token and the access secret) are available, this
61 method can be used to quickly log into the service root.
62
63- :param consumer_name: the consumer name, as appropriate for the
64- `Consumer` constructor
65+ :param consumer_name: the application name.
66 :type consumer_name: string
67 :param token_string: the access token, as appropriate for the
68 `AccessToken` constructor
69@@ -209,15 +217,22 @@
70 a web browser and ask the user to come back here and tell us when they
71 finished the authorization process.
72
73- :param consumer_name: The consumer name, as appropriate for the
74- `Consumer` constructor
75+ :param consumer_name: Either a consumer name, as appropriate for
76+ the `Consumer` constructor, or a premade Consumer object.
77 :type consumer_name: string
78 :param service_root: The URL to the root of the web service.
79 :type service_root: string
80 :return: The web service root
81 :rtype: `Launchpad`
82 """
83- credentials = Credentials(consumer_name)
84+ if isinstance(consumer_name, Consumer):
85+ # Create the authorizer with no Consumer, then set the Consumer
86+ # object directly.
87+ credentials = Credentials(None)
88+ credentials.consumer = consumer_name
89+ else:
90+ # Have the authorizer create the Consumer itself.
91+ credentials = Credentials(consumer_name_or_consumer)
92 service_root = uris.lookup_service_root(service_root)
93 web_root_uri = URI(service_root)
94 web_root_uri.path = ""
95@@ -250,13 +265,13 @@
96 timeout=timeout, proxy_info=proxy_info, version=version)
97
98 @classmethod
99- def login_with(cls, consumer_name,
100+ def login_with(cls, application_name,
101 service_root=uris.STAGING_SERVICE_ROOT,
102 launchpadlib_dir=None, timeout=None, proxy_info=None,
103 authorizer_class=AuthorizeRequestTokenWithBrowser,
104 allow_access_levels=[], max_failed_attempts=3,
105 credentials_file=None, version=DEFAULT_VERSION,
106- credential_save_failed=None):
107+ consumer_name=None, credential_save_failed=None):
108 """Log in to Launchpad with possibly cached credentials.
109
110 This is a convenience method for either setting up new login
111@@ -273,7 +288,13 @@
112 See `Launchpad.get_token_and_login()` for more information about
113 how new tokens are generated.
114
115- `credential_save_failed` is a callback that is called if saving the
116+ :param application_name: The application name. This is *not*
117+ the OAuth consumer name. Unless a consumer_name is also
118+ provided, the OAuth consumer will be a system-wide
119+ consumer representing the end-user's computer as a whole.
120+ :type application_name: string
121+
122+ :param credential_save_failed: a callback that is called if saving the
123 credentials in the auto-detected back-end (keyring or file) failed.
124
125 :param consumer_name: The consumer name, as appropriate for the
126@@ -285,27 +306,55 @@
127 :param launchpadlib_dir: The directory where the cache and
128 credentials are stored.
129 :type launchpadlib_dir: string
130+ :param allow_access_levels: The acceptable access levels for
131+ this application. This is ignored unless you specify a
132+ consumer_name. All applications using the default
133+ (desktop-wide) consumer name will ask for "desktop integration"
134+ access, which gives read-write access to public and private data.
135+ :type allow_access_levels: list of strings
136 :param credentials_file: ignored, only here for backward compatability
137 :type credentials_file: string
138+
139+ :param consumer_name: The OAuth consumer name. In most cases,
140+ this is not necessary. You should only use this if you
141+ don't want to take advantage of desktop-wide integration.
142+ :type consumer_name: string
143+
144 :return: The web service root
145 :rtype: `Launchpad`
146
147 """
148+ if consumer_name is None:
149+ # System-wide integration. Create a system-wide consumer
150+ # and identify the application using a separate
151+ # application name.
152+ allow_access_levels = ["DESKTOP_INTEGRATION"]
153+ consumer = SystemWideConsumer(application_name)
154+ consumer_name = consumer.key
155+ else:
156+ # Application-specific integration. Use the provided
157+ # consumer name to create a consumer automatically.
158+ consumer = consumer_name
159+
160 (service_root, launchpadlib_dir, cache_path,
161 service_root_dir) = cls._get_paths(service_root, launchpadlib_dir)
162- credentials_path = os.path.join(service_root_dir, 'credentials')
163-
164- if not os.path.exists(credentials_path):
165- os.makedirs(credentials_path, 0700)
166
167 credentials = keyring.get_password(
168 'launchpadlib', consumer_name + '@' + service_root)
169 if credentials is not None:
170 credentials = unserialize_credentials(credentials)
171
172+ if credentials is not None:
173+ # We loaded credentials from a file, but the application
174+ # name wasn't stored in the file, because a single set of
175+ # credentials may be shared by many applications. We need
176+ # to set the application name for this specific instance
177+ # of the credentials.
178+ credentials.consumer.application_name = application_name
179+
180 if credentials is None:
181 launchpad = cls.get_token_and_login(
182- consumer_name, service_root=service_root, cache=cache_path,
183+ consumer, service_root=service_root, cache=cache_path,
184 timeout=timeout, proxy_info=proxy_info,
185 authorizer_class=authorizer_class,
186 allow_access_levels=allow_access_levels,
187
188=== modified file 'src/launchpadlib/tests/test_launchpad.py'
189--- src/launchpadlib/tests/test_launchpad.py 2010-11-01 20:02:03 +0000
190+++ src/launchpadlib/tests/test_launchpad.py 2010-11-01 20:02:03 +0000
191@@ -33,6 +33,7 @@
192 AccessToken,
193 AuthorizeRequestTokenWithBrowser,
194 Credentials,
195+ SystemWideConsumer,
196 )
197 from launchpadlib.launchpad import Launchpad
198 from launchpadlib import uris
199@@ -55,14 +56,16 @@
200 self.passed_in_kwargs = kw
201
202 @classmethod
203- def get_token_and_login(cls, consumer_name, **kw):
204+ def get_token_and_login(cls, consumer, **kw):
205 """Create fake credentials and record that we were called."""
206 credentials = Credentials(
207- consumer_name, consumer_secret='consumer_secret:42',
208- access_token=AccessToken('access_key:84', 'access_secret:168'))
209+ consumer.key, consumer_secret='consumer_secret:42',
210+ access_token=AccessToken('access_key:84', 'access_secret:168'),
211+ application_name=consumer.application_name)
212 launchpad = cls(credentials, **kw)
213+
214 launchpad.get_token_and_login_called = True
215- launchpad.consumer_name = consumer_name
216+ launchpad.consumer = consumer
217 launchpad.passed_in_kwargs = kw
218 return launchpad
219
220@@ -196,12 +199,15 @@
221 # A directory for the passed in service root was created.
222 service_path = os.path.join(launchpadlib_dir, 'api.example.com')
223 self.assertTrue(os.path.isdir(service_path))
224- # Inside the service root directory, there is a 'cache' and a
225- # 'credentials' directory.
226+ # Inside the service root directory, there is a 'cache'
227+ # directory.
228 self.assertTrue(
229 os.path.isdir(os.path.join(service_path, 'cache')))
230+
231+ # In older versions there was also a 'credentials' directory,
232+ # but no longer.
233 credentials_path = os.path.join(service_path, 'credentials')
234- self.assertTrue(os.path.isdir(credentials_path))
235+ self.assertFalse(os.path.isdir(credentials_path))
236
237 def test_dirs_created_are_changed_to_secure(self):
238 launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
239@@ -250,6 +256,29 @@
240 launchpadlib_dir=launchpadlib_dir, version="bar")
241 self.assertEquals(launchpad.passed_in_kwargs['version'], 'bar')
242
243+ def test_application_name_is_propagated(self):
244+ # Create a Launchpad instance for a given application name.
245+ # Credentials are stored, but they don't include the
246+ # application name, since multiple applications may share a
247+ # single system-wide credential.
248+ launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
249+ launchpad = NoNetworkLaunchpad.login_with(
250+ 'very important', service_root='http://api.example.com/',
251+ launchpadlib_dir=launchpadlib_dir)
252+ self.assertEquals(
253+ launchpad.credentials.consumer.application_name, 'very important')
254+
255+ # Now execute the same test a second time. This time, the
256+ # credentials are loaded from disk and a different code path
257+ # is executed. We want to make sure this code path propagates
258+ # the application name, instead of picking an empty one from
259+ # disk.
260+ launchpad = NoNetworkLaunchpad.login_with(
261+ 'very important', service_root='http://api.example.com/',
262+ launchpadlib_dir=launchpadlib_dir)
263+ self.assertEquals(
264+ launchpad.credentials.consumer.application_name, 'very important')
265+
266 def test_no_credentials_calls_get_token_and_login(self):
267 # If no credentials are found, get_token_and_login() is called.
268 service_root = 'http://api.example.com/'
269@@ -258,9 +287,9 @@
270 launchpad = NoNetworkLaunchpad.login_with(
271 'app name', launchpadlib_dir=self.temp_dir,
272 service_root=service_root, timeout=timeout, proxy_info=proxy_info)
273- self.assertEqual(launchpad.consumer_name, 'app name')
274+ self.assertEqual(launchpad.consumer.application_name, 'app name')
275 expected_arguments = dict(
276- allow_access_levels=[],
277+ allow_access_levels=['DESKTOP_INTEGRATION'],
278 authorizer_class=AuthorizeRequestTokenWithBrowser,
279 max_failed_attempts=3,
280 service_root=service_root,
281@@ -331,8 +360,7 @@
282 launchpadlib_dir, os.path.join(self.temp_dir, '.launchpadlib'))
283 self.assertTrue(os.path.exists(
284 os.path.join(launchpadlib_dir, 'api.example.com', 'cache')))
285- self.assertTrue(os.path.exists(
286- os.path.join(launchpadlib_dir, 'api.example.com', 'credentials')))
287+
288
289 def test_short_service_name(self):
290 # A short service name is converted to the full service root URL.
291@@ -435,11 +463,12 @@
292 launchpadlib_dir = os.path.join(self.temp_dir, 'launchpadlib')
293 keyring = InMemoryKeyring()
294 service_root = 'http://api.example.com/'
295- consumer_name = 'Super App 3000'
296+ application_name = 'Super App 3000'
297 with fake_keyring(keyring):
298- NoNetworkLaunchpad.login_with(
299- consumer_name, service_root=service_root,
300+ launchpad = NoNetworkLaunchpad.login_with(
301+ application_name, service_root=service_root,
302 launchpadlib_dir=launchpadlib_dir)
303+ consumer_name = launchpad.credentials.consumer.key
304
305 application_key = keyring.data.keys()[0][1]
306

Subscribers

People subscribed via source and target branches