Merge lp:~mvo/ubiquity/ssologin into lp:ubiquity

Proposed by Michael Vogt
Status: Merged
Merged at revision: 5933
Proposed branch: lp:~mvo/ubiquity/ssologin
Merge into: lp:ubiquity
Diff against target: 1200 lines (+1122/-6)
8 files modified
debian/ubiquity.templates (+16/-0)
gui/gtk/stepUbuntuOne.ui (+451/-0)
plugin-viewer-gtk.py (+87/-0)
scripts/plugininstall.py (+10/-5)
scripts/ubuntuone-keyring-helper (+31/-0)
tests/test_ubi_ubuntuone.py (+144/-0)
ubiquity/gtkwidgets.py (+5/-1)
ubiquity/plugins/ubi-ubuntuone.py (+378/-0)
To merge this branch: bzr merge lp:~mvo/ubiquity/ssologin
Reviewer Review Type Date Requested Status
Ubuntu Installer Team Pending
Review via email: mp+137264@code.launchpad.net

Description of the change

This branch adds a "Ubuntu ONE" login page to the install and moves the oauth token into the users login keyring.

At this point its WIP and what is still missing is:
- a API for registering new users without a capture (or with a delayed capture)
- approval for the UI design
- the lp:~mvo/+junk/cli-sso-login helper binary must go into a package (like ubuntu-sso-login)
- review from a ubiquity developer to get all the ubiquity details right
- kill(?) plugin-viewer, I mostly used it to explore the system

But its work for existing logins and the token get added to the users login keyring.

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

I think the branch is at a point where a first tentative review from a ubiquity developer would be nice to get a idea if it fits well enough into ubiquity or if it needs refactor there. I expect some UI tweaks when the final design is there, but that should all be relatively contained.

Revision history for this message
Stéphane Graber (stgraber) wrote :

Some comments:
 - You appear to have a duplicate ubiquity/text/ubuntuone_heading_label entry in the debconf template.
 - KEYRING_FILE shouldn't hardcode the live username as the livefs username changes depending on the flavour or casper configuration.
 - I'm surprised not to see any added dependencies to debian/control? Does ubiquity already depend on gnome-keyring and the other libraries you're using?

A few more thoughts:
 - Did you test your plugin in pre-seeded "automatic" mode? My guess is that it should be skipped in such case.
 - What happens when the user reinstalls the system (keeping /home intact) instead of doing a clean install?

I must admit having only very quickly read through the code, so there very well may be more

lp:~mvo/ubiquity/ssologin updated
5806. By Michael Vogt

debian/ubiquity.templates: remove duplicated ubiquity/text/ubuntuone_heading_label - thanks Stephane

5807. By Michael Vogt

do not hardcode KEYRING_FILE but get from uid 999 instead, thanks to Stephane

5808. By Michael Vogt

skip copy of the keyring if the target path is already there, thanks to Stephane

5809. By Michael Vogt

ubiquity/plugins/ubi-ubuntuone.py: do not create the page if in UBIQUITY_AUTOMATIC mode (or if the user has UBIQUITY_NO_SSO set). also skip if GnomeKeyring can not be imported

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks a lot Stephane!

that is very helpful feedback and I addressed the points you raised now.

The only thing I'm unsure about is the dependencies. Right now the regular ubuntu desktop CD has
everything needed and I'm not sure what the policy should be for the other desktop environments, i.e.
I don't know if xubuntu has a gnome-keyring-daemon and if so if they want to integrate with ubuntusso.
So I changed the code to simply skip the page if "from gi.repository import GnomeKeyring" fails.

Revision history for this message
Michael Vogt (mvo) wrote :

Setting back to WIP based on the recent discussions around if this actually should be part of the installer of if it should go in as a first-run wizard.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/ubiquity.templates'
2--- debian/ubiquity.templates 2012-10-19 14:27:05 +0000
3+++ debian/ubiquity.templates 2012-12-04 15:21:26 +0000
4@@ -149,6 +149,22 @@
5 Type: text
6 _Description: Take Photo
7
8+Template: ubiquity/text/ubuntuone_heading_label
9+Type: text
10+_Description: Setup Ubuntu ONE
11+
12+Template: ubiquity/text/error_register
13+Type: text
14+_Description: Error registering account
15+
16+Template: ubiquity/text/error_login
17+Type: text
18+_Description: Error loging into the account
19+
20+Template: ubiquity/text/email_inactive_label
21+Type: text
22+_Description: Enter your E-Mail address
23+
24 Template: ubiquity/text/fullname_label
25 Type: text
26 _Description: Your name:
27
28=== added file 'gui/gtk/stepUbuntuOne.ui'
29--- gui/gtk/stepUbuntuOne.ui 1970-01-01 00:00:00 +0000
30+++ gui/gtk/stepUbuntuOne.ui 2012-12-04 15:21:26 +0000
31@@ -0,0 +1,451 @@
32+<?xml version="1.0" encoding="UTF-8"?>
33+<interface>
34+ <!-- interface-requires gtk+ 3.0 -->
35+ <object class="GtkBox" id="stepUbuntuOne">
36+ <property name="visible">True</property>
37+ <property name="can_focus">False</property>
38+ <property name="orientation">vertical</property>
39+ <child>
40+ <object class="GtkNotebook" id="notebook_main">
41+ <property name="visible">True</property>
42+ <property name="can_focus">True</property>
43+ <property name="show_border">False</property>
44+ <child>
45+ <object class="GtkAlignment" id="alignment3">
46+ <property name="visible">True</property>
47+ <property name="can_focus">False</property>
48+ <property name="top_padding">6</property>
49+ <property name="bottom_padding">6</property>
50+ <property name="left_padding">6</property>
51+ <property name="right_padding">6</property>
52+ <child>
53+ <object class="GtkBox" id="box2">
54+ <property name="visible">True</property>
55+ <property name="can_focus">False</property>
56+ <property name="orientation">vertical</property>
57+ <property name="spacing">18</property>
58+ <child>
59+ <object class="GtkLabel" id="label4">
60+ <property name="visible">True</property>
61+ <property name="can_focus">False</property>
62+ <property name="xalign">0</property>
63+ <property name="label" translatable="yes">Ubuntu ONE</property>
64+ <attributes>
65+ <attribute name="weight" value="bold"/>
66+ </attributes>
67+ </object>
68+ <packing>
69+ <property name="expand">False</property>
70+ <property name="fill">True</property>
71+ <property name="position">0</property>
72+ </packing>
73+ </child>
74+ <child>
75+ <object class="GtkLabel" id="label10">
76+ <property name="visible">True</property>
77+ <property name="can_focus">False</property>
78+ <property name="xalign">0</property>
79+ <property name="label" translatable="yes">Ubuntu ONE allows you to store, share and sync your files accross multiple plattforms...</property>
80+ </object>
81+ <packing>
82+ <property name="expand">False</property>
83+ <property name="fill">True</property>
84+ <property name="position">1</property>
85+ </packing>
86+ </child>
87+ <child>
88+ <object class="GtkGrid" id="grid1">
89+ <property name="visible">True</property>
90+ <property name="can_focus">False</property>
91+ <property name="row_spacing">6</property>
92+ <property name="column_spacing">6</property>
93+ <child>
94+ <object class="GtkEntry" id="entry_email">
95+ <property name="visible">True</property>
96+ <property name="can_focus">True</property>
97+ <property name="invisible_char">•</property>
98+ <property name="invisible_char_set">True</property>
99+ <signal name="changed" handler="info_loop" swapped="no"/>
100+ </object>
101+ <packing>
102+ <property name="left_attach">1</property>
103+ <property name="top_attach">0</property>
104+ <property name="width">1</property>
105+ <property name="height">1</property>
106+ </packing>
107+ </child>
108+ <child>
109+ <object class="GtkLabel" id="label6">
110+ <property name="visible">True</property>
111+ <property name="can_focus">False</property>
112+ <property name="label" translatable="yes">Your E-Mail:</property>
113+ </object>
114+ <packing>
115+ <property name="left_attach">0</property>
116+ <property name="top_attach">0</property>
117+ <property name="width">1</property>
118+ <property name="height">1</property>
119+ </packing>
120+ </child>
121+ <child>
122+ <object class="GtkLabel" id="label11">
123+ <property name="visible">True</property>
124+ <property name="can_focus">False</property>
125+ <property name="label" translatable="yes">Password:</property>
126+ </object>
127+ <packing>
128+ <property name="left_attach">0</property>
129+ <property name="top_attach">1</property>
130+ <property name="width">1</property>
131+ <property name="height">1</property>
132+ </packing>
133+ </child>
134+ <child>
135+ <object class="GtkLabel" id="label12">
136+ <property name="visible">True</property>
137+ <property name="can_focus">False</property>
138+ <property name="label" translatable="yes">Password:</property>
139+ </object>
140+ <packing>
141+ <property name="left_attach">0</property>
142+ <property name="top_attach">2</property>
143+ <property name="width">1</property>
144+ <property name="height">1</property>
145+ </packing>
146+ </child>
147+ <child>
148+ <object class="GtkEntry" id="entry_new_password">
149+ <property name="visible">True</property>
150+ <property name="can_focus">True</property>
151+ <property name="visibility">False</property>
152+ <property name="invisible_char">•</property>
153+ <signal name="changed" handler="info_loop" swapped="no"/>
154+ </object>
155+ <packing>
156+ <property name="left_attach">1</property>
157+ <property name="top_attach">1</property>
158+ <property name="width">1</property>
159+ <property name="height">1</property>
160+ </packing>
161+ </child>
162+ <child>
163+ <object class="GtkEntry" id="entry_new_password2">
164+ <property name="visible">True</property>
165+ <property name="can_focus">True</property>
166+ <property name="visibility">False</property>
167+ <property name="invisible_char">•</property>
168+ <signal name="changed" handler="info_loop" swapped="no"/>
169+ </object>
170+ <packing>
171+ <property name="left_attach">1</property>
172+ <property name="top_attach">2</property>
173+ <property name="width">1</property>
174+ <property name="height">1</property>
175+ </packing>
176+ </child>
177+ </object>
178+ <packing>
179+ <property name="expand">False</property>
180+ <property name="fill">True</property>
181+ <property name="position">2</property>
182+ </packing>
183+ </child>
184+ <child>
185+ <object class="GtkBox" id="box4">
186+ <property name="visible">True</property>
187+ <property name="can_focus">False</property>
188+ <property name="orientation">vertical</property>
189+ <property name="spacing">12</property>
190+ <child>
191+ <object class="GtkBox" id="box1">
192+ <property name="visible">True</property>
193+ <property name="can_focus">False</property>
194+ <child>
195+ <object class="GtkLinkButton" id="linkbutton_have_account">
196+ <property name="label" translatable="yes">I'm already a registered user</property>
197+ <property name="visible">True</property>
198+ <property name="can_focus">True</property>
199+ <property name="receives_default">True</property>
200+ <property name="has_tooltip">True</property>
201+ <property name="relief">none</property>
202+ <signal name="clicked" handler="on_button_have_account_clicked" swapped="no"/>
203+ </object>
204+ <packing>
205+ <property name="expand">False</property>
206+ <property name="fill">True</property>
207+ <property name="position">0</property>
208+ </packing>
209+ </child>
210+ <child>
211+ <placeholder/>
212+ </child>
213+ </object>
214+ <packing>
215+ <property name="expand">False</property>
216+ <property name="fill">True</property>
217+ <property name="position">0</property>
218+ </packing>
219+ </child>
220+ <child>
221+ <object class="GtkBox" id="box5">
222+ <property name="visible">True</property>
223+ <property name="can_focus">False</property>
224+ <child>
225+ <object class="GtkLinkButton" id="linkbutton_skip_account">
226+ <property name="label" translatable="yes">Skip this step</property>
227+ <property name="visible">True</property>
228+ <property name="can_focus">True</property>
229+ <property name="receives_default">True</property>
230+ <property name="has_tooltip">True</property>
231+ <property name="relief">none</property>
232+ <signal name="clicked" handler="on_button_skip_account_clicked" swapped="no"/>
233+ </object>
234+ <packing>
235+ <property name="expand">False</property>
236+ <property name="fill">True</property>
237+ <property name="position">0</property>
238+ </packing>
239+ </child>
240+ <child>
241+ <placeholder/>
242+ </child>
243+ </object>
244+ <packing>
245+ <property name="expand">False</property>
246+ <property name="fill">True</property>
247+ <property name="position">1</property>
248+ </packing>
249+ </child>
250+ </object>
251+ <packing>
252+ <property name="expand">False</property>
253+ <property name="fill">False</property>
254+ <property name="position">3</property>
255+ </packing>
256+ </child>
257+ </object>
258+ </child>
259+ </object>
260+ </child>
261+ <child type="tab">
262+ <object class="GtkLabel" id="label1">
263+ <property name="visible">True</property>
264+ <property name="can_focus">False</property>
265+ <property name="label" translatable="yes">page 1</property>
266+ </object>
267+ <packing>
268+ <property name="tab_fill">False</property>
269+ </packing>
270+ </child>
271+ <child>
272+ <object class="GtkAlignment" id="alignment4">
273+ <property name="visible">True</property>
274+ <property name="can_focus">False</property>
275+ <property name="top_padding">12</property>
276+ <property name="bottom_padding">12</property>
277+ <property name="left_padding">12</property>
278+ <property name="right_padding">12</property>
279+ <child>
280+ <object class="GtkBox" id="box6">
281+ <property name="visible">True</property>
282+ <property name="can_focus">False</property>
283+ <property name="orientation">vertical</property>
284+ <property name="spacing">12</property>
285+ <child>
286+ <object class="GtkLabel" id="label5">
287+ <property name="visible">True</property>
288+ <property name="can_focus">False</property>
289+ <property name="xalign">0</property>
290+ <property name="label" translatable="yes">Connect to Ubuntu ONE</property>
291+ <attributes>
292+ <attribute name="weight" value="bold"/>
293+ </attributes>
294+ </object>
295+ <packing>
296+ <property name="expand">False</property>
297+ <property name="fill">True</property>
298+ <property name="position">0</property>
299+ </packing>
300+ </child>
301+ <child>
302+ <object class="GtkAlignment" id="alignment1">
303+ <property name="visible">True</property>
304+ <property name="can_focus">False</property>
305+ <property name="yalign">1</property>
306+ <property name="xscale">0.5</property>
307+ <property name="yscale">0.5</property>
308+ <child>
309+ <object class="GtkGrid" id="grid2">
310+ <property name="visible">True</property>
311+ <property name="can_focus">False</property>
312+ <property name="column_spacing">6</property>
313+ <child>
314+ <object class="GtkEntry" id="entry_existing_email">
315+ <property name="visible">True</property>
316+ <property name="can_focus">True</property>
317+ <property name="invisible_char">•</property>
318+ <property name="invisible_char_set">True</property>
319+ <signal name="changed" handler="info_loop" swapped="no"/>
320+ </object>
321+ <packing>
322+ <property name="left_attach">1</property>
323+ <property name="top_attach">0</property>
324+ <property name="width">1</property>
325+ <property name="height">1</property>
326+ </packing>
327+ </child>
328+ <child>
329+ <object class="GtkEntry" id="entry_existing_password">
330+ <property name="visible">True</property>
331+ <property name="can_focus">True</property>
332+ <property name="visibility">False</property>
333+ <property name="invisible_char">•</property>
334+ <property name="invisible_char_set">True</property>
335+ <signal name="changed" handler="info_loop" swapped="no"/>
336+ </object>
337+ <packing>
338+ <property name="left_attach">1</property>
339+ <property name="top_attach">1</property>
340+ <property name="width">1</property>
341+ <property name="height">1</property>
342+ </packing>
343+ </child>
344+ <child>
345+ <object class="GtkLabel" id="label8">
346+ <property name="visible">True</property>
347+ <property name="can_focus">False</property>
348+ <property name="label" translatable="yes">E-Mail:</property>
349+ </object>
350+ <packing>
351+ <property name="left_attach">0</property>
352+ <property name="top_attach">0</property>
353+ <property name="width">1</property>
354+ <property name="height">1</property>
355+ </packing>
356+ </child>
357+ <child>
358+ <object class="GtkLabel" id="label9">
359+ <property name="visible">True</property>
360+ <property name="can_focus">False</property>
361+ <property name="label" translatable="yes">Password:</property>
362+ </object>
363+ <packing>
364+ <property name="left_attach">0</property>
365+ <property name="top_attach">1</property>
366+ <property name="width">1</property>
367+ <property name="height">1</property>
368+ </packing>
369+ </child>
370+ </object>
371+ </child>
372+ </object>
373+ <packing>
374+ <property name="expand">True</property>
375+ <property name="fill">True</property>
376+ <property name="position">1</property>
377+ </packing>
378+ </child>
379+ <child>
380+ <object class="GtkBox" id="box3">
381+ <property name="visible">True</property>
382+ <property name="can_focus">False</property>
383+ <child>
384+ <object class="GtkLinkButton" id="linkbutton_need_account">
385+ <property name="label" translatable="yes">I need to create a new account</property>
386+ <property name="visible">True</property>
387+ <property name="can_focus">True</property>
388+ <property name="receives_default">True</property>
389+ <property name="has_tooltip">True</property>
390+ <property name="relief">none</property>
391+ <signal name="clicked" handler="on_button_need_account_clicked" swapped="no"/>
392+ </object>
393+ <packing>
394+ <property name="expand">False</property>
395+ <property name="fill">True</property>
396+ <property name="position">0</property>
397+ </packing>
398+ </child>
399+ <child>
400+ <placeholder/>
401+ </child>
402+ </object>
403+ <packing>
404+ <property name="expand">False</property>
405+ <property name="fill">True</property>
406+ <property name="position">2</property>
407+ </packing>
408+ </child>
409+ </object>
410+ </child>
411+ </object>
412+ <packing>
413+ <property name="position">1</property>
414+ </packing>
415+ </child>
416+ <child type="tab">
417+ <object class="GtkLabel" id="label2">
418+ <property name="visible">True</property>
419+ <property name="can_focus">False</property>
420+ <property name="label" translatable="yes">page 2</property>
421+ </object>
422+ <packing>
423+ <property name="position">1</property>
424+ <property name="tab_fill">False</property>
425+ </packing>
426+ </child>
427+ <child>
428+ <object class="GtkAspectFrame" id="aspectframe1">
429+ <property name="visible">True</property>
430+ <property name="can_focus">False</property>
431+ <property name="label_xalign">0</property>
432+ <property name="shadow_type">none</property>
433+ <child>
434+ <object class="GtkSpinner" id="spinner_connect">
435+ <property name="height_request">32</property>
436+ <property name="visible">True</property>
437+ <property name="can_focus">False</property>
438+ </object>
439+ </child>
440+ </object>
441+ <packing>
442+ <property name="position">2</property>
443+ </packing>
444+ </child>
445+ <child type="tab">
446+ <object class="GtkLabel" id="label3">
447+ <property name="visible">True</property>
448+ <property name="can_focus">False</property>
449+ <property name="label" translatable="yes">page 3</property>
450+ </object>
451+ <packing>
452+ <property name="position">2</property>
453+ <property name="tab_fill">False</property>
454+ </packing>
455+ </child>
456+ </object>
457+ <packing>
458+ <property name="expand">False</property>
459+ <property name="fill">True</property>
460+ <property name="position">0</property>
461+ </packing>
462+ </child>
463+ <child>
464+ <object class="GtkLabel" id="label_global_error">
465+ <property name="visible">True</property>
466+ <property name="can_focus">False</property>
467+ <property name="xalign">0</property>
468+ <property name="xpad">12</property>
469+ <property name="ypad">12</property>
470+ <property name="label" translatable="yes">error label</property>
471+ </object>
472+ <packing>
473+ <property name="expand">False</property>
474+ <property name="fill">True</property>
475+ <property name="position">1</property>
476+ </packing>
477+ </child>
478+ <child>
479+ <placeholder/>
480+ </child>
481+ </object>
482+</interface>
483
484=== added file 'plugin-viewer-gtk.py'
485--- plugin-viewer-gtk.py 1970-01-01 00:00:00 +0000
486+++ plugin-viewer-gtk.py 2012-12-04 15:21:26 +0000
487@@ -0,0 +1,87 @@
488+#!/usr/bin/python3
489+
490+import os
491+import sys
492+
493+from gi.repository import Gtk
494+
495+# we could use this as the base for the MockController as well
496+# from ubiquity.frontend.base import Controller
497+
498+
499+class MockController(object):
500+
501+ def __init__(self, parent):
502+ self.parent = parent
503+ self.oem_user_config = None
504+ self.oem_config = None
505+ self.dbfilter = None
506+ self._allow_go_foward = True
507+ self._allow_go_backward = True
508+
509+ def go_forward(self):
510+ self.parent.button_next.clicked()
511+
512+ def get_string(self, s, lang):
513+ return "get_string: %s (lang=%s)" % (s, lang)
514+
515+ def add_builder(self, builder):
516+ pass
517+
518+ def allow_go_forward(self, v):
519+ self._allow_go_forward = v
520+ self.parent.button_next.set_sensitive(v)
521+
522+ def allow_go_backward(self, v):
523+ self._allow_go_backward = v
524+ self.parent.button_back.set_sensitive(v)
525+
526+
527+if __name__ == "__main__":
528+ """ Run with:
529+ ./plugin-viewer-gtk.py ubi-ubuntuone
530+ """
531+ def _on_button_next_clicked(button):
532+ stop = page_gtk.plugin_on_next_clicked()
533+ if not stop:
534+ Gtk.main_quit()
535+
536+ # setup env
537+ for envvar, path in (
538+ ("UBIQUITY_PLUGIN_PATH", "./ubiquity/plugins"),
539+ ("UBIQUITY_GLADE", "./gui/gtk")):
540+ if os.path.exists(path):
541+ os.environ[envvar] = path
542+ # ... and then import the plugin_manager
543+ from ubiquity.plugin_manager import load_plugin
544+
545+ plugin_name = sys.argv[1]
546+ plugin_module = load_plugin(plugin_name)
547+
548+ win = Gtk.Window()
549+ win.button_next = Gtk.Button("next")
550+ win.button_back = Gtk.Button("back")
551+
552+ mock_controller = MockController(win)
553+ page_gtk = plugin_module.PageGtk(mock_controller)
554+ page_gtk.plugin_translate("en")
555+
556+ win.button_next.connect(
557+ "clicked", _on_button_next_clicked)
558+ win.button_back.connect(
559+ "clicked", lambda b: page_gtk.plugin_on_back_clicked())
560+
561+ button_box = Gtk.ButtonBox(spacing=12)
562+ button_box.set_layout(Gtk.ButtonBoxStyle.END)
563+ button_box.pack_start(win.button_back, True, True, 6)
564+ button_box.pack_start(win.button_next, True, True, 6)
565+
566+ box = Gtk.VBox()
567+ box.pack_start(page_gtk.page, True, True, 6)
568+ box.pack_start(button_box, True, True, 6)
569+
570+ win.add(box)
571+ win.connect("destroy", Gtk.main_quit)
572+ win.show_all()
573+
574+ Gtk.main()
575
576=== modified file 'scripts/plugininstall.py'
577--- scripts/plugininstall.py 2012-11-22 16:27:21 +0000
578+++ scripts/plugininstall.py 2012-12-04 15:21:26 +0000
579@@ -312,9 +312,8 @@
580
581 self.db.progress('SET', self.end)
582
583- def configure_face(self):
584- PHOTO_PATH = '/var/lib/ubiquity/webcam_photo.png'
585- target_user = self.db.get('passwd/username')
586+ def _get_uid_gid_on_target(self, target_user):
587+ """Helper that gets the uid/gid of the username in the target chroot"""
588 uid = subprocess.Popen(
589 ['chroot', self.target, 'sudo', '-u', target_user, '--',
590 'id', '-u'], stdout=subprocess.PIPE, universal_newlines=True)
591@@ -327,8 +326,14 @@
592 uid = int(uid)
593 gid = int(gid)
594 except ValueError:
595- return
596- if os.path.exists(PHOTO_PATH):
597+ return (None, None)
598+ return uid, gid
599+
600+ def configure_face(self):
601+ PHOTO_PATH = '/var/lib/ubiquity/webcam_photo.png'
602+ target_user = self.db.get('passwd/username')
603+ uid, gid = self._get_uid_gid_on_target(target_user)
604+ if os.path.exists(PHOTO_PATH) and uid and gid:
605 targetpath = self.target_file('home', target_user, '.face')
606 shutil.copy2(PHOTO_PATH, targetpath)
607 os.lchown(targetpath, uid, gid)
608
609=== added file 'scripts/ubuntuone-keyring-helper'
610--- scripts/ubuntuone-keyring-helper 1970-01-01 00:00:00 +0000
611+++ scripts/ubuntuone-keyring-helper 2012-12-04 15:21:26 +0000
612@@ -0,0 +1,31 @@
613+#!/usr/bin/python
614+
615+import os
616+import syslog
617+import sys
618+
619+from gi.repository import GnomeKeyring, GLib
620+
621+if __name__ == "__main__":
622+
623+ if not "DBUS_SESSION_BUS_ADDRESS" in os.environ:
624+ os.environ["DBUS_SESSION_BUS_ADDRESS"] = "autolaunch:"
625+
626+ password = sys.stdin.readline().strip("\n")
627+ token = sys.stdin.readline().strip("\n")
628+
629+ # we create the keyring using the users login password
630+ KEYRING_NAME = "login"
631+ TOKEN_NAME = "Ubuntu one"
632+ res = GnomeKeyring.create_sync(KEYRING_NAME, password)
633+ if res == GnomeKeyring.Result.OK:
634+ res = GnomeKeyring.item_create_sync(
635+ KEYRING_NAME,
636+ GnomeKeyring.ItemType.GENERIC_SECRET,
637+ TOKEN_NAME,
638+ GLib.Array(),
639+ token,
640+ True)
641+ syslog.syslog("failed to create item '%s': %s" % (TOKEN_NAME, res))
642+ else:
643+ syslog.syslog("failed to create keyring '%s': %s" % (KEYRING_NAME, res))
644
645=== added file 'tests/test_ubi_ubuntuone.py'
646--- tests/test_ubi_ubuntuone.py 1970-01-01 00:00:00 +0000
647+++ tests/test_ubi_ubuntuone.py 2012-12-04 15:21:26 +0000
648@@ -0,0 +1,144 @@
649+#!/usr/bin/python3
650+
651+import tempfile
652+import unittest
653+
654+import mock
655+from gi.repository import Gtk, GObject, GLib
656+
657+from ubiquity import plugin_manager
658+
659+
660+ubi_ubuntuone = plugin_manager.load_plugin('ubi-ubuntuone')
661+
662+
663+class BaseTestPageGtk(unittest.TestCase):
664+
665+ def setUp(self):
666+ mock_controller = mock.Mock()
667+ self.page = ubi_ubuntuone.PageGtk(mock_controller, ui=mock.Mock())
668+
669+
670+class TestPageGtk(BaseTestPageGtk):
671+
672+ def test_ui_visible(self):
673+ self.page.plugin_get_current_page()
674+ self.assertTrue(self.page.entry_email.get_property("visible"))
675+
676+ def test_init_ui(self):
677+ self.page.plugin_get_current_page()
678+ self.assertEqual(
679+ self.page.notebook_main.get_current_page(),
680+ ubi_ubuntuone.PAGE_REGISTER)
681+
682+ def test_switch_pages(self):
683+ self.page.plugin_get_current_page()
684+ self.page.linkbutton_have_account.clicked()
685+ self.assertEqual(
686+ self.page.notebook_main.get_current_page(),
687+ ubi_ubuntuone.PAGE_LOGIN)
688+ self.page.linkbutton_need_account.clicked()
689+ self.assertEqual(
690+ self.page.notebook_main.get_current_page(),
691+ ubi_ubuntuone.PAGE_REGISTER)
692+
693+ def test_verify_email_entry(self):
694+ self.assertFalse(self.page._verify_email_entry("meep"))
695+ self.assertTrue(self.page._verify_email_entry("mup@example.com"))
696+
697+ def test_verify_password_entry(self):
698+ self.assertFalse(self.page._verify_password_entry(""))
699+ self.assertTrue(self.page._verify_password_entry("xxx"))
700+
701+
702+class MockSSOTestCase(BaseTestPageGtk):
703+
704+ class MockUbuntuSSO():
705+
706+ TOKEN = "{'token': 'nonex'}"
707+
708+ def mock_done(self, callback, errback, data):
709+ callback(self.TOKEN, data)
710+ Gtk.main_quit()
711+
712+ def register(self, email, passw, callback, errback, data):
713+ GObject.idle_add(self.mock_done, callback, errback, data)
714+
715+ def login(self, email, passw, callback, errback, data):
716+ GObject.idle_add(self.mock_done, callback, errback, data)
717+
718+ def test_click_next(self):
719+ self.page.ubuntu_sso = self.MockUbuntuSSO()
720+ with mock.patch.object(
721+ self.page, "_create_keyring_and_store_u1_token") as m_create:
722+ self.page.plugin_on_next_clicked()
723+ self.assertEqual(self.page.notebook_main.get_current_page(),
724+ ubi_ubuntuone.PAGE_SPINNER)
725+ m_create.assert_called_with(self.page.ubuntu_sso.TOKEN)
726+
727+
728+class RegisterTestCase(BaseTestPageGtk):
729+
730+ def test_register_allow_go_forward_not_yet(self):
731+ self.page.entry_email.set_text("foo")
732+ self.page.controller.allow_go_forward.assert_called_with(False)
733+
734+ def test_register_allow_go_foward(self):
735+ self.page.entry_email.set_text("foo@bar.com")
736+ self.page.entry_new_password.set_text("pw")
737+ self.page.entry_new_password2.set_text("pw")
738+ self.page.controller.allow_go_forward.assert_called_with(True)
739+
740+
741+class LoginTestCase(BaseTestPageGtk):
742+
743+ def test_login_allow_go_forward_not_yet(self):
744+ self.page.entry_existing_email.set_text("foo")
745+ self.page.entry_existing_password.set_text("pass")
746+ self.page.controller.allow_go_forward.assert_called_with(False)
747+
748+ def test_login_allow_go_foward(self):
749+ self.page.linkbutton_have_account.clicked()
750+ self.page.entry_existing_email.set_text("foo@bar.com")
751+ self.page.entry_existing_password.set_text("pass")
752+ self.page.controller.allow_go_forward.assert_called_with(True)
753+
754+
755+class UbuntuSSOHelperTestCase(unittest.TestCase):
756+
757+ def setUp(self):
758+ self.callback = mock.Mock()
759+ self.callback.side_effect = lambda *args: self.loop.quit()
760+ self.errback = mock.Mock()
761+ self.errback.side_effect = lambda *args: self.loop.quit()
762+ self.loop = GLib.MainLoop(GLib.main_context_default())
763+ self.sso_helper = ubi_ubuntuone.UbuntuSSO()
764+
765+ def test_spawning_error(self):
766+ self.sso_helper.login("foo@example.com", "nopass",
767+ self.callback, self.errback)
768+ self.loop.run()
769+ self.assertTrue(self.errback.called)
770+ self.assertFalse(self.callback.called)
771+
772+ def test_spawning_success(self):
773+ self.sso_helper.BINARY = "/bin/echo"
774+ self.sso_helper.login("foo@example.com", "nopass",
775+ self.callback, self.errback, data="data")
776+ self.loop.run()
777+ self.assertFalse(self.errback.called)
778+ self.assertTrue(self.callback.called)
779+ # ensure stdout is captured and data is also send
780+ self.callback.assert_called_with("--login foo@example.com\n", "data")
781+
782+
783+if __name__ == '__main__':
784+ # run tests in a sourcetree with:
785+ """
786+ UBIQUITY_GLADE=./gui/gtk \
787+ UBIQUITY_PLUGIN_PATH=./ubiquity/plugins/ \
788+ PYTHONPATH=. python3 tests/test_ubi_ubuntuone.py
789+ """
790+ #from test.support import run_unittest
791+ # run_unittest()
792+ unittest.main()
793
794=== modified file 'ubiquity/gtkwidgets.py'
795--- ubiquity/gtkwidgets.py 2012-11-27 05:48:42 +0000
796+++ ubiquity/gtkwidgets.py 2012-12-04 15:21:26 +0000
797@@ -2,7 +2,7 @@
798
799 import cairo
800 from gi.repository import Gtk, Gdk, GObject, Pango
801-from gi.repository import UbiquityWebcam, GdkPixbuf
802+from gi.repository import GdkPixbuf
803
804 from ubiquity import misc
805
806@@ -383,7 +383,11 @@
807 self.photo_label = Gtk.Label('Take a photo:')
808 vb_left.pack_start(self.photo_label, False, False, 0)
809 f = Gtk.Frame()
810+
811+ # import here because mvo is too lazy to build the tree
812+ from gi.repository import UbiquityWebcam
813 self.webcam = UbiquityWebcam.Webcam()
814+
815 self.webcam.connect('image-captured', self.image_captured)
816 f.add(self.webcam)
817 vb_left.pack_start(f, True, True, 0)
818
819=== added file 'ubiquity/plugins/ubi-ubuntuone.py'
820--- ubiquity/plugins/ubi-ubuntuone.py 1970-01-01 00:00:00 +0000
821+++ ubiquity/plugins/ubi-ubuntuone.py 2012-12-04 15:21:26 +0000
822@@ -0,0 +1,378 @@
823+# -*- coding: utf-8; Mode: Python; indent-tabs-mode: nil; tab-width: 4 -*-
824+
825+# Copyright (C) 2012 Canonical Ltd.
826+# Written by Michael Vogt <mvo@ubuntu.com>
827+#
828+# This program is free software; you can redistribute it and/or modify
829+# it under the terms of the GNU General Public License as published by
830+# the Free Software Foundation; either version 2 of the License, or
831+# (at your option) any later version.
832+#
833+# This program is distributed in the hope that it will be useful,
834+# but WITHOUT ANY WARRANTY; without even the implied warranty of
835+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
836+# GNU General Public License for more details.
837+#
838+# You should have received a copy of the GNU General Public License
839+# along with this program; if not, write to the Free Software
840+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
841+
842+import pwd
843+import re
844+import os
845+import os.path
846+import subprocess
847+import shutil
848+import syslog
849+
850+from ubiquity import plugin, misc
851+
852+
853+NAME = 'ubuntuone'
854+AFTER = 'usersetup'
855+WEIGHT = 10
856+
857+(PAGE_REGISTER,
858+ PAGE_LOGIN,
859+ PAGE_SPINNER,
860+ ) = range(3)
861+
862+# TODO:
863+# - network awareness (steal from timezone map page)
864+# - rename this all to ubuntu sso instead of ubuntuone to avoid confusion
865+# that we force people to sign up for payed services on install (?) where
866+# what we want is to make it super simple to use our services
867+# - take the username from the usersetup step when creating the token
868+# - get a design for the UI
869+# * to create a new account
870+# * to login into a existing account
871+# * deal with forgoten passwords
872+# * skip account creation
873+
874+
875+# TESTING end-to-end for real
876+#
877+# * get a raring cdimage
878+# * run:
879+# kvm -m 1500 -hda /path/to/random-image -cdrom /path/to/raring-arch.iso \
880+# -boot d
881+# * in the VM:
882+# - add universe
883+# - sudo apt-get install bzr build-essential python3-setuptools debhelper python3-piston-mini-client
884+# - bzr co lp:~mvo/+junk/cli-sso-login
885+# - (cd cli-sso-login; dpkg-buildpackage; sudo dpkg -i ../python3*.deb)
886+#
887+# - install cli-sso-login from
888+# - bzr co --lightweight lp:~mvo/ubiquity/ssologin
889+# - cd ssologin
890+# - sudo cp ubiquity/plugins/* /usr/lib/ubiquity/plugins
891+# - sudo cp ubiquity/* /usr/lib/ubiquity/ubiquity
892+# - sudo cp gui/gtk//*.ui /usr/share/ubiquity/gtk
893+# - sudo cp scripts/* /usr/share/ubiquity/
894+# - sudo cp bin/ubiquity /usr/bin
895+# - sudo ubiquity
896+
897+
898+class UbuntuSSO(object):
899+
900+ # this will need the helper
901+ # lp:~mvo/+junk/cli-sso-login installed
902+
903+ BINARY = "/usr/bin/ubuntu-sso-cli"
904+
905+ def _child_exited(self, pid, status, data):
906+ stdin_fd, stdout_fd, stderr_fd, callback, errback, user_data = data
907+ exit_code = os.WEXITSTATUS(status)
908+ # the delayed reading will only work if the amount of data is
909+ # small enough to not cause the pipe to block which on most
910+ # system is ok as "ulimit -p" shows 8 pages by default (4k)
911+ stdout = os.read(stdout_fd, 2048).decode("utf-8")
912+ stderr = os.read(stderr_fd, 2048).decode("utf-8")
913+ if exit_code == 0:
914+ callback(stdout, user_data)
915+ else:
916+ errback(stderr, user_data)
917+
918+ def _spawn_sso_helper(self, cmd, password, callback, errback, data):
919+ from gi.repository import GLib
920+ res, pid, stdin_fd, stdout_fd, stderr_fd = GLib.spawn_async_with_pipes(
921+ "/", cmd, None,
922+ (GLib.SpawnFlags.LEAVE_DESCRIPTORS_OPEN |
923+ GLib.SpawnFlags.DO_NOT_REAP_CHILD), None, None)
924+ if res:
925+ os.write(stdin_fd, password.encode("utf-8"))
926+ os.write(stdin_fd, "\n".encode("utf-8"))
927+ GLib.child_watch_add(
928+ GLib.PRIORITY_DEFAULT, pid, self._child_exited,
929+ (stdin_fd, stdout_fd, stderr_fd, callback, errback, data))
930+ else:
931+ errback("Failed to spawn %s" % cmd, data)
932+
933+ def login(self, email, password, callback, errback, data=None):
934+ cmd = [self.BINARY, "--login", email]
935+ self._spawn_sso_helper(cmd, password, callback, errback, data)
936+
937+ def register(self, email, password, callback, errback, data=None):
938+ cmd = [self.BINARY, "--register", email]
939+ self._spawn_sso_helper(cmd, password, callback, errback, data)
940+
941+
942+class Page(plugin.Plugin):
943+
944+ def prepare(self, unfiltered=False):
945+ self.ui._user_password = self.db.get('passwd/user-password')
946+ return plugin.Plugin.prepare(unfiltered)
947+
948+
949+class PageGtk(plugin.PluginUI):
950+ plugin_title = 'ubiquity/text/ubuntuone_heading_label'
951+
952+ def __init__(self, controller, *args, **kwargs):
953+ from gi.repository import Gtk
954+ self.controller = controller
955+ # check if we are needed at all
956+ if ('UBIQUITY_AUTOMATIC' in os.environ or
957+ 'UBIQUITY_NO_SSO' in os.environ):
958+ self.page = None
959+ return
960+ # check dependencies
961+ try:
962+ from gi.repository import GnomeKeyring
963+ assert(GnomeKeyring)
964+ except ImportError as e:
965+ syslog.syslog("skipping SSO page, no GnomeKeyring (%s)" % e)
966+ self.page = None
967+ return
968+ # add builder/signals
969+ builder = Gtk.Builder()
970+ self.controller.add_builder(builder)
971+ builder.add_from_file(
972+ os.path.join(os.environ['UBIQUITY_GLADE'], 'stepUbuntuOne.ui'))
973+ builder.connect_signals(self)
974+ # make the widgets available under their gtkbuilder name
975+ for obj in builder.get_objects():
976+ if issubclass(type(obj), Gtk.Buildable):
977+ setattr(self, Gtk.Buildable.get_name(obj), obj)
978+ self.page = builder.get_object('stepUbuntuOne')
979+ self.notebook_main.set_show_tabs(False)
980+ self.plugin_widgets = self.page
981+ self.oauth_token = None
982+ self.skip_step = False
983+ self.online = False
984+ self.label_global_error.set_text("")
985+ # the worker
986+ self.ubuntu_sso = UbuntuSSO()
987+ self.info_loop(None)
988+
989+ def plugin_set_online_state(self, state):
990+ self.online = state
991+
992+ def plugin_get_current_page(self):
993+ self.page.show_all()
994+ self.notebook_main.set_current_page(PAGE_REGISTER)
995+ return self.page
996+
997+ def plugin_on_back_clicked(self):
998+ # stop whatever needs stopping
999+ return False
1000+
1001+ def plugin_on_next_clicked(self):
1002+ from gi.repository import Gtk
1003+ if self.skip_step:
1004+ return False
1005+ if self.notebook_main.get_current_page() == PAGE_REGISTER:
1006+ self.ubuntu_sso.register(self.entry_email.get_text(),
1007+ self.entry_new_password.get_text(),
1008+ callback=self._ubuntu_sso_callback,
1009+ errback=self._ubuntu_sso_errback,
1010+ data=PAGE_REGISTER)
1011+ elif self.notebook_main.get_current_page() == PAGE_LOGIN:
1012+ self.ubuntu_sso.login(self.entry_existing_email.get_text(),
1013+ self.entry_existing_password.get_text(),
1014+ callback=self._ubuntu_sso_callback,
1015+ errback=self._ubuntu_sso_errback,
1016+ data=PAGE_LOGIN)
1017+ else:
1018+ raise AssertionError("Should never be reached happen")
1019+
1020+ self.notebook_main.set_current_page(PAGE_SPINNER)
1021+ self.spinner_connect.start()
1022+ # the ubuntu_sso.{login,register} will stop this loop when its done
1023+ Gtk.main()
1024+ self.spinner_connect.stop()
1025+
1026+ # if there is no token at this point, there is a error,
1027+ # so stop moving forward
1028+ if self.oauth_token is None:
1029+ return True
1030+
1031+ # all good, create a (encrypted) keyring and store the token for later
1032+ self._create_keyring_and_store_u1_token(self.oauth_token)
1033+ return False
1034+
1035+ def _create_keyring_and_store_u1_token(self, token):
1036+ """Helper that spawns a external helper to create the keyring"""
1037+ # this needs to be a external helper as ubiquity is running as
1038+ # root and it seems that anything other than "drop_all_privileges"
1039+ # will not trigger the correct dbus activation for the
1040+ # gnome-keyring daemon
1041+ #
1042+ # mvo: We could do this in the "install" phase too, but more fragile
1043+ # I think, here is what would be required:
1044+ # - copy over XAUTHORITY to /target/home/$targetuser/.Xauthority
1045+ # - chown $targetuser.$targetuser \
1046+ # /target/home/$targetuser/.Xauthority
1047+ # - (bind)mount /proc in /target
1048+ # - run "dbus-uuidgen --ensure" in /target to get a dbus
1049+ # machine-id
1050+ # - run the helper with:
1051+ # chroot /target sudo -u $targetuser HOME=/home/$targetuser \
1052+ # XAUTHORITY=/home/$targetuser/.Xauthority \
1053+ # DBUS_SESSION_BUS_ADDRESS="autolaunch:" \
1054+ # ubuntuone-keyring-helper
1055+ # - ensure that the dbus-daemon and gnome-keyring-daemon that
1056+ # get spawned in /target get killed so that /target can
1057+ # get unmounted again
1058+ # - umount /proc
1059+ p = subprocess.Popen(
1060+ ["/usr/share/ubiquity/ubuntuone-keyring-helper"],
1061+ stdin=subprocess.PIPE,
1062+ preexec_fn=misc.drop_all_privileges,
1063+ universal_newlines=True)
1064+ p.stdin.write(self._user_password)
1065+ p.stdin.write("\n")
1066+ p.stdin.write(token)
1067+ p.stdin.write("\n")
1068+ res = p.wait()
1069+ syslog.syslog("keyring helper returned %s" % res)
1070+
1071+ def plugin_translate(self, lang):
1072+ pasw = self.controller.get_string('password_inactive_label', lang)
1073+ self.entry_new_password.set_placeholder_text(pasw)
1074+ self.entry_existing_password.set_placeholder_text(pasw)
1075+ pasw_again = self.controller.get_string(
1076+ 'password_again_inactive_label', lang)
1077+ self.entry_new_password2.set_placeholder_text(pasw_again)
1078+ email_p = self.controller.get_string('email_inactive_label', lang)
1079+ self.entry_email.set_placeholder_text(email_p)
1080+ self.entry_existing_email.set_placeholder_text(email_p)
1081+ # error messages
1082+ self._error_register = self.controller.get_string(
1083+ 'error_register', lang)
1084+ self._error_login = self.controller.get_string(
1085+ 'error_login', lang)
1086+
1087+ # callbacks
1088+ def _ubuntu_sso_callback(self, oauth_token, data):
1089+ """Called when a oauth token was returned successfully"""
1090+ from gi.repository import Gtk
1091+ self.oauth_token = oauth_token
1092+ Gtk.main_quit()
1093+
1094+ def _ubuntu_sso_errback(self, error, data):
1095+ """Called when a error acquiring the oauth token from the helper"""
1096+ from gi.repository import Gtk
1097+ syslog.syslog("ubuntu sso failed: '%s'" % error)
1098+ self.notebook_main.set_current_page(data)
1099+ if data == PAGE_REGISTER:
1100+ err = self._error_register
1101+ else:
1102+ err = self._error_login
1103+ self.label_global_error.set_markup("<b><big>%s</big></b>" % err)
1104+ Gtk.main_quit()
1105+
1106+ # signals
1107+ def on_button_have_account_clicked(self, button):
1108+ self.notebook_main.set_current_page(PAGE_LOGIN)
1109+
1110+ def on_button_need_account_clicked(self, button):
1111+ self.notebook_main.set_current_page(PAGE_REGISTER)
1112+
1113+ def on_button_skip_account_clicked(self, button):
1114+ self.oauth_token = None
1115+ self.skip_step = True
1116+ self.controller.go_forward()
1117+
1118+ def _verify_email_entry(self, email):
1119+ """Return True if the email address looks valid"""
1120+ EMAIL_REGEXP = "[a-zA-Z]+@[a-zA-Z]+\.[a-zA-Z]+"
1121+ match = re.match(EMAIL_REGEXP, email)
1122+ return (match is not None)
1123+
1124+ def _verify_password_entry(self, password):
1125+ """Return True if there is a valid password"""
1126+ return len(password) > 0
1127+
1128+ def info_loop(self, widget):
1129+ """Run each time the user inputs something to make controlls
1130+ sensitive or insensitive
1131+ """
1132+ complete = False
1133+ if self.notebook_main.get_current_page() == PAGE_REGISTER:
1134+ email = self.entry_email.get_text()
1135+ password = self.entry_new_password.get_text()
1136+ password2 = self.entry_new_password2.get_text()
1137+ complete = (self._verify_email_entry(email) and
1138+ len(password) > 0 and
1139+ (password == password2))
1140+ elif self.notebook_main.get_current_page() == PAGE_LOGIN:
1141+ email = self.entry_existing_email.get_text()
1142+ password = self.entry_existing_password.get_text()
1143+ complete = (self._verify_email_entry(email) and
1144+ self._verify_password_entry(password))
1145+ self.controller.allow_go_forward(complete)
1146+
1147+
1148+class Install(plugin.InstallPlugin):
1149+
1150+ def install(self, target, progress, *args, **kwargs):
1151+ self.configure_oauth_token(target)
1152+
1153+ def _get_target_uid(self, target_path, target_user):
1154+ # stolen from: plugininstall.py, is there a better way?
1155+ p = subprocess.Popen(
1156+ ['chroot', target_path, 'sudo', '-u', target_user, '--',
1157+ 'id', '-u'], stdout=subprocess.PIPE, universal_newlines=True)
1158+ uid = int(p.communicate()[0].strip('\n'))
1159+ return uid
1160+
1161+ def _get_casper_user_keyring_file_path(self):
1162+ # stolen (again) from pluginstall.py
1163+ try:
1164+ casper_user = pwd.getpwuid(999).pw_name
1165+ except KeyError:
1166+ # We're on a weird system where the casper user isn't uid 999
1167+ # just stop there
1168+ return ""
1169+ casper_user_home = os.path.expanduser('~%s' % casper_user)
1170+ keyring_file = os.path.join(casper_user_home, ".local", "share",
1171+ "keyrings", "login.keyring")
1172+ return keyring_file
1173+
1174+ # XXX: I am untested
1175+ def configure_oauth_token(self, target):
1176+ target_user = self.db.get('passwd/username')
1177+ uid = self._get_target_uid(target, target_user)
1178+ keyring_file = self._get_casper_user_keyring_file_path()
1179+ if os.path.exists(keyring_file) and uid:
1180+ targetpath = os.path.join(
1181+ target, 'home', target_user, '.local', 'share', 'keyrings',
1182+ 'login.keyring')
1183+ # skip copy if the target already exists, this can happen
1184+ # if e.g. the user selected reinstall-with-keep-home
1185+ if os.path.exists(targetpath):
1186+ syslog.syslog("keyring path: '%s' already exists, skip copy" %
1187+ targetpath)
1188+ return
1189+ basedir = os.path.dirname(targetpath)
1190+ # ensure we have the basedir with the righ permissions
1191+ if not os.path.exists(basedir):
1192+ basedir_in_chroot = os.path.join(
1193+ "home", target_user, ".local", "share", "keyrings")
1194+ subprocess.call(
1195+ ["chroot", target, "sudo", "-u", target_user, "--",
1196+ "mkdir", "-p", basedir_in_chroot])
1197+ shutil.copy2(self.KEYRING_FILE, targetpath)
1198+ os.lchown(targetpath, uid, uid)
1199+ os.chmod(targetpath, 0o600)
1200+ os.chmod(basedir, 0o700)

Subscribers

People subscribed via source and target branches

to status/vote changes: