Merge lp:~mikemc/ubiquity/replace-sso-cli into lp:~mikemc/ubiquity/add-online-debug

Proposed by Mike McCracken
Status: Merged
Merge reported by: Mike McCracken
Merged at revision: not available
Proposed branch: lp:~mikemc/ubiquity/replace-sso-cli
Merge into: lp:~mikemc/ubiquity/add-online-debug
Prerequisite: lp:~mikemc/ubiquity/tolerate-weird-emails
Diff against target: 828 lines (+488/-202)
3 files modified
plugin-viewer-gtk.py (+3/-0)
tests/test_ubi_ubuntuone.py (+277/-59)
ubiquity/plugins/ubi-ubuntuone.py (+208/-143)
To merge this branch: bzr merge lp:~mikemc/ubiquity/replace-sso-cli
Reviewer Review Type Date Requested Status
dobey (community) Approve
Alejandro J. Cura (community) Approve
Review via email: mp+150088@code.launchpad.net

Commit message

- Replace separate SSO v1 helper with direct libsoup calls to access the v2 API.

Description of the change

- Replace separate SSO v1 helper with direct libsoup calls to access the v2 API.

Can be tested semi-IRL like this, from the top level:
UBUNTU_SSO_URL="https://login.staging.ubuntu.com/api/v2/" UBUNTU_ONE_URL="https://staging.one.ubuntu.com/" DEBUG_SSO_API=1 ./plugin-viewer-gtk.py ubi-ubuntuone

the DEBUG env var tells libsoup to dump the REST calls to stderr, which lets you see what's going on.
The viewer script doesn't have your user password, so the parts after the SSO calls will die, but you can see what's going on with the dumped REST calls.

Updated tests as well. Run those like this, from the top level:
UBIQUITY_GLADE=./gui/gtk UBIQUITY_PLUGIN_PATH=./ubiquity/plugins PYTHONPATH=. python3 tests/test_ubi_ubuntuone.py

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

This does look like it will create/log in to an ubuntu sso account, but for this to properly be a valid Ubuntu One token, there is some extra work that needs to be done. There is an API URL on one.ubuntu.com which must be signed using the oauth token that was just received, and loaded. You can see how this is done in the credentials code of ubuntuone-client.

Also, there is a bit where the hostname is used (in ubuntu-sso-client) in the token name. We'll have to pull this information from wherever in the installer it is stored, and pass it on to the server and stored as a property on the token in the keyring. I'm not 100% sure what the code is like for this in ubuntu-sso-client right now, but it's there.

review: Needs Fixing
lp:~mikemc/ubiquity/replace-sso-cli updated
5822. By Mike McCracken

add API call to GET the ubuntuone ping url.

5823. By Mike McCracken

- Add hostname to token name using same scheme as SSO client.

5824. By Mike McCracken

- get hostname from debconf interface, and fake it in plugin-viewer-gtk.py
- get token after registering new account.
- improve test coverage
- allow specifying API URLS to enable testing with staging.

Revision history for this message
Mike McCracken (mikemc) wrote :

Updated change description to include pointing the code at staging urls for testing.

Revision history for this message
Alejandro J. Cura (alecu) wrote :

I tested IRL against staging, and it seems to be creating the account successfully, and then pinging the right u1 url.
The tests pass, and the code looks clean. Good work!

review: Approve
lp:~mikemc/ubiquity/replace-sso-cli updated
5825. By Mike McCracken

remove out-of-date comments and update copyright

Revision history for this message
dobey (dobey) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'plugin-viewer-gtk.py'
2--- plugin-viewer-gtk.py 2013-02-14 23:29:50 +0000
3+++ plugin-viewer-gtk.py 2013-02-27 23:14:21 +0000
4@@ -77,6 +77,9 @@
5
6 add_connection_watch(page_gtk.plugin_set_online_state)
7
8+ # fake debconf interface:
9+ page_gtk.db = {'netcfg/get_hostname': 'test hostname'}
10+
11 button_box = Gtk.ButtonBox(spacing=12)
12 button_box.set_layout(Gtk.ButtonBoxStyle.END)
13 button_box.pack_start(win.button_back, True, True, 6)
14
15=== modified file 'tests/test_ubi_ubuntuone.py'
16--- tests/test_ubi_ubuntuone.py 2013-02-27 23:14:21 +0000
17+++ tests/test_ubi_ubuntuone.py 2013-02-27 23:14:21 +0000
18@@ -1,10 +1,12 @@
19 #!/usr/bin/python3
20
21-import tempfile
22+import http.client
23+import json
24+import oauthlib
25 import unittest
26
27-import mock
28-from gi.repository import Gtk, GObject, GLib
29+from mock import call, DEFAULT, Mock, patch, PropertyMock, sentinel
30+from gi.repository import Gtk
31
32 from ubiquity import plugin_manager
33
34@@ -12,11 +14,23 @@
35 ubi_ubuntuone = plugin_manager.load_plugin('ubi-ubuntuone')
36
37
38+class TokenNameTestCase(unittest.TestCase):
39+
40+ def test_simple_token_name(self):
41+ name = ubi_ubuntuone.get_token_name('simple')
42+ self.assertEqual(name, "Ubuntu One @ simple")
43+
44+ def test_complex_token_name(self):
45+ name = ubi_ubuntuone.get_token_name('simple @ complex')
46+ self.assertEqual(name, "Ubuntu One @ simple AT complex")
47+
48+
49 class BaseTestPageGtk(unittest.TestCase):
50
51 def setUp(self):
52- mock_controller = mock.Mock()
53- self.page = ubi_ubuntuone.PageGtk(mock_controller, ui=mock.Mock())
54+ mock_controller = Mock()
55+ self.page = ubi_ubuntuone.PageGtk(mock_controller, ui=Mock())
56+ self.page.db = Mock(name='db')
57
58
59 class TestPageGtk(BaseTestPageGtk):
60@@ -53,39 +67,19 @@
61 self.assertTrue(self.page._verify_password_entry("xxx"))
62
63
64-class MockSSOTestCase(BaseTestPageGtk):
65-
66- class MockUbuntuSSO():
67-
68- TOKEN = "{'token': 'nonex'}"
69-
70- def mock_done(self, callback, errback, data):
71- callback(self.TOKEN, data)
72- Gtk.main_quit()
73-
74- def register(self, email, passw, callback, errback, data):
75- GObject.idle_add(self.mock_done, callback, errback, data)
76-
77- def login(self, email, passw, callback, errback, data):
78- GObject.idle_add(self.mock_done, callback, errback, data)
79-
80- def test_click_next(self):
81- self.page.ubuntu_sso = self.MockUbuntuSSO()
82- with mock.patch.object(
83- self.page, "_create_keyring_and_store_u1_token") as m_create:
84- self.page.plugin_on_next_clicked()
85- self.assertEqual(self.page.notebook_main.get_current_page(),
86- ubi_ubuntuone.PAGE_SPINNER)
87- m_create.assert_called_with(self.page.ubuntu_sso.TOKEN)
88-
89-
90 class RegisterTestCase(BaseTestPageGtk):
91
92- def test_register_allow_go_forward_not_yet(self):
93+ def test_allow_go_forward_not_without_any_password(self):
94 self.page.entry_email.set_text("foo")
95 self.page.controller.allow_go_forward.assert_called_with(False)
96
97- def test_register_allow_go_foward(self):
98+ def test_allow_go_foward_not_without_matching_password(self):
99+ self.page.entry_email.set_text("foo@bar.com")
100+ self.page.entry_new_password.set_text("pw")
101+ self.page.entry_new_password2.set_text("pwd")
102+ self.page.controller.allow_go_forward.assert_called_with(False)
103+
104+ def test_allow_go_foward(self):
105 self.page.entry_email.set_text("foo@bar.com")
106 self.page.entry_new_password.set_text("pw")
107 self.page.entry_new_password2.set_text("pw")
108@@ -106,32 +100,256 @@
109 self.page.controller.allow_go_forward.assert_called_with(True)
110
111
112-class UbuntuSSOHelperTestCase(unittest.TestCase):
113-
114- def setUp(self):
115- self.callback = mock.Mock()
116- self.callback.side_effect = lambda *args: self.loop.quit()
117- self.errback = mock.Mock()
118- self.errback.side_effect = lambda *args: self.loop.quit()
119- self.loop = GLib.MainLoop(GLib.main_context_default())
120- self.sso_helper = ubi_ubuntuone.UbuntuSSO()
121-
122- def test_spawning_error(self):
123- self.sso_helper.login("foo@example.com", "nopass",
124- self.callback, self.errback)
125- self.loop.run()
126- self.assertTrue(self.errback.called)
127- self.assertFalse(self.callback.called)
128-
129- def test_spawning_success(self):
130- self.sso_helper.BINARY = "/bin/echo"
131- self.sso_helper.login("foo@example.com", "nopass",
132- self.callback, self.errback, data="data")
133- self.loop.run()
134- self.assertFalse(self.errback.called)
135- self.assertTrue(self.callback.called)
136- # ensure stdout is captured and data is also send
137- self.callback.assert_called_with("--login foo@example.com\n", "data")
138+@patch('syslog.syslog', new=print)
139+@patch.object(ubi_ubuntuone, 'get_token_name')
140+@patch.object(Gtk, 'main')
141+class NextButtonActionTestCase(BaseTestPageGtk):
142+
143+ def _call_register(self, mock_token_name, create_success=True):
144+ mock_token_name.return_value = 'tokenname'
145+
146+ self.page.entry_email.set_text("foo@bar.com")
147+ self.page.entry_new_password.set_text("pw")
148+ self.page.entry_new_password2.set_text("pw")
149+ self.page.notebook_main.set_current_page(ubi_ubuntuone.PAGE_REGISTER)
150+
151+ def set_page_register_success(*args, **kwargs):
152+ self.page.account_creation_successful = create_success
153+
154+ with patch.multiple(self.page,
155+ register_new_sso_account=DEFAULT,
156+ login_to_sso=DEFAULT) as mocks:
157+ mr = mocks['register_new_sso_account']
158+ mr.side_effect = set_page_register_success
159+
160+ self.page.plugin_on_next_clicked()
161+
162+ # TODO displayname is temporarily just the email, pending UI
163+ mr.assert_called_once_with("foo@bar.com", "pw", "foo@bar.com")
164+
165+ if create_success:
166+ ml = mocks['login_to_sso']
167+ ml.assert_called_once_with("foo@bar.com", "pw", 'tokenname',
168+ ubi_ubuntuone.PAGE_REGISTER)
169+
170+ def test_call_register_success(self, mock_gtk_main, mock_token_name):
171+ self._call_register(mock_token_name)
172+
173+ def test_call_register_err(self, mock_gtk_main, mock_token_name):
174+ self._call_register(mock_token_name, create_success=False)
175+
176+ def test_call_login(self, mock_gtk_main, mock_token_name):
177+ mock_token_name.return_value = 'tokenname'
178+
179+ self.page.entry_existing_email.set_text("foo")
180+ self.page.entry_existing_password.set_text("pass")
181+ self.page.notebook_main.set_current_page(ubi_ubuntuone.PAGE_LOGIN)
182+
183+ with patch.object(self.page, 'login_to_sso') as mock_login:
184+ self.page.plugin_on_next_clicked()
185+ mock_login.assert_called_once_with("foo", "pass", 'tokenname',
186+ ubi_ubuntuone.PAGE_LOGIN)
187+
188+
189+@patch('syslog.syslog', new=print)
190+@patch.object(Gtk, 'main')
191+class SSOAPITestCase(BaseTestPageGtk):
192+
193+ def _call_handle_done(self, status, response_body, action, from_page):
194+ mock_session = Mock()
195+ mock_msg = Mock()
196+ cfgstr = ('response_body.flatten.return_value'
197+ '.get_data.return_value.decode.return_value')
198+ cfg = {cfgstr: response_body}
199+ mock_msg.configure_mock(**cfg)
200+ mock_status_code = PropertyMock(return_value=status)
201+ type(mock_msg).status_code = mock_status_code
202+
203+ info = {'action': action, 'from_page': from_page}
204+ self.page._handle_soup_message_done(mock_session, mock_msg, info)
205+ self.assertEqual(self.page.notebook_main.get_current_page(),
206+ from_page)
207+
208+ def test_handle_done_token_OK(self, mock_gtk_main):
209+ expected_body = "TESTBODY"
210+ self._call_handle_done(http.client.OK, expected_body,
211+ ubi_ubuntuone.TOKEN_CALLBACK_ACTION,
212+ ubi_ubuntuone.PAGE_REGISTER)
213+ self.assertEqual(self.page.oauth_token,
214+ expected_body)
215+
216+ def test_handle_done_token_CREATED(self, mock_gtk_main):
217+ expected_body = "TESTBODY"
218+ self._call_handle_done(http.client.CREATED,
219+ expected_body,
220+ ubi_ubuntuone.TOKEN_CALLBACK_ACTION,
221+ ubi_ubuntuone.PAGE_REGISTER)
222+ self.assertEqual(self.page.oauth_token,
223+ expected_body)
224+
225+ def test_handle_done_ping_OK(self, mock_gtk_main):
226+ expected_body = "TESTBODY"
227+ self._call_handle_done(http.client.OK, expected_body,
228+ ubi_ubuntuone.PING_CALLBACK_ACTION,
229+ ubi_ubuntuone.PAGE_REGISTER)
230+ self.assertTrue(self.page.ping_successful)
231+
232+ def test_handle_done_ping_CREATED(self, mock_gtk_main):
233+ expected_body = "TESTBODY"
234+ self._call_handle_done(http.client.CREATED,
235+ expected_body,
236+ ubi_ubuntuone.PING_CALLBACK_ACTION,
237+ ubi_ubuntuone.PAGE_REGISTER)
238+ self.assertTrue(self.page.ping_successful)
239+
240+ def test_handle_done_error_token(self, mock_gtk_main):
241+ expected_body = json.dumps({"message": "tstmsg"})
242+ # GONE or anything other than OK/CREATED:
243+ self._call_handle_done(http.client.GONE, expected_body,
244+ ubi_ubuntuone.TOKEN_CALLBACK_ACTION,
245+ ubi_ubuntuone.PAGE_REGISTER)
246+ self.assertEqual(self.page.oauth_token, None)
247+ self.assertEqual(self.page.label_global_error.get_text(),
248+ "tstmsg")
249+
250+ def test_handle_done_error_ping(self, mock_gtk_main):
251+ expected_body = "error"
252+ with patch.object(self.page.label_global_error,
253+ 'get_text') as mock_get_text:
254+ mock_get_text.return_value = "err"
255+ # GONE or anything other than OK/CREATED:
256+ self._call_handle_done(http.client.GONE, expected_body,
257+ ubi_ubuntuone.PING_CALLBACK_ACTION,
258+ ubi_ubuntuone.PAGE_REGISTER)
259+ self.assertFalse(self.page.ping_successful)
260+ self.assertEqual(self.page.label_global_error.get_text(),
261+ "err")
262+
263+ @patch('json.dumps')
264+ def test_login_to_sso(self, mock_json_dumps, mock_gtk_main):
265+ email = 'email'
266+ password = 'pass'
267+ token_name = 'tok'
268+ json_ct = 'application/json'
269+ expected_dict = {'email': email,
270+ 'password': password,
271+ 'token_name': token_name}
272+ # NOTE: in order to avoid failing tests when dict key ordering
273+ # changes, we pass the actual dict by mocking json.dumps. This
274+ # way we can compare the dicts instead of their
275+ # serializations.
276+ mock_json_dumps.return_value = expected_dict
277+ with patch.multiple(self.page, soup=DEFAULT, session=DEFAULT) as mocks:
278+ typeobj = type(mocks['soup'].MemoryUse)
279+ typeobj.COPY = PropertyMock(return_value=sentinel.COPY)
280+ self.page.login_to_sso(email, password, token_name,
281+ ubi_ubuntuone.PAGE_LOGIN)
282+ expected = [call.Message.new("POST",
283+ ubi_ubuntuone.UBUNTU_SSO_URL +
284+ 'tokens/oauth'),
285+ call.Message.new().set_request(json_ct,
286+ sentinel.COPY,
287+ expected_dict,
288+ len(expected_dict)),
289+ call.Message.new().request_headers.append('Accept',
290+ json_ct)]
291+ self.assertEqual(mocks['soup'].mock_calls,
292+ expected)
293+
294+ info = {'action': ubi_ubuntuone.TOKEN_CALLBACK_ACTION,
295+ 'from_page': ubi_ubuntuone.PAGE_LOGIN}
296+
297+ e = [call.queue_message(mocks['soup'].Message.new.return_value,
298+ self.page._handle_soup_message_done,
299+ info)]
300+
301+ self.assertEqual(mocks['session'].mock_calls, e)
302+
303+ @patch('json.dumps')
304+ def test_register_new_sso_account(self, mock_json_dumps, mock_gtk_main):
305+ email = 'email'
306+ password = 'pass'
307+ displayname = 'mr tester'
308+ json_ct = 'application/json'
309+ expected_dict = {'email': email,
310+ 'displayname': displayname,
311+ 'password': password}
312+
313+ # See test_login_to_sso for comment about patching json.dumps():
314+ mock_json_dumps.return_value = expected_dict
315+ with patch.multiple(self.page, soup=DEFAULT, session=DEFAULT) as mocks:
316+ typeobj = type(mocks['soup'].MemoryUse)
317+ typeobj.COPY = PropertyMock(return_value=sentinel.COPY)
318+ self.page.register_new_sso_account(email, password,
319+ displayname)
320+ expected = [call.Message.new("POST",
321+ ubi_ubuntuone.UBUNTU_SSO_URL +
322+ 'accounts'),
323+ call.Message.new().set_request(json_ct,
324+ sentinel.COPY,
325+ expected_dict,
326+ len(expected_dict)),
327+ call.Message.new().request_headers.append('Accept',
328+ json_ct)]
329+ self.assertEqual(mocks['soup'].mock_calls,
330+ expected)
331+
332+ info = {'action': ubi_ubuntuone.ACCOUNT_CALLBACK_ACTION,
333+ 'from_page': ubi_ubuntuone.PAGE_REGISTER}
334+
335+ e = [call.queue_message(mocks['soup'].Message.new.return_value,
336+ self.page._handle_soup_message_done,
337+ info)]
338+
339+ self.assertEqual(mocks['session'].mock_calls, e)
340+
341+ @patch('json.loads')
342+ @patch.multiple(ubi_ubuntuone, Client=DEFAULT, get_ping_info=DEFAULT)
343+ def test_ping_u1_url(self, mock_json_loads,
344+ mock_gtk_main, Client, get_ping_info):
345+
346+ from_page = 1
347+ email = 'email'
348+ signed_url = "signed_url"
349+ signed_headers = {'a': 'b'}
350+ Client.return_value.sign.return_value = (signed_url,
351+ signed_headers,
352+ None)
353+ get_ping_info.return_value = ('url', {'C': 'D'})
354+ mock_json_loads.return_value = {'consumer_key': 'ck',
355+ 'consumer_secret': 'cs',
356+ 'token_key': 'tk',
357+ 'token_secret': 'ts'}
358+
359+ with patch.multiple(self.page, soup=DEFAULT, session=DEFAULT,
360+ oauth_token=sentinel.token) as mocks:
361+ self.page.ping_u1_url(email, from_page)
362+
363+ mock_json_loads.assert_called_once_with(sentinel.token)
364+
365+ sigtype = oauthlib.oauth1.SIGNATURE_TYPE_AUTH_HEADER
366+ ct_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
367+ expected = [call('ck', 'cs', 'tk', 'ts',
368+ signature_method=oauthlib.oauth1.SIGNATURE_HMAC,
369+ signature_type=sigtype),
370+ call().sign('url?C=D', 'GET', {},
371+ headers=ct_headers)]
372+ self.assertEqual(Client.mock_calls, expected)
373+
374+ expected = [call.Message.new("GET", signed_url),
375+ call.Message.new().request_headers.append('a', 'b')]
376+
377+ self.assertEqual(mocks['soup'].mock_calls,
378+ expected)
379+
380+ info = {'action': ubi_ubuntuone.PING_CALLBACK_ACTION,
381+ 'from_page': from_page}
382+
383+ e = [call.queue_message(mocks['soup'].Message.new.return_value,
384+ self.page._handle_soup_message_done,
385+ info)]
386+
387+ self.assertEqual(mocks['session'].mock_calls, e)
388
389
390 if __name__ == '__main__':
391
392=== modified file 'ubiquity/plugins/ubi-ubuntuone.py'
393--- ubiquity/plugins/ubi-ubuntuone.py 2013-02-27 23:14:21 +0000
394+++ ubiquity/plugins/ubi-ubuntuone.py 2013-02-27 23:14:21 +0000
395@@ -1,7 +1,6 @@
396 # -*- coding: utf-8; Mode: Python; indent-tabs-mode: nil; tab-width: 4 -*-
397
398-# Copyright (C) 2012 Canonical Ltd.
399-# Written by Michael Vogt <mvo@ubuntu.com>
400+# Copyright (C) 2012-2013 Canonical Ltd.
401 #
402 # This program is free software; you can redistribute it and/or modify
403 # it under the terms of the GNU General Public License as published by
404@@ -17,16 +16,33 @@
405 # along with this program; if not, write to the Free Software
406 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
407
408-import pwd
409-import re
410+import http.client
411+import json
412+from oauthlib.oauth1 import (Client, SIGNATURE_HMAC,
413+ SIGNATURE_TYPE_AUTH_HEADER)
414 import os
415 import os.path
416+import platform
417+import pwd
418 import subprocess
419 import shutil
420 import syslog
421+import traceback
422+from urllib.parse import urlencode
423
424 from ubiquity import plugin, misc
425
426+PLUGIN_VERSION = "1.0"
427+UBUNTU_SSO_URL = "https://login.ubuntu.com/api/v2/"
428+UBUNTU_ONE_URL = "https://one.ubuntu.com/"
429+
430+TOKEN_SEPARATOR = ' @ '
431+SEPARATOR_REPLACEMENT = ' AT '
432+U1_APP_NAME = "Ubuntu One"
433+
434+(ACCOUNT_CALLBACK_ACTION,
435+ TOKEN_CALLBACK_ACTION,
436+ PING_CALLBACK_ACTION) = range(3)
437
438 NAME = 'ubuntuone'
439 AFTER = 'usersetup'
440@@ -37,85 +53,6 @@
441 PAGE_SPINNER,
442 ) = range(3)
443
444-# TODO:
445-# - network awareness (steal from timezone map page)
446-# - rename this all to ubuntu sso instead of ubuntuone to avoid confusion
447-# that we force people to sign up for payed services on install (?) where
448-# what we want is to make it super simple to use our services
449-# - take the username from the usersetup step when creating the token
450-# - get a design for the UI
451-# * to create a new account
452-# * to login into a existing account
453-# * deal with forgoten passwords
454-# * skip account creation
455-
456-
457-# TESTING end-to-end for real
458-#
459-# * get a raring cdimage
460-# * run:
461-# kvm -m 1500 -hda /path/to/random-image -cdrom /path/to/raring-arch.iso \
462-# -boot d
463-# * in the VM:
464-# - add universe
465-# - sudo apt-get install bzr build-essential python3-setuptools debhelper python3-piston-mini-client
466-# - bzr co lp:~mvo/+junk/cli-sso-login
467-# - (cd cli-sso-login; dpkg-buildpackage; sudo dpkg -i ../python3*.deb)
468-#
469-# - install cli-sso-login from
470-# - bzr co --lightweight lp:~mvo/ubiquity/ssologin
471-# - cd ssologin
472-# - sudo cp ubiquity/plugins/* /usr/lib/ubiquity/plugins
473-# - sudo cp ubiquity/* /usr/lib/ubiquity/ubiquity
474-# - sudo cp gui/gtk//*.ui /usr/share/ubiquity/gtk
475-# - sudo cp scripts/* /usr/share/ubiquity/
476-# - sudo cp bin/ubiquity /usr/bin
477-# - sudo ubiquity
478-
479-
480-class UbuntuSSO(object):
481-
482- # this will need the helper
483- # lp:~mvo/+junk/cli-sso-login installed
484-
485- BINARY = "/usr/bin/ubuntu-sso-cli"
486-
487- def _child_exited(self, pid, status, data):
488- stdin_fd, stdout_fd, stderr_fd, callback, errback, user_data = data
489- exit_code = os.WEXITSTATUS(status)
490- # the delayed reading will only work if the amount of data is
491- # small enough to not cause the pipe to block which on most
492- # system is ok as "ulimit -p" shows 8 pages by default (4k)
493- stdout = os.read(stdout_fd, 2048).decode("utf-8")
494- stderr = os.read(stderr_fd, 2048).decode("utf-8")
495- if exit_code == 0:
496- callback(stdout, user_data)
497- else:
498- errback(stderr, user_data)
499-
500- def _spawn_sso_helper(self, cmd, password, callback, errback, data):
501- from gi.repository import GLib
502- res, pid, stdin_fd, stdout_fd, stderr_fd = GLib.spawn_async_with_pipes(
503- "/", cmd, None,
504- (GLib.SpawnFlags.LEAVE_DESCRIPTORS_OPEN |
505- GLib.SpawnFlags.DO_NOT_REAP_CHILD), None, None)
506- if res:
507- os.write(stdin_fd, password.encode("utf-8"))
508- os.write(stdin_fd, "\n".encode("utf-8"))
509- GLib.child_watch_add(
510- GLib.PRIORITY_DEFAULT, pid, self._child_exited,
511- (stdin_fd, stdout_fd, stderr_fd, callback, errback, data))
512- else:
513- errback("Failed to spawn %s" % cmd, data)
514-
515- def login(self, email, password, callback, errback, data=None):
516- cmd = [self.BINARY, "--login", email]
517- self._spawn_sso_helper(cmd, password, callback, errback, data)
518-
519- def register(self, email, password, callback, errback, data=None):
520- cmd = [self.BINARY, "--register", email]
521- self._spawn_sso_helper(cmd, password, callback, errback, data)
522-
523
524 class Page(plugin.Plugin):
525
526@@ -124,6 +61,22 @@
527 return plugin.Plugin.prepare(unfiltered)
528
529
530+def get_ping_info():
531+ base = os.environ.get("UBUNTU_ONE_URL", UBUNTU_ONE_URL)
532+ url = base + "oauth/sso-finished-so-get-tokens/{email}"
533+ params = dict(platform=platform.system(),
534+ platform_version=platform.release(),
535+ platform_arch=platform.machine(),
536+ client_version=PLUGIN_VERSION)
537+ return (url, params)
538+
539+
540+def get_token_name(hostname):
541+ computer_name = hostname.replace(TOKEN_SEPARATOR,
542+ SEPARATOR_REPLACEMENT)
543+ return TOKEN_SEPARATOR.join((U1_APP_NAME, computer_name))
544+
545+
546 class PageGtk(plugin.PluginUI):
547 plugin_title = 'ubiquity/text/ubuntuone_heading_label'
548
549@@ -156,14 +109,122 @@
550 self.page = builder.get_object('stepUbuntuOne')
551 self.notebook_main.set_show_tabs(False)
552 self.plugin_widgets = self.page
553- self.oauth_token = None
554 self.skip_step = False
555 self.online = False
556 self.label_global_error.set_text("")
557- # the worker
558- self.ubuntu_sso = UbuntuSSO()
559+ self._generic_error = "error"
560+
561+ self.oauth_token = None
562+ self.ping_successful = False
563+ self.account_creation_successful = False
564+ from gi.repository import Soup
565+ self.soup = Soup
566+ self.session = Soup.SessionAsync()
567+ if "DEBUG_SSO_API" in os.environ:
568+ self.session.add_feature(Soup.Logger.new(Soup.LoggerLogLevel.BODY,
569+ -1))
570+
571 self.info_loop(None)
572
573+ def login_to_sso(self, email, password, token_name, from_page):
574+ """Queue POST message to /tokens to get oauth token.
575+ See _handle_soup_message_done() for completion details.
576+ """
577+ body = json.dumps({'email': email,
578+ 'password': password,
579+ 'token_name': token_name})
580+ service_url = os.environ.get("UBUNTU_SSO_URL", UBUNTU_SSO_URL)
581+ tokens_url = service_url + "tokens/oauth"
582+ message = self.soup.Message.new("POST", tokens_url)
583+ message.set_request('application/json',
584+ self.soup.MemoryUse.COPY,
585+ body, len(body))
586+ message.request_headers.append('Accept', 'application/json')
587+
588+ self.session.queue_message(message, self._handle_soup_message_done,
589+ dict(action=TOKEN_CALLBACK_ACTION,
590+ from_page=from_page))
591+
592+ def register_new_sso_account(self, email, password, displayname):
593+ """Queue POST to /accounts to register new account and get token.
594+ See _handle_soup_message_done() for completion details.
595+ """
596+ params = {'email': email,
597+ 'password': password,
598+ 'displayname': displayname}
599+ body = json.dumps(params)
600+ service_url = os.environ.get("UBUNTU_SSO_URL", UBUNTU_SSO_URL)
601+ accounts_url = service_url + "accounts"
602+ message = self.soup.Message.new("POST", accounts_url)
603+ message.set_request('application/json',
604+ self.soup.MemoryUse.COPY,
605+ body, len(body))
606+ message.request_headers.append('Accept', 'application/json')
607+
608+ self.session.queue_message(message, self._handle_soup_message_done,
609+ dict(action=ACCOUNT_CALLBACK_ACTION,
610+ from_page=PAGE_REGISTER))
611+
612+ def _handle_soup_message_done(self, session, message, info):
613+ """Handle message completion, check for errors."""
614+ from gi.repository import Gtk
615+ syslog.syslog("soup message ({}) code: {}".format(info,
616+ message.status_code))
617+ content = message.response_body.flatten().get_data().decode("utf-8")
618+
619+ if message.status_code in [http.client.OK, http.client.CREATED]:
620+ if info['action'] == TOKEN_CALLBACK_ACTION:
621+ self.oauth_token = content
622+ elif info['action'] == PING_CALLBACK_ACTION:
623+ self.ping_successful = True
624+ elif info['action'] == ACCOUNT_CALLBACK_ACTION:
625+ self.account_creation_successful = True
626+ else:
627+ self.notebook_main.set_current_page(info['from_page'])
628+
629+ syslog.syslog("Error in soup message: %r" % message.reason_phrase)
630+ syslog.syslog("Error response headers: %r" %
631+ message.get_property("response-headers"))
632+ syslog.syslog("error response body: %r " %
633+ message.response_body.flatten().get_data())
634+
635+ try:
636+ response_dict = json.loads(content)
637+ error_message = response_dict["message"]
638+ except ValueError:
639+ error_message = self._generic_error
640+
641+ self.label_global_error.set_markup("<b><big>%s</big></b>" %
642+ error_message)
643+
644+ Gtk.main_quit()
645+
646+ def ping_u1_url(self, email, from_page):
647+ """Sign and GET a URL to enable U1 server access."""
648+ token = json.loads(self.oauth_token)
649+
650+ oauth_client = Client(token['consumer_key'],
651+ token['consumer_secret'],
652+ token['token_key'],
653+ token['token_secret'],
654+ signature_method=SIGNATURE_HMAC,
655+ signature_type=SIGNATURE_TYPE_AUTH_HEADER)
656+
657+ url, params = get_ping_info()
658+ url = url.format(email=email)
659+ url += "?" + urlencode(params)
660+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
661+ signed_url, signed_headers, _ = oauth_client.sign(url, "GET", {},
662+ headers=headers)
663+ message = self.soup.Message.new("GET", signed_url)
664+
665+ for k, v in signed_headers.items():
666+ message.request_headers.append(k, v)
667+
668+ self.session.queue_message(message, self._handle_soup_message_done,
669+ dict(action=PING_CALLBACK_ACTION,
670+ from_page=from_page))
671+
672 def plugin_set_online_state(self, state):
673 self.online = state
674
675@@ -180,34 +241,72 @@
676 from gi.repository import Gtk
677 if self.skip_step:
678 return False
679- if self.notebook_main.get_current_page() == PAGE_REGISTER:
680- self.ubuntu_sso.register(self.entry_email.get_text(),
681- self.entry_new_password.get_text(),
682- callback=self._ubuntu_sso_callback,
683- errback=self._ubuntu_sso_errback,
684- data=PAGE_REGISTER)
685- elif self.notebook_main.get_current_page() == PAGE_LOGIN:
686- self.ubuntu_sso.login(self.entry_existing_email.get_text(),
687- self.entry_existing_password.get_text(),
688- callback=self._ubuntu_sso_callback,
689- errback=self._ubuntu_sso_errback,
690- data=PAGE_LOGIN)
691- else:
692- raise AssertionError("Should never be reached happen")
693
694+ from_page = self.notebook_main.get_current_page()
695 self.notebook_main.set_current_page(PAGE_SPINNER)
696 self.spinner_connect.start()
697- # the ubuntu_sso.{login,register} will stop this loop when its done
698- Gtk.main()
699+
700+ if from_page == PAGE_REGISTER:
701+
702+ # First create new account before getting token:
703+ email = self.entry_email.get_text()
704+ password = self.entry_new_password.get_text()
705+ displayname = email # TODO get real displayname from UI
706+
707+ try:
708+ self.register_new_sso_account(email, password, displayname)
709+ except Exception:
710+ syslog.syslog("exception in register_new_sso_account: %r" %
711+ traceback.format_exc())
712+ return True
713+
714+ Gtk.main()
715+
716+ if not self.account_creation_successful:
717+ syslog.syslog("Error registering SSO account, exiting.")
718+ return True
719+
720+ elif from_page == PAGE_LOGIN:
721+ email = self.entry_existing_email.get_text()
722+ password = self.entry_existing_password.get_text()
723+
724+ else:
725+ raise AssertionError("'Next' from invalid page: %r" % from_page)
726+
727+ # Now get the token, regardless of which page we came from
728+ try:
729+ hostname = self.db.get('netcfg/get_hostname')
730+ self.login_to_sso(email, password, get_token_name(hostname),
731+ from_page)
732+ except Exception:
733+ syslog.syslog("exception in login_to_sso: %r" %
734+ traceback.format_exc())
735+ return True
736+
737+ Gtk.main()
738+
739+ if self.oauth_token is None:
740+ syslog.syslog("Error getting oauth_token, not creating keyring")
741+ return True
742+
743+ try:
744+ self.ping_u1_url(email, from_page)
745+ except Exception:
746+ syslog.syslog("exception in ping_u1_url: %r" %
747+ traceback.format_exc())
748+
749+ Gtk.main()
750+
751 self.spinner_connect.stop()
752
753- # if there is no token at this point, there is a error,
754- # so stop moving forward
755- if self.oauth_token is None:
756+ if not self.ping_successful:
757+ syslog.syslog("Error pinging U1 URL, not creating keyring")
758 return True
759
760 # all good, create a (encrypted) keyring and store the token for later
761- self._create_keyring_and_store_u1_token(self.oauth_token)
762+ rv = self._create_keyring_and_store_u1_token(self.oauth_token)
763+ if rv != 0:
764+ return True
765 return False
766
767 def _create_keyring_and_store_u1_token(self, token):
768@@ -216,24 +315,6 @@
769 # root and it seems that anything other than "drop_all_privileges"
770 # will not trigger the correct dbus activation for the
771 # gnome-keyring daemon
772- #
773- # mvo: We could do this in the "install" phase too, but more fragile
774- # I think, here is what would be required:
775- # - copy over XAUTHORITY to /target/home/$targetuser/.Xauthority
776- # - chown $targetuser.$targetuser \
777- # /target/home/$targetuser/.Xauthority
778- # - (bind)mount /proc in /target
779- # - run "dbus-uuidgen --ensure" in /target to get a dbus
780- # machine-id
781- # - run the helper with:
782- # chroot /target sudo -u $targetuser HOME=/home/$targetuser \
783- # XAUTHORITY=/home/$targetuser/.Xauthority \
784- # DBUS_SESSION_BUS_ADDRESS="autolaunch:" \
785- # ubuntuone-keyring-helper
786- # - ensure that the dbus-daemon and gnome-keyring-daemon that
787- # get spawned in /target get killed so that /target can
788- # get unmounted again
789- # - umount /proc
790 p = subprocess.Popen(
791 ["/usr/share/ubiquity/ubuntuone-keyring-helper"],
792 stdin=subprocess.PIPE,
793@@ -245,6 +326,7 @@
794 p.stdin.write("\n")
795 res = p.wait()
796 syslog.syslog("keyring helper returned %s" % res)
797+ return res
798
799 def plugin_translate(self, lang):
800 pasw = self.controller.get_string('password_inactive_label', lang)
801@@ -261,25 +343,8 @@
802 'error_register', lang)
803 self._error_login = self.controller.get_string(
804 'error_login', lang)
805-
806- # callbacks
807- def _ubuntu_sso_callback(self, oauth_token, data):
808- """Called when a oauth token was returned successfully"""
809- from gi.repository import Gtk
810- self.oauth_token = oauth_token
811- Gtk.main_quit()
812-
813- def _ubuntu_sso_errback(self, error, data):
814- """Called when a error acquiring the oauth token from the helper"""
815- from gi.repository import Gtk
816- syslog.syslog("ubuntu sso failed: '%s'" % error)
817- self.notebook_main.set_current_page(data)
818- if data == PAGE_REGISTER:
819- err = self._error_register
820- else:
821- err = self._error_login
822- self.label_global_error.set_markup("<b><big>%s</big></b>" % err)
823- Gtk.main_quit()
824+ self._generic_error = self.controller.get_string(
825+ 'generic_error', lang)
826
827 # signals
828 def on_button_have_account_clicked(self, button):

Subscribers

People subscribed via source and target branches

to all changes: