Merge lp:~alecu/ubuntu-sso-client/keyring-api-change into lp:ubuntu-sso-client

Proposed by Alejandro J. Cura
Status: Merged
Approved by: Rodrigo Moya
Approved revision: 593
Merged at revision: 599
Proposed branch: lp:~alecu/ubuntu-sso-client/keyring-api-change
Merge into: lp:ubuntu-sso-client
Diff against target: 526 lines (+219/-152)
4 files modified
ubuntu_sso/keyring.py (+89/-38)
ubuntu_sso/main.py (+1/-28)
ubuntu_sso/tests/test_keyring.py (+125/-29)
ubuntu_sso/tests/test_main.py (+4/-57)
To merge this branch: bzr merge lp:~alecu/ubuntu-sso-client/keyring-api-change
Reviewer Review Type Date Requested Status
Rodrigo Moya (community) Approve
John Lenton (community) Approve
Review via email: mp+33840@code.launchpad.net

Commit message

Use the keyring unlocking gnomekeyring APIs (LP: #623622)
Search all keyrings for the credentials (LP: #624033)

Description of the change

Use the keyring unlocking gnomekeyring apis and search all keyrings for the credentials

To post a comment you must log in.
Revision history for this message
John Lenton (chipaca) wrote :

tests are delicious!

review: Approve
Revision history for this message
Rodrigo Moya (rodrigo-moya) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntu_sso/keyring.py'
2--- ubuntu_sso/keyring.py 2010-08-19 13:39:50 +0000
3+++ ubuntu_sso/keyring.py 2010-08-26 21:18:42 +0000
4@@ -19,77 +19,128 @@
5
6 """Handle keys in the gnome kerying."""
7
8+import socket
9+import urllib
10+import urlparse
11+
12 import gnomekeyring
13
14-from urllib import urlencode
15-from urlparse import parse_qsl
16+from ubuntu_sso.logger import setupLogging
17+logger = setupLogging("ubuntu_sso.main")
18+
19+U1_APP_NAME = "Ubuntu One"
20+U1_KEY_NAME = "UbuntuOne token for https://ubuntuone.com"
21+U1_KEY_ATTR = {
22+ "oauth-consumer-key": "ubuntuone",
23+ "ubuntuone-realm": "https://ubuntuone.com",
24+}
25+
26+
27+def get_token_name(app_name):
28+ """Build the token name."""
29+ quoted_app_name = urllib.quote(app_name)
30+ computer_name = socket.gethostname()
31+ quoted_computer_name = urllib.quote(computer_name)
32+ return "%s - %s" % (quoted_app_name, quoted_computer_name)
33
34
35 class Keyring(object):
36-
37+ """A Keyring for a given application name."""
38 KEYRING_NAME = "login"
39
40 def __init__(self, app_name):
41+ """Initialize this instance given the app_name."""
42 if not gnomekeyring.is_available():
43 raise gnomekeyring.NoKeyringDaemonError
44 self.app_name = app_name
45+ self.token_name = get_token_name(self.app_name)
46
47 def _create_keyring(self, name):
48- """Creates a keyring, if it already exists, do nothing."""
49+ """Creates a keyring, or if it already exists, it does nothing."""
50 keyring_names = gnomekeyring.list_keyring_names_sync()
51 if not name in keyring_names:
52 gnomekeyring.create_sync(name)
53
54- def _get_item_id_from_name(self, sync, name):
55- """Return the ID for a named item."""
56- for item_id in gnomekeyring.list_item_ids_sync(sync):
57- item_info = gnomekeyring.item_get_info_sync(sync, item_id)
58- display_name = item_info.get_display_name()
59- if display_name == name:
60- return item_id
61-
62- def _get_item_info_from_name(self, sync, name):
63- """Return the ID for a named item."""
64- for item_id in gnomekeyring.list_item_ids_sync(sync):
65- item_info = gnomekeyring.item_get_info_sync(sync, item_id)
66- display_name = item_info.get_display_name()
67- if display_name == name:
68- return item_info
69+ def _find_keyring_item(self):
70+ """Return the keyring item or None if not found."""
71+ try:
72+ items = gnomekeyring.find_items_sync(
73+ gnomekeyring.ITEM_GENERIC_SECRET,
74+ self._get_keyring_attr())
75+ except gnomekeyring.NoMatchError:
76+ # if no items found, return None
77+ return None
78+
79+ # we priorize the item in the "login" keyring
80+ for item in items:
81+ if item.keyring == "login":
82+ return item
83+
84+ # if not on the "login" keyring, we return the first item
85+ return items[0]
86+
87+ def _get_keyring_attr(self):
88+ """Build the keyring attributes for this credentials."""
89+ attr = {"key-type": "Ubuntu SSO credentials",
90+ "token-name": self.token_name}
91+ return attr
92
93 def set_ubuntusso_attr(self, cred):
94 """Set the credentials of the Ubuntu SSO item."""
95 # Creates the secret from the credentials
96- secret = urlencode(cred)
97+ secret = urllib.urlencode(cred)
98
99- # Create the keyring
100+ # Create the keyring if it does not exists
101 self._create_keyring(self.KEYRING_NAME)
102
103- # No need to delete the item if it already exists
104-
105- # A set of attributes for this credentials
106- attr = {"key-type": "Ubuntu SSO credentials",
107- "token-name": cred["name"].encode("utf8")}
108-
109 # Add our SSO credentials to the keyring
110 gnomekeyring.item_create_sync(self.KEYRING_NAME,
111- gnomekeyring.ITEM_GENERIC_SECRET, self.app_name, attr,
112- secret, True)
113+ gnomekeyring.ITEM_GENERIC_SECRET, self.app_name,
114+ self._get_keyring_attr(), secret, True)
115
116 def get_ubuntusso_attr(self):
117 """Return the secret of the SSO item in a dictionary."""
118 # If we have no attributes, return None
119- exist = self._get_item_info_from_name(self.KEYRING_NAME, self.app_name)
120- if exist is not None:
121- secret = self._get_item_info_from_name(self.KEYRING_NAME,
122- self.app_name).get_secret()
123- return dict(parse_qsl(secret))
124+ item = self._find_keyring_item()
125+ if item is not None:
126+ return dict(urlparse.parse_qsl(item.secret))
127+ else:
128+ # if no item found, try getting the old credentials
129+ if self.app_name == U1_APP_NAME:
130+ return try_old_credentials(self.app_name)
131+ # nothing was found
132+ return None
133
134 def delete_ubuntusso_attr(self):
135 """Delete a set of credentials from the keyring."""
136- item_id = self._get_item_id_from_name(self.KEYRING_NAME,
137- self.app_name)
138- if item_id is not None:
139- gnomekeyring.item_delete_sync(self.KEYRING_NAME, item_id)
140+ item = self._find_keyring_item()
141+ if item is not None:
142+ gnomekeyring.item_delete_sync(item.keyring, item.item_id)
143+
144+
145+class UbuntuOneOAuthKeyring(Keyring):
146+
147+ def _get_keyring_attr(self):
148+ """Build the keyring attributes for this credentials."""
149+ return U1_KEY_ATTR
150+
151+
152+def try_old_credentials(app_name):
153+ """Try to get old U1 credentials and format them as new."""
154+ logger.debug('trying to get old credentials.')
155+ old_creds = UbuntuOneOAuthKeyring(U1_KEY_NAME).get_ubuntusso_attr()
156+ if old_creds is not None:
157+ # Old creds found, build a new credentials dict with them
158+ creds = {
159+ 'consumer_key': "ubuntuone",
160+ 'consumer_secret': "hammertime",
161+ 'name': U1_KEY_NAME,
162+ 'token': old_creds["oauth_token"],
163+ 'token_secret': old_creds["oauth_token_secret"],
164+ }
165+ logger.debug('found old credentials')
166+ return creds
167+ logger.debug('try_old_credentials: No old credentials for this app.')
168
169
170 if __name__ == "__main__":
171
172=== modified file 'ubuntu_sso/main.py'
173--- ubuntu_sso/main.py 2010-08-26 13:43:31 +0000
174+++ ubuntu_sso/main.py 2010-08-26 21:18:42 +0000
175@@ -28,11 +28,9 @@
176 """
177
178 import re
179-import socket
180 import time
181 import threading
182 import traceback
183-import urllib
184 import urllib2
185 import urlparse
186
187@@ -49,7 +47,7 @@
188 from ubuntu_sso import (DBUS_IFACE_AUTH_NAME, DBUS_IFACE_USER_NAME,
189 DBUS_IFACE_CRED_NAME, DBUS_CRED_PATH, DBUS_BUS_NAME, gui)
190 from ubuntu_sso.config import get_config
191-from ubuntu_sso.keyring import Keyring
192+from ubuntu_sso.keyring import Keyring, get_token_name, U1_APP_NAME
193 from ubuntu_sso.logger import setupLogging
194 logger = setupLogging("ubuntu_sso.main")
195
196@@ -57,8 +55,6 @@
197 # Disable the invalid name warning, as we have a lot of DBus style names
198 # pylint: disable-msg=C0103
199
200-OLD_KEY_NAME = "UbuntuOne token for https://ubuntuone.com"
201-U1_APP_NAME = "Ubuntu One"
202 PING_URL = "http://edge.one.ubuntu.com/oauth/sso-finished-so-get-tokens/"
203
204
205@@ -113,32 +109,9 @@
206 creds = Keyring(app_name).get_ubuntusso_attr()
207 logger.debug('keyring_get_credentials: Keyring returned credentials? %r',
208 creds is not None)
209- if creds is None and app_name == U1_APP_NAME:
210- logger.debug('keyring_get_credentials: trying for old service.')
211- # No new creds, try to get old credentials
212- old_creds = Keyring(OLD_KEY_NAME).get_ubuntusso_attr()
213- if old_creds is not None:
214- # Old creds found, build a new credentials dict with them
215- creds = {
216- 'consumer_key': "ubuntuone",
217- 'consumer_secret': "hammertime",
218- 'token_name': OLD_KEY_NAME,
219- 'token': old_creds["oauth_token"],
220- 'token_secret': old_creds["oauth_token_secret"],
221- }
222- else:
223- logger.debug('keyring_get_credentials: Keyring returned credentials.')
224 return creds
225
226
227-def get_token_name(app_name):
228- """Build the token name."""
229- quoted_app_name = urllib.quote(app_name)
230- computer_name = socket.gethostname()
231- quoted_computer_name = urllib.quote(computer_name)
232- return "%s - %s" % (quoted_app_name, quoted_computer_name)
233-
234-
235 class SSOLoginProcessor(object):
236 """Login and register users using the Ubuntu Single Sign On service."""
237
238
239=== modified file 'ubuntu_sso/tests/test_keyring.py'
240--- ubuntu_sso/tests/test_keyring.py 2010-08-19 05:50:41 +0000
241+++ ubuntu_sso/tests/test_keyring.py 2010-08-26 21:18:42 +0000
242@@ -17,24 +17,39 @@
243 # with this program. If not, see <http://www.gnu.org/licenses/>.
244 """Tests for the keyring.py module."""
245
246+import itertools
247+import socket
248+import urllib
249+
250 import gnomekeyring
251 from twisted.trial.unittest import TestCase
252
253 from ubuntu_sso import keyring
254
255
256-class MockInfoSync(object):
257- """A mock object that represents a fake key."""
258-
259- def __init__(self, (key, key_name, secret)):
260+def build_fake_gethostname(fake_hostname):
261+ """Return a fake hostname getter."""
262+ return lambda *a: fake_hostname
263+
264+
265+class MockKeyringItem(object):
266+ """A mock object that fakes an item found in a keyring search."""
267+
268+ def __init__(self, item_id, keyring, attributes, secret):
269 """Initialize this instance."""
270- self.key = key
271- self.key_name = key_name
272+ self.item_id = item_id
273+ self.keyring = keyring
274+ self.attributes = attributes
275 self.secret = secret
276
277- def get_display_name(self):
278- """Return info on the mocked object."""
279- return self.key_name
280+ def matches(self, search_attr):
281+ """See if this item matches a given search."""
282+ for k, v in search_attr.items():
283+ if k not in self.attributes:
284+ return False
285+ if self.attributes[k] != v:
286+ return False
287+ return True
288
289
290 class MockGnomeKeyring(object):
291@@ -43,28 +58,33 @@
292 def __init__(self):
293 """Initialize this instance."""
294 self.keyrings = set()
295- self.store = []
296+ self.id_counter = itertools.count()
297+ self.store = {}
298 self.deleted = []
299
300+ def _get_next_id(self):
301+ """Return the next keyring id."""
302+ return self.id_counter.next()
303+
304 def item_create_sync(self, keyring_name, item_type, key_name, attr,
305 secret, update_if_exists):
306- """Sets a value in the keyring."""
307- # we'll use the attr as the dict key, so make it a tuple
308- key = tuple(attr.items())
309- self.store.append((key, key_name, secret))
310+ """Add a key to a keyring."""
311+ new_id = self._get_next_id()
312+ i = MockKeyringItem(new_id, keyring_name, attr, secret)
313+ self.store[new_id] = i
314
315 def item_delete_sync(self, keyring_name, item_id):
316- """Makes a list of deleted items."""
317- key, key_name, secret = self.store.pop(item_id)
318- self.deleted.append(key_name)
319-
320- def list_item_ids_sync(self, keyring_name):
321- """Return a list of ids for all the items."""
322- return [n for n, v in enumerate(self.store)]
323-
324- def item_get_info_sync(self, keyring_name, item_id):
325- """Return an info sync object for the item."""
326- return MockInfoSync(self.store[item_id])
327+ """Delete a key from a keyring, and keep a list of deleted keys."""
328+ item = self.store.pop(item_id)
329+ assert keyring_name == item.keyring
330+ self.deleted.append(item)
331+
332+ def find_items_sync(self, item_type, attr):
333+ """Find all keys that match the given attributes."""
334+ items = [i for i in self.store.values() if i.matches(attr)]
335+ if len(items) == 0:
336+ raise gnomekeyring.NoMatchError()
337+ return items
338
339 def list_keyring_names_sync(self):
340 """The keyring you are looking for may be here."""
341@@ -79,6 +99,32 @@
342 return True
343
344
345+class TestTokenNameBuilder(TestCase):
346+ """Test the method that builds the token name."""
347+
348+ def test_get_simple_token_name(self):
349+ """A simple token name is built right."""
350+ sample_app_name = "UbuntuTwo"
351+ sample_hostname = "Darkstar"
352+ expected_result = "UbuntuTwo - Darkstar"
353+
354+ fake_gethostname = build_fake_gethostname(sample_hostname)
355+ self.patch(socket, "gethostname", fake_gethostname)
356+ result = keyring.get_token_name(sample_app_name)
357+ self.assertEqual(result, expected_result)
358+
359+ def test_get_complex_token_name(self):
360+ """A complex token name is built right too."""
361+ sample_app_name = "Ubuntu Eleven"
362+ sample_hostname = "Mate+Cocido"
363+ expected_result = "Ubuntu%20Eleven - Mate%2BCocido"
364+
365+ fake_gethostname = build_fake_gethostname(sample_hostname)
366+ self.patch(socket, "gethostname", fake_gethostname)
367+ result = keyring.get_token_name(sample_app_name)
368+ self.assertEqual(result, expected_result)
369+
370+
371 class TestKeyring(TestCase):
372 """Test the gnome keyring related functions."""
373 def setUp(self):
374@@ -89,11 +135,10 @@
375 self.patch(gnomekeyring, "create_sync", self.mgk.create_sync)
376 self.patch(gnomekeyring, "list_keyring_names_sync",
377 self.mgk.list_keyring_names_sync)
378- self.patch(gnomekeyring, "list_item_ids_sync",
379- self.mgk.list_item_ids_sync)
380- self.patch(gnomekeyring, "item_get_info_sync",
381- self.mgk.item_get_info_sync)
382+ self.patch(gnomekeyring, "find_items_sync", self.mgk.find_items_sync)
383 self.patch(gnomekeyring, "item_delete_sync", self.mgk.item_delete_sync)
384+ fake_gethostname = build_fake_gethostname("darkstar")
385+ self.patch(socket, "gethostname", fake_gethostname)
386
387 def test_set_ubuntusso(self):
388 """Test that the set method does not erase previous keys."""
389@@ -113,3 +158,54 @@
390
391 self.assertEqual(len(self.mgk.store), 0)
392 self.assertEqual(len(self.mgk.deleted), 1)
393+
394+ def test_get_credentials(self):
395+ """Test that credentials are properly retrieved."""
396+ sample_creds = {"name": "sample creds name"}
397+ keyring.Keyring("appname").set_ubuntusso_attr(sample_creds)
398+
399+ result = keyring.Keyring("appname").get_ubuntusso_attr()
400+ self.assertEqual(result, sample_creds)
401+
402+ def test_get_old_cred_found(self):
403+ """The method returns a new set of creds if old creds are found."""
404+ sample_oauth_token = "sample oauth token"
405+ sample_oauth_secret = "sample oauth secret"
406+ old_creds = {
407+ "oauth_token": sample_oauth_token,
408+ "oauth_token_secret": sample_oauth_secret,
409+ }
410+ secret = urllib.urlencode(old_creds)
411+ self.mgk.item_create_sync(keyring.U1_APP_NAME, None,
412+ keyring.Keyring.KEYRING_NAME,
413+ keyring.U1_KEY_ATTR,
414+ secret, True)
415+
416+ result = keyring.Keyring(keyring.U1_APP_NAME).get_ubuntusso_attr()
417+
418+ self.assertIn("token", result)
419+ self.assertEqual(result["token"], sample_oauth_token)
420+ self.assertIn("token_secret", result)
421+ self.assertEqual(result["token_secret"], sample_oauth_secret)
422+
423+ def test_get_old_cred_found_but_not_asked_for(self):
424+ """Returns None if old creds are present but the appname is not U1"""
425+ sample_oauth_token = "sample oauth token"
426+ sample_oauth_secret = "sample oauth secret"
427+ old_creds = {
428+ "oauth_token": sample_oauth_token,
429+ "oauth_token_secret": sample_oauth_secret,
430+ }
431+ secret = urllib.urlencode(old_creds)
432+ self.mgk.item_create_sync(keyring.U1_APP_NAME, None,
433+ keyring.Keyring.KEYRING_NAME,
434+ keyring.U1_KEY_ATTR,
435+ secret, True)
436+
437+ result = keyring.Keyring("Software Center").get_ubuntusso_attr()
438+ self.assertEqual(result, None)
439+
440+ def test_get_old_cred_not_found(self):
441+ """The method returns None if no old nor new credentials found."""
442+ result = keyring.Keyring(keyring.U1_APP_NAME).get_ubuntusso_attr()
443+ self.assertEqual(result, None)
444
445=== modified file 'ubuntu_sso/tests/test_main.py'
446--- ubuntu_sso/tests/test_main.py 2010-08-26 13:43:31 +0000
447+++ ubuntu_sso/tests/test_main.py 2010-08-26 21:18:42 +0000
448@@ -35,15 +35,14 @@
449
450 from contrib.testing.testcase import MementoHandler
451 from ubuntu_sso import config, gui
452+from ubuntu_sso.keyring import get_token_name, U1_APP_NAME
453 from ubuntu_sso.main import (
454 AuthenticationError, BadRealmError, blocking, EmailTokenError,
455- except_to_errdict, get_token_name,
456- InvalidEmailError, InvalidPasswordError,
457+ except_to_errdict, InvalidEmailError, InvalidPasswordError,
458 keyring_get_credentials, keyring_store_credentials, logger,
459- LoginProcessor, NewPasswordError, OLD_KEY_NAME, PING_URL,
460+ LoginProcessor, NewPasswordError, PING_URL,
461 RegistrationError, ResetPasswordTokenError,
462- SSOCredentials, SSOLogin, SSOLoginProcessor,
463- U1_APP_NAME)
464+ SSOCredentials, SSOLogin, SSOLoginProcessor)
465
466
467 APP_NAME = 'The Coolest App Ever'
468@@ -842,58 +841,6 @@
469 token = keyring_get_credentials(APP_NAME)
470 self.assertEqual(token, None)
471
472- def test_keyring_get_old_cred_found(self):
473- """The method returns a new set of creds if old creds are found."""
474- sample_oauth_token = "sample oauth token"
475- sample_oauth_secret = "sample oauth secret"
476- old_creds = {
477- "oauth_token": sample_oauth_token,
478- "oauth_token_secret": sample_oauth_secret,
479- }
480-
481- mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")
482- mockKeyringClass(U1_APP_NAME)
483- mockKeyring = self.mocker.mock()
484- self.mocker.result(mockKeyring)
485- mockKeyring.get_ubuntusso_attr()
486- self.mocker.result(None)
487-
488- mockKeyringClass(OLD_KEY_NAME)
489- mockKeyring2 = self.mocker.mock()
490- self.mocker.result(mockKeyring2)
491- mockKeyring2.get_ubuntusso_attr()
492- self.mocker.result(old_creds)
493-
494- self.mocker.replay()
495-
496- new_creds = keyring_get_credentials(U1_APP_NAME)
497- self.assertIn("token", new_creds)
498- self.assertEqual(new_creds["token"], sample_oauth_token)
499- self.assertIn("token_secret", new_creds)
500- self.assertEqual(new_creds["token_secret"], sample_oauth_secret)
501- self.assertIn("token_name", new_creds)
502- self.assertEqual(new_creds["token_name"], OLD_KEY_NAME)
503-
504- def test_keyring_get_old_cred_not_found(self):
505- """The method returns None if no old nor new credentials found."""
506- mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")
507- mockKeyringClass(U1_APP_NAME)
508- mockKeyring = self.mocker.mock()
509- self.mocker.result(mockKeyring)
510- mockKeyring.get_ubuntusso_attr()
511- self.mocker.result(None)
512-
513- mockKeyringClass(OLD_KEY_NAME)
514- mockKeyring2 = self.mocker.mock()
515- self.mocker.result(mockKeyring2)
516- mockKeyring2.get_ubuntusso_attr()
517- self.mocker.result(None)
518-
519- self.mocker.replay()
520-
521- token = keyring_get_credentials(U1_APP_NAME)
522- self.assertEqual(token, None)
523-
524
525 class RegisterSampleException(Exception):
526 """A mock exception thrown just when testing."""

Subscribers

People subscribed via source and target branches