Merge lp:~mikemc/ubiquity/replace-sso-cli into lp:~mikemc/ubiquity/add-online-debug
- replace-sso-cli
- Merge into add-online-debug
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 |
Related bugs: |
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:/
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_
- 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.
Mike McCracken (mikemc) wrote : | # |
Updated change description to include pointing the code at staging urls for testing.
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!
- 5825. By Mike McCracken
-
remove out-of-date comments and update copyright
dobey (dobey) : | # |
Preview Diff
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): |
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.