Merge lp:~kissiel/checkbox/password-dialog into lp:checkbox

Proposed by Maciej Kisielewski
Status: Merged
Approved by: Zygmunt Krynicki
Approved revision: 3597
Merged at revision: 3768
Proposed branch: lp:~kissiel/checkbox/password-dialog
Merge into: lp:checkbox
Diff against target: 473 lines (+358/-24)
5 files modified
checkbox-touch/components/CheckboxTouchApplication.qml (+5/-0)
checkbox-touch/components/PasswordDialog.qml (+88/-0)
checkbox-touch/main.qml (+51/-22)
checkbox-touch/py/checkbox_touch.py (+30/-2)
checkbox-touch/py/sudo_with_pass_ctrl.py (+184/-0)
To merge this branch: bzr merge lp:~kissiel/checkbox/password-dialog
Reviewer Review Type Date Requested Status
Zygmunt Krynicki (community) Approve
Review via email: mp+249700@code.launchpad.net

Description of the change

This MR teaches Checkbox-Touch how to run jobs as different user (root usually)

Sequence of running jobs as different user in CBT is as follows:
When Checkbox-Touch session is started, CheckboxTouchApplication instance is
given list of controllers that can be used for job execution. One of the
controllers is TouchRootViaSudoExecutionController. To instantiate this
controller one must provide callable that will be used when executing sudo.
The reason we're defferring password polling is that we don't yet know if
password will be needed (for instance all selected jobs run as current user).
Once the session is started, Checkbox-Touch checks every job if job requires
user change. If that's the case, it displays QML password prompt and sends
entered password to CheckboxTouchApplication instance, so the
password-returning callable can provide controller with the pass.
It's crucial that password is stored in CheckboxTouchApplication prior to
execution of any sudo-related job.

d8fd76d checkbox-touch: make checkbox-touch ask for password
09258d7 checkbox-touch: add rememberPassword to App component on QML side
8ca815e checkbox-touch: use custom execution controllers
125b4c9 checkbox-touch: add storing of password in CheckboxTouchApplication
c1eba56 checkbox-touch: add PasswordDialog
a08900e checkbox-touch: add RootViaSudoWithPassExecutionController
2355c3d checkbox-touch: move perform*Test calls to distinctive function
8437551 checkbox-touch: add job's user field to 'test' model

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

On piątek, 13 lutego 2015 19:45:30 CET, Maciej Kisielewski wrote:
> Maciej Kisielewski has proposed merging
> lp:~kissiel/checkbox/password-dialog into lp:checkbox.
>
> Requested reviews:
> Checkbox Developers (checkbox-dev)
>
> For more details, see:
> https://code.launchpad.net/~kissiel/checkbox/password-dialog/+merge/249700
>
> This MR teaches Checkbox-Touch how to run jobs as different
> user (root usually)
>
> Sequence of running jobs as different user in CBT is as follows:
> When Checkbox-Touch session is started,
> CheckboxTouchApplication instance is
> given list of controllers that can be used for job execution. One of the
> controllers is TouchRootViaSudoExecutionController. To instantiate this
> controller one must provide callable that will be used when executing sudo.
> The reason we're defferring password polling is that we don't yet know if
> password will be needed (for instance all selected jobs run as
> current user).

There is an existing, better mechanism for doing this, namely voting. Keep
this execution controller focused on just running root jobs

> Once the session is started, Checkbox-Touch checks every job if
> job requires
> user change. If that's the case, it displays QML password prompt and sends
> entered password to CheckboxTouchApplication instance, so the
> password-returning callable can provide controller with the pass.
> It's crucial that password is stored in CheckboxTouchApplication prior to
> execution of any sudo-related job.
>
> d8fd76d checkbox-touch: make checkbox-touch ask for password
> 09258d7 checkbox-touch: add rememberPassword to App component on QML side
> 8ca815e checkbox-touch: use custom execution controllers
> 125b4c9 checkbox-touch: add storing of password in CheckboxTouchApplication
> c1eba56 checkbox-touch: add PasswordDialog
> a08900e checkbox-touch: add RootViaSudoWithPassExecutionController
> 2355c3d checkbox-touch: move perform*Test calls to distinctive function
> 8437551 checkbox-touch: add job's user field to 'test' model
>
Ill read the rest later

Thanks for posting this
ZK

--
Sent using Dekko from my Ubuntu device

Revision history for this message
Zygmunt Krynicki (zyga) wrote :

Hey. This looks good. I only have one real comment below.

And I'm sorry for keeping this lingering so long.

review: Needs Fixing
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Options changed to long versions. Added comment for uncommon options. Repushed.

Revision history for this message
Zygmunt Krynicki (zyga) wrote :

+1

Thanks for pinging me. I don't see anything that would hold it anymore now.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'checkbox-touch/components/CheckboxTouchApplication.qml'
2--- checkbox-touch/components/CheckboxTouchApplication.qml 2014-11-27 22:31:39 +0000
3+++ checkbox-touch/components/CheckboxTouchApplication.qml 2015-03-10 12:19:50 +0000
4@@ -146,6 +146,11 @@
5 });
6 }
7
8+ function rememberPassword(password, continuation) {
9+ // using low-level py.call() to 'silently' pass password string through pyotherside
10+ py.call("py_invoke", [handle, "remember_password", [password]], continuation);
11+ }
12+
13 // A wrapper around invoke() that works with the @view decorator. The fn_ok
14 // and fn_err are called on a normal reply and on error, respectively.
15 function request(name, args, fn_ok, fn_err) {
16
17=== added file 'checkbox-touch/components/PasswordDialog.qml'
18--- checkbox-touch/components/PasswordDialog.qml 1970-01-01 00:00:00 +0000
19+++ checkbox-touch/components/PasswordDialog.qml 2015-03-10 12:19:50 +0000
20@@ -0,0 +1,88 @@
21+/*
22+ * This file is part of Checkbox
23+ *
24+ * Copyright 2015 Canonical Ltd.
25+ *
26+ * Authors:
27+ * - Maciej Kisielewski <maciej.kisielewski@canonical.com>
28+ *
29+ * This program is free software; you can redistribute it and/or modify
30+ * it under the terms of the GNU General Public License as published by
31+ * the Free Software Foundation; version 3.
32+ *
33+ * This program is distributed in the hope that it will be useful,
34+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
35+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36+ * GNU General Public License for more details.
37+ *
38+ * You should have received a copy of the GNU General Public License
39+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
40+ */
41+import QtQuick 2.0
42+import Ubuntu.Components 1.1
43+import Ubuntu.Components.Popups 0.1
44+
45+/*! \brief Password prompt dialog.
46+ \inherits Item
47+
48+ This component is a prompt for user's password.
49+*/
50+Item {
51+ id: passwordDialog
52+
53+ /*!
54+ This alias aids the process of popping up the dialog.
55+ Usage: PopupUtils.open(passwordDialog.dialogComponent);
56+ */
57+ property alias dialogComponent: component
58+
59+ signal passwordEntered(string password)
60+ signal dialogCancelled
61+
62+ Component {
63+ id: component
64+
65+ Dialog {
66+ id: dialog
67+ title: i18n.tr("Enter password")
68+
69+ modal: true
70+
71+ TextField {
72+ id: passwordBox
73+ placeholderText: i18n.tr("password")
74+ echoMode: TextInput.Password
75+ onAccepted: okButton.clicked(text)
76+ }
77+
78+ Button {
79+ id: okButton
80+ text: i18n.tr("OK")
81+ color: UbuntuColors.green
82+ onClicked: {
83+ PopupUtils.close(dialog);
84+ // once qml pam authentication goes live, we might want to
85+ // check if the password is correct in some QML-ish manner
86+ passwordEntered(passwordBox.text);
87+ passwordBox.text = "";
88+ }
89+ }
90+
91+ Button {
92+ text: i18n.tr("Cancel")
93+ color: UbuntuColors.red
94+ onClicked: {
95+ passwordBox.text = "";
96+ PopupUtils.close(dialog);
97+ dialogCancelled();
98+ }
99+ }
100+
101+ Component.onCompleted: {
102+ // let user type in password without tapping on
103+ // the text field
104+ passwordBox.forceActiveFocus();
105+ }
106+ }
107+ }
108+}
109
110=== modified file 'checkbox-touch/main.qml'
111--- checkbox-touch/main.qml 2015-02-09 14:19:48 +0000
112+++ checkbox-touch/main.qml 2015-03-10 12:19:50 +0000
113@@ -21,6 +21,7 @@
114 */
115 import QtQuick 2.0
116 import Ubuntu.Components 1.1
117+import Ubuntu.Components.Popups 0.1
118 import QtQuick.Layouts 1.1
119 import io.thp.pyotherside 1.2
120 import "components"
121@@ -302,6 +303,11 @@
122 });
123 }
124 }
125+
126+ PasswordDialog {
127+ id: passwordDialog
128+ }
129+
130 function resumeOrStartSession() {
131 app.isSessionResumable(function(result) {
132 if(result.resumable === true) {
133@@ -325,32 +331,55 @@
134 if (test.plugin === undefined) {
135 return showResultsScreen();
136 }
137- switch (test['plugin']) {
138- case 'manual':
139- performManualTest(test);
140- break;
141- case 'user-interact-verify':
142- performUserInteractVerifyTest(test);
143- break;
144- case 'shell':
145- performAutomatedTest(test);
146- break;
147- case 'user-verify':
148- performUserVerifyTest(test);
149- break;
150- case 'user-interact':
151- performUserInteractTest(test);
152- break;
153- case 'qml':
154- performQmlTest(test);
155- break;
156- default:
157- test.outcome = "skip";
158- completeTest(test);
159+ if (test.user) {
160+ // running this test will require to be run as a different
161+ // user (therefore requiring user to enter sudo password)
162+ if (!appSettings.sudoPasswordProvided) {
163+ // ask user for password
164+ passwordDialog.passwordEntered.connect(function(pass) {
165+ app.rememberPassword(pass, function(){
166+ appSettings.sudoPasswordProvided = true;
167+ performTest(test);
168+ });
169+ });
170+ passwordDialog.dialogCancelled.connect(function() {
171+ test.outcome = "skip";
172+ completeTest(test);
173+ });
174+ PopupUtils.open(passwordDialog.dialogComponent);
175+ return;
176+ }
177 }
178+ performTest(test);
179 });
180 }
181
182+ function performTest(test) {
183+ switch (test['plugin']) {
184+ case 'manual':
185+ performManualTest(test);
186+ break;
187+ case 'user-interact-verify':
188+ performUserInteractVerifyTest(test);
189+ break;
190+ case 'shell':
191+ performAutomatedTest(test);
192+ break;
193+ case 'user-verify':
194+ performUserVerifyTest(test);
195+ break;
196+ case 'user-interact':
197+ performUserInteractTest(test);
198+ break;
199+ case 'qml':
200+ performQmlTest(test);
201+ break;
202+ default:
203+ test.outcome = "skip";
204+ completeTest(test);
205+ }
206+ }
207+
208 function completeTest(test) {
209 app.registerTestResult(test, processNextTest);
210 }
211
212=== modified file 'checkbox-touch/py/checkbox_touch.py'
213--- checkbox-touch/py/checkbox_touch.py 2015-02-12 08:24:52 +0000
214+++ checkbox-touch/py/checkbox_touch.py 2015-03-10 12:19:50 +0000
215@@ -1,6 +1,6 @@
216 # This file is part of Checkbox.
217 #
218-# Copyright 2014 Canonical Ltd.
219+# Copyright 2014-2015 Canonical Ltd.
220 # Written by:
221 # Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
222 # Maciej Kisielewski <maciej.kisielewski@canonical.com>
223@@ -337,6 +337,7 @@
224 self.session_storage_repo = None
225 self.timestamp = datetime.datetime.utcnow().isoformat()
226 self.config = None
227+ self._password = None
228
229 def __repr__(self):
230 return "app"
231@@ -369,11 +370,22 @@
232 # Checkpoint the session so that we have something to see
233 self._checkpoint()
234 self.config = PlainBoxConfig()
235+
236+ # Prepare custom execution controller list
237+ from plainbox.impl.ctrl import UserJobExecutionController
238+ from sudo_with_pass_ctrl import \
239+ RootViaSudoWithPassExecutionController
240+ controllers = [
241+ RootViaSudoWithPassExecutionController(
242+ self.context.provider_list, self._password_provider),
243+ UserJobExecutionController(self.context.provider_list),
244+ ]
245 self.runner = JobRunner(
246 self.manager.storage.location,
247 self.context.provider_list,
248 # TODO: tie this with well-known-dirs helper
249- os.path.join(self.manager.storage.location, 'io-logs'))
250+ os.path.join(self.manager.storage.location, 'io-logs'),
251+ execution_ctrl_list=controllers)
252 app_cache_dir = self._get_app_cache_directory()
253 if not os.path.exists(app_cache_dir):
254 os.makedirs(app_cache_dir)
255@@ -604,6 +616,7 @@
256 job.tr_verification() is not None else description,
257 "plugin": job.plugin,
258 "id": job.id,
259+ "user": job.user,
260 "qml_file": job.qml_file,
261 "start_time": time.time(),
262 "test_number": self.index,
263@@ -783,6 +796,21 @@
264 provider_list.append(get_categories())
265 return provider_list
266
267+ def _password_provider(self):
268+ if self._password is None:
269+ raise RuntimeError("execute_job called without providing password"
270+ " first")
271+ return self._password
272+
273+ def remember_password(self, password):
274+ """
275+ Save password in app instance
276+
277+ It deliberately doesn't use view decorator to omit all logging that
278+ might happen
279+ """
280+ self._password = password
281+
282
283 def bootstrap():
284 logging.basicConfig(level=logging.INFO, stream=sys.stderr)
285
286=== added file 'checkbox-touch/py/sudo_with_pass_ctrl.py'
287--- checkbox-touch/py/sudo_with_pass_ctrl.py 1970-01-01 00:00:00 +0000
288+++ checkbox-touch/py/sudo_with_pass_ctrl.py 2015-03-10 12:19:50 +0000
289@@ -0,0 +1,184 @@
290+# This file is part of Checkbox.
291+#
292+# Copyright 2015 Canonical Ltd.
293+# Written by:
294+# Maciej Kisielewski <maciej.kisielewski@canonical.com>
295+#
296+# Checkbox is free software: you can redistribute it and/or modify
297+# it under the terms of the GNU General Public License version 3,
298+# as published by the Free Software Foundation.
299+#
300+# Checkbox is distributed in the hope that it will be useful,
301+# but WITHOUT ANY WARRANTY; without even the implied warranty of
302+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
303+# GNU General Public License for more details.
304+#
305+# You should have received a copy of the GNU General Public License
306+# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
307+"""
308+:mod:`sudo_with_pass_ctrl` -- Self Providing Sudo Execution Controller
309+======================================================================
310+
311+This module defines an Execution Controller capable of running plainbox' jobs
312+as the user supplied in job definition. It does so by launching `sudo -S (...)`
313+in a subprocess and writing password to the subprocess' stdin.
314+"""
315+try:
316+ import grp
317+except ImportError:
318+ grp = None
319+import logging
320+import os
321+try:
322+ import posix
323+except ImportError:
324+ posix = None
325+import subprocess
326+
327+from plainbox.i18n import gettext as _
328+from plainbox.impl.ctrl import CheckBoxDifferentialExecutionController
329+
330+logger = logging.getLogger("checkbox_touch.sudo_with_pass_ctrl")
331+
332+
333+class RootViaSudoWithPassExecutionController(
334+ CheckBoxDifferentialExecutionController):
335+ """
336+ Execution controller that gains root by using sudo, automatically
337+ supplying password to the sudo process.
338+
339+ This controller should be used for jobs that need root in front-ends that
340+ are not CLI based.
341+ """
342+
343+ def __init__(self, provider_list, password_provider_cls):
344+ """
345+ Initialize a new RootViaSudoWithPassExecutionController
346+
347+ :param provider_list:
348+ A list of Provider1 objects that will be available for script
349+ dependency resolutions. Currently all of the scripts are makedirs
350+ available but this will be refined to the minimal set later.
351+ :param password_provider_cls:
352+ A callable that will be used to obtain user's password.
353+ It is called when the controller runs a sudo command.
354+ """
355+ super().__init__(provider_list)
356+ try:
357+ in_sudo_group = grp.getgrnam("sudo").gr_gid in posix.getgroups()
358+ except KeyError:
359+ in_sudo_group = False
360+ try:
361+ in_admin_group = grp.getgrnam("admin").gr_gid in posix.getgroups()
362+ except KeyError:
363+ in_admin_group = False
364+ self.user_can_sudo = in_sudo_group or in_admin_group
365+ self._password_provider = password_provider_cls
366+
367+ def get_execution_command(self, job, job_state, config, session_dir,
368+ nest_dir):
369+ """
370+ Get the command to execute the specified job
371+
372+ :param job:
373+ job definition with the command and environment definitions
374+ :param job_state:
375+ The JobState associated to the job to execute.
376+ :param config:
377+ A PlainBoxConfig instance which can be used to load missing
378+ environment definitions that apply to all jobs. It is used to
379+ provide values for missing environment variables that are required
380+ by the job (as expressed by the environ key in the job definition
381+ file).
382+ :param session_dir:
383+ Base directory of the session this job will execute in.
384+ This directory is used to co-locate some data that is unique to
385+ this execution as well as data that is shared by all executions.
386+ :param nest_dir:
387+ A directory with a nest of symlinks to all executables required to
388+ execute the specified job. This argument may or may not be used,
389+ depending on how PATH is passed to the command (via environment or
390+ via the commant line)
391+ :returns:
392+ List of command arguments
393+ """
394+ # Run env(1) as the required user
395+ # --prompt is used to set sudo promp to an empty string so we don't
396+ # pollute command output
397+ # --reset-timestamp makes sudo ask for password even if it has been
398+ # supplied recently
399+ cmd = ['sudo', '--prompt=', '--reset-timestamp', '--stdin',
400+ '--user', job.user, 'env']
401+ # Append all environment data
402+ env = self.get_differential_execution_environment(
403+ job, job_state, config, session_dir, nest_dir)
404+ cmd += ["{key}={value}".format(key=key, value=value)
405+ for key, value in sorted(env.items())]
406+ # Lastly use job.shell -c, to run our command
407+ cmd += [job.shell, '-c', job.command]
408+ return cmd
409+
410+ def execute_job(self, job, job_state, config, session_dir, extcmd_popen):
411+ """
412+ Execute the job using standard subprocess.Popen
413+
414+ :param job:
415+ The JobDefinition to execute
416+ :param job_state:
417+ The JobState associated to the job to execute.
418+ :param config:
419+ A PlainBoxConfig instance which can be used to load missing
420+ environment definitions that apply to all jobs. It is used to
421+ provide values for missing environment variables that are required
422+ by the job (as expressed by the environ key in the job definition
423+ file).
424+ :param session_dir:
425+ Base directory of the session this job will execute in.
426+ This directory is used to co-locate some data that is unique to
427+ this execution as well as data that is shared by all executions.
428+ :param extcmd_popen:
429+ A subprocess.Popen like object - ignored
430+ :returns:
431+ The return code of the command, as returned by subprocess.call()
432+
433+ The reason behind not using extcmd_popen is that it doesn't support
434+ connecting pipe to stdin of the process it spawns. And this is required
435+ for running 'sudo -S'.
436+ """
437+ if not os.path.isdir(self.get_CHECKBOX_DATA(session_dir)):
438+ os.makedirs(self.get_CHECKBOX_DATA(session_dir))
439+ # Setup the executable nest directory
440+ with self.configured_filesystem(job, config) as nest_dir:
441+ # Get the command and the environment.
442+ # of this execution controller
443+ cmd = self.get_execution_command(
444+ job, job_state, config, session_dir, nest_dir)
445+ env = self.get_execution_environment(
446+ job, job_state, config, session_dir, nest_dir)
447+ with self.temporary_cwd(job, config) as cwd_dir:
448+ # run the command
449+ logger.debug(_("job[%s] executing %r with env %r in cwd %r"),
450+ job.id, cmd, env, cwd_dir)
451+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
452+ env=env, cwd=cwd_dir)
453+ # sudo manpage explicitly states that \n should be appended
454+ pass_bytes = bytes(self._password_provider() + '\n', 'UTF-8')
455+ p.communicate(input=pass_bytes)
456+ return_code = p.returncode
457+ if 'noreturn' in job.get_flag_set():
458+ self._halt()
459+ return return_code
460+
461+ def get_checkbox_score(self, job):
462+ """
463+ Compute how applicable this controller is for the specified job.
464+
465+ :returns:
466+ -1 if the job does not have a user override or the user cannot use
467+ sudo and 2 otherwise
468+ """
469+ # Only makes sense with jobs that need to run as another user
470+ if job.user is not None and self.user_can_sudo:
471+ return 2
472+ else:
473+ return -1

Subscribers

People subscribed via source and target branches