Merge lp:~sylvain-pineau/checkbox/trusted-launcher-standalone into lp:checkbox
- trusted-launcher-standalone
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | 2107 |
Merged at revision: | 2107 |
Proposed branch: | lp:~sylvain-pineau/checkbox/trusted-launcher-standalone |
Merge into: | lp:checkbox |
Diff against target: |
1208 lines (+814/-111) 11 files modified
plainbox/.coveragerc (+2/-0) plainbox/MANIFEST.in (+1/-0) plainbox/plainbox/data/org.freedesktop.policykit.pkexec.policy (+30/-0) plainbox/plainbox/impl/job.py (+10/-59) plainbox/plainbox/impl/rfc822.py (+4/-19) plainbox/plainbox/impl/runner.py (+39/-16) plainbox/plainbox/impl/secure/__init__.py (+27/-0) plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py (+402/-0) plainbox/plainbox/impl/secure/test_checkbox_trusted_launcher.py (+268/-0) plainbox/plainbox/impl/test_rfc822.py (+23/-16) plainbox/setup.py (+8/-1) |
To merge this branch: | bzr merge lp:~sylvain-pineau/checkbox/trusted-launcher-standalone |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Approve | ||
Zygmunt Krynicki (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
This is an implementation proposal for Plainbox.
It provides the trusted launcher component to run commands as another user (root most of time).
This launcher has be started by pkexec to apply the rules defined in the policy file.
The root password will only be asked once for the lifetime of the plainbox process (see auth_admin_keep).
The policy file has to be installed manually (sorry...) to test it (in /usr/share/
It's just a proof of concept as some code is needed to ask the password when the sessions starts not before the first test requiring root.
To avoid running the trusted launcher as a service, it will be started for each test requiring root privileges.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Zygmunt Krynicki (zyga) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Zygmunt Krynicki (zyga) wrote : | # |
92 + cmd = ['checkbox-
93 "{key}=
94 for key, value in self._get_
95 job, only_changes=True
96 ).items()
97 - ] + cmd
98 + ]
99 + if job.via is not None:
100 + cmd = cmd + ['--via', job.via]
101 + if job._checkbox._mode == 'src':
102 + # Running PlainBox from source doesn't require the trusted
103 + # launcher
This looks as if it was structured incorrectly. How about something like this:
if job._checkbox._mode == 'src':
cmd = self._get_
elif job.user is not None:
cmd = self._get_
else:
...
+ for filename in path_expand(
Isn't that /usr/share/
+
365 + for job in desired_job_list:
366 + job_result = subprocess.
This means we'll runn all jobs as root. I think we should respect the user key and actually use the right user for each particular job that we execute (including local jobs)
367 + job.command,
368 + shell=True,
369 + universal_
370 + env=job.
371 + stream = io.StringIO()
372 + stream.
Yuck! Just pass PIPE and use the stream, there is no need to copy anything
373 + stream.seek(0)
374 + for message in RFC822Parser(
I'll keep reviewing this
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Zygmunt Krynicki (zyga) wrote : | # |
+include org.freedesktop
Let's keep that in a non-root directory. Ideas welcome
172 +class Job():
No need to use () there
340 + # List of all available jobs in system-wide locations
341 + builtin_jobs = []
342 + # List of all checkbox related packages, like checkbox-oem
343 + packages = []
I don't like this part. I would much rather have a fixed set of places and not probe the system for anything.
344 +
345 + def path_expand(path):
346 + for path in glob.glob(path):
Variable shadowed
347 + packages.
348 + for dirpath, dirs, filenames in os.walk(
349 + path, 'jobs')):
350 + for name in filenames:
351 + if name.endswith(
352 + yield os.path.
All of that _lacks_tests_, copying tested code without adding test is semi-okay but adding features without a shred of tests is a no-go.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Zygmunt Krynicki (zyga) wrote : | # |
Marking as needs fixing to get it to move to appropriate slot in +activereviews
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
The new version with tests is ready for review.
The documentation is still missing but I will propose it in a separate branch.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Zygmunt Krynicki (zyga) wrote : | # |
103 + cmd = cmd + ['--via', job.via]
+=
107 + # Running PlainBox from source doesn't require the trusted launcher
108 + # That's why we use the env(1)' command and pass it the list of
109 + # changed environment variables.
110 + # The whole pkexec and env part gets prepended to the command
111 + # we were supposed to run.
docstring, also affects the method above
193 +:mod:`
Move to plainbox.
337 +
338 + pass
Remove
420 + Runner for jobs - executes jobs and produces results
Not true, the runner just executes the process and pipes back stdout/stderr
436 + if name.endswith(
.txt.in should be skipped, it is only relevant to in-tree development
477 + try:
478 + target_job = [j for j in lookup_list
479 + if j.get_checksum() == args.HASH][0]
480 + os.execve(
481 + '/bin/bash',
482 + ['bash', '-c', target_
483 + target_
484 + args.ENV,
485 + self.packages)
486 + )
487 + except IndexError:
488 + return "Job not found"
Move try/index errror together, do anther try/catch for anything you may want to catch from os.execve
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
Thanks for your review, I've fixed all the issues except the docstring. env(1) is really only used for the 'src' mode.
When usinf the trusted launcher, env keys are appended to the list of optional parameters.
Regarding the tests, I can't subclass the existing RFCParserTests. When called, it use the plainbox parser load method instead of the launcher one. So it was easier do just duplicate tests.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Zygmunt Krynicki (zyga) wrote : | # |
> Regarding the tests, I can't subclass the existing RFCParserTests. When
> called, it use the plainbox parser load method instead of the launcher one. So
> it was easier do just duplicate tests.
You could add a test method like get_parser() that would return the appropriate parser method.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
RFC822 parser tests are now shared between the trusted launcher and PlainBox core. No duplication of tests!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
Rebasing/
setup.py: pep8 fixes (just for long lines)
runner.py: removed an uneeded logging call
policy file: use the checkbox icon
job.py: add a docstring to Job.via attribute
trusted-launcher: Removed a useless comment in the main function
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
(05:29:23 PM) zyga: spineau: http://
(05:30:59 PM) spineau: zyga: hehe, I just put there the existing command, but I can make it readable, gimme 5 m
(05:31:20 PM) zyga: spineau: in http://
(05:31:29 PM) zyga: spineau: but the code there looks great
(05:31:48 PM) zyga: spineau: and that's a very nice way to reuse tests and keep both implementation in sync
(05:33:37 PM) zyga: spineau: lookin at http://
(05:34:10 PM) zyga: spineau: ditto for errors and records, we _might_ import them from the non-secure codepath and cut code
(05:35:45 PM) zyga: spineau: in http://
(05:36:49 PM) spineau: zyga: I use both, but give precedence to bin
(05:37:00 PM) zyga: spineau: again in http://
(05:37:10 PM) zyga: spineau: yeah, I know you give precendence to bin
(05:37:17 PM) zyga: spineau: I was just checking if that was the cause
(05:37:20 PM) spineau: zyga: and yes it's for the old style packaging
(05:37:43 PM) zyga: spineau: on the same line, VIA_HASH should be lowercase as this is just a variable
(05:38:22 PM) zyga: spineau: same revision, diff lines 319-322, unless you had a reason not to, use with open(...) there
(05:39:41 PM) spineau: zyga: it may be a bug in coverage but with a "with" statement, I get a partial branch coverage, don't know why
(05:40:12 PM) zyga: spineau: same revision, you don't wait for the process wrapped by via_job_result, that adds zombies
(05:40:24 PM) zyga: spineau: interesting, flip that and repush, I'll investigate
(05:40:49 PM) zyga: spineau: I suspect you just need try/finally: with via_job_
(05:40:59 PM) zyga: spineau: otherwise the runner looks okay
(05:41:42 PM) zyga: spineau: I've yet to read http://
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
This new version is mainly for code optimization and reuse.
base classes are provided by the secure module and inherited by the core.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
Ready to land
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Daniel Manrique (roadmr) wrote : | # |
The attempt to merge lp:~sylvain-pineau/checkbox/trusted-launcher-standalone into lp:checkbox failed. Below is the output from the failed tests.
[precise] Bringing VM 'up'
Timing for [precise] Bringing VM 'up'
7.99user 3.50system 5:44.30elapsed 3%CPU (0avgtext+0avgdata 21464maxresident)k
0inputs+200outputs (0major+
[precise] Starting tests...
[precise] CheckBox test suite: pass
Timing for Checkbox test suite
1.14user 0.48system 0:11.96elapsed 13%CPU (0avgtext+0avgdata 20020maxresident)k
7352inputs+
Timing for refreshing plainbox installation
0.75user 0.29system 0:05.57elapsed 18%CPU (0avgtext+0avgdata 20592maxresident)k
0inputs+16outputs (0major+
[precise] PlainBox test suite: fail
[precise] stdout: http://
[precise] stderr: http://
Timing for plainbox test suite
Command exited with non-zero status 1
0.92user 0.33system 0:11.58elapsed 10%CPU (0avgtext+0avgdata 20796maxresident)k
0inputs+184outputs (0major+
[precise] PlainBox documentation build: pass
Timing for plainbox documentation build
0.71user 0.31system 0:14.56elapsed 7%CPU (0avgtext+0avgdata 20512maxresident)k
0inputs+16outputs (0major+
[precise] Integration tests: pass
Timing for integration tests
0.78user 0.25system 0:08.41elapsed 12%CPU (0avgtext+0avgdata 20832maxresident)k
0inputs+8outputs (0major+
[precise] Destroying VM
[quantal] Bringing VM 'up'
Timing for [quantal] Bringing VM 'up'
8.05user 3.72system 4:02.01elapsed 4%CPU (0avgtext+0avgdata 21244maxresident)k
24inputs+184outputs (1major+
[quantal] Starting tests...
[quantal] CheckBox test suite: pass
Timing for Checkbox test suite
0.76user 0.31system 0:12.89elapsed 8%CPU (0avgtext+0avgdata 20764maxresident)k
0inputs+16outputs (0major+
Timing for refreshing plainbox installation
0.78user 0.30system 0:06.91elapsed 15%CPU (0avgtext+0avgdata 20800maxresident)k
0inputs+16outputs (0major+
[quantal] PlainBox test suite: fail
[quantal] stdout: http://
[quantal] stderr: http://
Timing for plainbox test suite
Command exited with non-zero status 1
0.98user 0.34system 0:14.49elapsed 9%CPU (0avgtext+0avgdata 19996maxresident)k
0inputs+184outputs (0major+
[quantal] PlainBox documentation build: pass
Timing for plainbox documentation build
0.72user 0.33system 0:07.19elapsed 14%CPU (0avgtext+0avgdata 20612maxresident)k
0inputs+8outputs (0major+
[quantal] Integration tests: pass
Timing for integration tests
0.72user 0.32system 0:09.98elapsed 10%CPU (0avgtext+0avgdata 20608maxresident)k
0inputs+8outputs (0major+
[quantal] Destroying VM
[raring] Bringing VM 'up'
Timing for [raring] Bringing VM 'up'
8.42user 3.67system 6:04.12elapsed 3%CPU (0avgtext+0avgdata 21468maxresident)k
0inputs+232outputs (0major+
[raring] Starting tests...
[raring] CheckBox test suite: pass
Timi...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Zygmunt Krynicki (zyga) wrote : | # |
14:21 < zyga> spineau_afk: you have two test failures
14:22 < zyga> spineau_afk: so first one is mock older api, you can probably work around that with just assinging stuff after you instantiate mock itself
14:22 < zyga> spineau_afk: the second failure is a bit more annoying, that's argparse incompatibility between 3.2 and 3.3
14:22 < zyga> spineau_afk: plainbox has a fix for that, look at how I resolved it, the fix is in box.py with a lenghty comment that explains why it is needed
14:22 < zyga> spineau_afk: thanks
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
Self-approval.
This version fixed the bug with Mock version 0.7.2 on precise and 0.8.0 on quantal.
Also resolved the argparse imcompatibility with python3.3, basically with run the launcher without args we call parser.error().
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Daniel Manrique (roadmr) wrote : | # |
The attempt to merge lp:~sylvain-pineau/checkbox/trusted-launcher-standalone into lp:checkbox failed. Below is the output from the failed tests.
[precise] Bringing VM 'up'
Timing for [precise] Bringing VM 'up'
8.50user 3.87system 6:01.39elapsed 3%CPU (0avgtext+0avgdata 21308maxresident)k
0inputs+216outputs (0major+
[precise] Starting tests...
[precise] CheckBox test suite: pass
Timing for Checkbox test suite
0.82user 0.25system 0:11.25elapsed 9%CPU (0avgtext+0avgdata 20600maxresident)k
0inputs+16outputs (0major+
Timing for refreshing plainbox installation
0.76user 0.31system 0:05.58elapsed 19%CPU (0avgtext+0avgdata 20584maxresident)k
0inputs+16outputs (0major+
[precise] PlainBox test suite: pass
Timing for plainbox test suite
0.96user 0.32system 0:12.08elapsed 10%CPU (0avgtext+0avgdata 20800maxresident)k
0inputs+184outputs (0major+
[precise] PlainBox documentation build: pass
Timing for plainbox documentation build
0.84user 0.29system 0:14.77elapsed 7%CPU (0avgtext+0avgdata 20820maxresident)k
0inputs+16outputs (0major+
[precise] Integration tests: pass
Timing for integration tests
0.80user 0.32system 0:08.99elapsed 12%CPU (0avgtext+0avgdata 19980maxresident)k
0inputs+8outputs (0major+
[precise] Destroying VM
[quantal] Bringing VM 'up'
Timing for [quantal] Bringing VM 'up'
7.78user 3.36system 3:49.48elapsed 4%CPU (0avgtext+0avgdata 22176maxresident)k
0inputs+176outputs (0major+
[quantal] Starting tests...
[quantal] CheckBox test suite: pass
Timing for Checkbox test suite
0.78user 0.28system 0:13.06elapsed 8%CPU (0avgtext+0avgdata 20496maxresident)k
0inputs+16outputs (0major+
Timing for refreshing plainbox installation
0.80user 0.27system 0:06.67elapsed 16%CPU (0avgtext+0avgdata 20680maxresident)k
0inputs+16outputs (0major+
[quantal] PlainBox test suite: pass
Timing for plainbox test suite
0.98user 0.28system 0:14.31elapsed 8%CPU (0avgtext+0avgdata 20812maxresident)k
0inputs+184outputs (0major+
[quantal] PlainBox documentation build: pass
Timing for plainbox documentation build
0.78user 0.26system 0:07.18elapsed 14%CPU (0avgtext+0avgdata 20600maxresident)k
0inputs+8outputs (0major+
[quantal] Integration tests: pass
Timing for integration tests
0.82user 0.26system 0:10.03elapsed 10%CPU (0avgtext+0avgdata 20748maxresident)k
0inputs+8outputs (0major+
[quantal] Destroying VM
[raring] Bringing VM 'up'
[raring] Unable to 'up' VM!
[raring] stdout: http://
[raring] stderr: http://
[raring] NOTE: unable to execute tests, marked as failed
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Daniel Manrique (roadmr) wrote : | # |
The attempt to merge lp:~sylvain-pineau/checkbox/trusted-launcher-standalone into lp:checkbox failed. Below is the output from the failed tests.
[precise] Bringing VM 'up'
[precise] Unable to 'up' VM!
[precise] stdout: http://
[precise] stderr: http://
[precise] NOTE: unable to execute tests, marked as failed
[quantal] Bringing VM 'up'
[quantal] Unable to 'up' VM!
[quantal] stdout: http://
[quantal] stderr: http://
[quantal] NOTE: unable to execute tests, marked as failed
[raring] Bringing VM 'up'
[raring] Unable to 'up' VM!
[raring] stdout: http://
[raring] stderr: http://
[raring] NOTE: unable to execute tests, marked as failed
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Daniel Manrique (roadmr) wrote : | # |
The attempt to merge lp:~sylvain-pineau/checkbox/trusted-launcher-standalone into lp:checkbox failed. Below is the output from the failed tests.
[precise] Bringing VM 'up'
[precise] Unable to 'up' VM!
[precise] stdout: http://
[precise] stderr: http://
[precise] NOTE: unable to execute tests, marked as failed
[quantal] Bringing VM 'up'
[quantal] Unable to 'up' VM!
[quantal] stdout: http://
[quantal] stderr: http://
[quantal] NOTE: unable to execute tests, marked as failed
[raring] Bringing VM 'up'
[raring] Unable to 'up' VM!
[raring] stdout: http://
[raring] stderr: http://
[raring] NOTE: unable to execute tests, marked as failed
Preview Diff
1 | === modified file 'plainbox/.coveragerc' |
2 | --- plainbox/.coveragerc 2013-03-22 08:30:28 +0000 |
3 | +++ plainbox/.coveragerc 2013-05-08 07:38:29 +0000 |
4 | @@ -9,6 +9,8 @@ |
5 | plainbox/impl/integration_tests.py |
6 | plainbox/impl/commands/test_* |
7 | plainbox/impl/exporter/test_* |
8 | + plainbox/impl/transport/test_* |
9 | + plainbox/impl/secure/test_* |
10 | plainbox/testing_utils/test_* |
11 | |
12 | [report] |
13 | |
14 | === modified file 'plainbox/MANIFEST.in' |
15 | --- plainbox/MANIFEST.in 2013-04-04 17:12:12 +0000 |
16 | +++ plainbox/MANIFEST.in 2013-05-08 07:38:29 +0000 |
17 | @@ -5,3 +5,4 @@ |
18 | recursive-include docs *.rst |
19 | include docs/conf.py |
20 | include plainbox/data/report/hardware-1_0.rng |
21 | +include plainbox/data/org.freedesktop.policykit.pkexec.policy |
22 | |
23 | === added file 'plainbox/plainbox/data/org.freedesktop.policykit.pkexec.policy' |
24 | --- plainbox/plainbox/data/org.freedesktop.policykit.pkexec.policy 1970-01-01 00:00:00 +0000 |
25 | +++ plainbox/plainbox/data/org.freedesktop.policykit.pkexec.policy 2013-05-08 07:38:29 +0000 |
26 | @@ -0,0 +1,30 @@ |
27 | +<?xml version="1.0" encoding="UTF-8"?> |
28 | +<!DOCTYPE policyconfig PUBLIC |
29 | + "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" |
30 | + "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> |
31 | +<policyconfig> |
32 | + |
33 | + <!-- |
34 | + Policy definitions for PlainBox system actions. |
35 | + (C) 2013 Canonical Ltd. |
36 | + Author: Sylvain Pineau <sylvain.pineau@canonical.com> |
37 | + --> |
38 | + |
39 | + <vendor>PlainBox</vendor> |
40 | + <vendor_url>https://launchpad.net/checkbox</vendor_url> |
41 | + <icon_name>checkbox</icon_name> |
42 | + |
43 | + <action id="org.freedesktop.policykit.pkexec.run-plainbox-job"> |
44 | + <description>Run Job command</description> |
45 | + <message>Authentication is required to run a job command as another user</message> |
46 | + <defaults> |
47 | + <allow_any>no</allow_any> |
48 | + <allow_inactive>no</allow_inactive> |
49 | + <allow_active>auth_admin_keep</allow_active> |
50 | + </defaults> |
51 | + <annotate key="org.freedesktop.policykit.exec.path">/usr/bin/checkbox-trusted-launcher</annotate> |
52 | + <annotate key="org.freedesktop.policykit.exec.allow_gui">TRUE</annotate> |
53 | + </action> |
54 | + |
55 | +</policyconfig> |
56 | + |
57 | |
58 | === modified file 'plainbox/plainbox/impl/job.py' |
59 | --- plainbox/plainbox/impl/job.py 2013-04-23 00:16:38 +0000 |
60 | +++ plainbox/plainbox/impl/job.py 2013-05-08 07:38:29 +0000 |
61 | @@ -26,9 +26,6 @@ |
62 | THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
63 | """ |
64 | |
65 | -import collections |
66 | -import hashlib |
67 | -import json |
68 | import logging |
69 | import os |
70 | import re |
71 | @@ -36,12 +33,13 @@ |
72 | from plainbox.abc import IJobDefinition |
73 | from plainbox.impl.config import Unset |
74 | from plainbox.impl.resource import ResourceProgram |
75 | +from plainbox.impl.secure.checkbox_trusted_launcher import BaseJob |
76 | |
77 | |
78 | logger = logging.getLogger("plainbox.job") |
79 | |
80 | |
81 | -class JobDefinition(IJobDefinition): |
82 | +class JobDefinition(BaseJob, IJobDefinition): |
83 | """ |
84 | Job definition class. |
85 | |
86 | @@ -50,10 +48,6 @@ |
87 | """ |
88 | |
89 | @property |
90 | - def plugin(self): |
91 | - return self.__getattr__('plugin') |
92 | - |
93 | - @property |
94 | def name(self): |
95 | return self.__getattr__('name') |
96 | |
97 | @@ -65,13 +59,6 @@ |
98 | return None |
99 | |
100 | @property |
101 | - def command(self): |
102 | - try: |
103 | - return self.__getattr__('command') |
104 | - except AttributeError: |
105 | - return None |
106 | - |
107 | - @property |
108 | def description(self): |
109 | try: |
110 | return self.__getattr__('description') |
111 | @@ -86,16 +73,13 @@ |
112 | return None |
113 | |
114 | @property |
115 | - def user(self): |
116 | - try: |
117 | - return self.__getattr__('user') |
118 | - except AttributeError: |
119 | - return None |
120 | - |
121 | - @property |
122 | - def environ(self): |
123 | - try: |
124 | - return self.__getattr__('environ') |
125 | + def via(self): |
126 | + """ |
127 | + The checksum of the "parent" job when the current JobDefinition comes |
128 | + from a job output using the local plugin |
129 | + """ |
130 | + try: |
131 | + return self.__getattr__('via') |
132 | except AttributeError: |
133 | return None |
134 | |
135 | @@ -184,15 +168,6 @@ |
136 | else: |
137 | return set() |
138 | |
139 | - def get_environ_settings(self): |
140 | - """ |
141 | - Return a set of requested environment variables |
142 | - """ |
143 | - if self.environ is not None: |
144 | - return {variable for variable in re.split('[\s,]+', self.environ)} |
145 | - else: |
146 | - return set() |
147 | - |
148 | @classmethod |
149 | def from_rfc822_record(cls, record): |
150 | """ |
151 | @@ -270,35 +245,11 @@ |
152 | jobs (plugin local). The intent is to encapsulate the sharing of the |
153 | embedded checkbox reference. |
154 | """ |
155 | + record.data['via'] = self.get_checksum() |
156 | job = self.from_rfc822_record(record) |
157 | job._checkbox = self._checkbox |
158 | return job |
159 | |
160 | - def get_checksum(self): |
161 | - """ |
162 | - Compute a checksum of the job definition. |
163 | - |
164 | - This method can be used to compute the checksum of the canonical form |
165 | - of the job definition. The canonical form is the UTF-8 encoded JSON |
166 | - serialization of the data that makes up the full definition of the job |
167 | - (all keys and values). The JSON serialization uses no indent and |
168 | - minimal separators. |
169 | - |
170 | - The checksum is defined as the SHA256 hash of the canonical form. |
171 | - """ |
172 | - # Ideally we'd use simplejson.dumps() with sorted keys to get |
173 | - # predictable serialization but that's another dependency. To get |
174 | - # something simple that is equally reliable, just sort all the keys |
175 | - # manually and ask standard json to serialize that.. |
176 | - sorted_data = collections.OrderedDict(sorted(self._data.items())) |
177 | - # Compute the canonical form which is arbitrarily defined as sorted |
178 | - # json text with default indent and separator settings. |
179 | - canonical_form = json.dumps( |
180 | - sorted_data, indent=None, separators=(',', ':')) |
181 | - # Compute the sha256 hash of the UTF-8 encoding of the canonical form |
182 | - # and return the hex digest as the checksum that can be displayed. |
183 | - return hashlib.sha256(canonical_form.encode('UTF-8')).hexdigest() |
184 | - |
185 | @classmethod |
186 | def from_json_record(cls, record): |
187 | """ |
188 | |
189 | === modified file 'plainbox/plainbox/impl/rfc822.py' |
190 | --- plainbox/plainbox/impl/rfc822.py 2013-02-25 11:02:59 +0000 |
191 | +++ plainbox/plainbox/impl/rfc822.py 2013-05-08 07:38:29 +0000 |
192 | @@ -32,6 +32,9 @@ |
193 | |
194 | from inspect import cleandoc |
195 | |
196 | +from plainbox.impl.secure.checkbox_trusted_launcher import RFC822SyntaxError |
197 | +from plainbox.impl.secure.checkbox_trusted_launcher import BaseRFC822Record |
198 | + |
199 | logger = logging.getLogger("plainbox.rfc822") |
200 | |
201 | |
202 | @@ -62,7 +65,7 @@ |
203 | self.filename, self.line_start, self.line_end) |
204 | |
205 | |
206 | -class RFC822Record: |
207 | +class RFC822Record(BaseRFC822Record): |
208 | """ |
209 | Class for tracking RFC822 records |
210 | |
211 | @@ -85,24 +88,6 @@ |
212 | """ |
213 | return self._origin |
214 | |
215 | - @property |
216 | - def data(self): |
217 | - """ |
218 | - The data set (dictionary) |
219 | - """ |
220 | - return self._data |
221 | - |
222 | - |
223 | -class RFC822SyntaxError(SyntaxError): |
224 | - """ |
225 | - SyntaxError subclass for RFC822 parsing functions |
226 | - """ |
227 | - |
228 | - def __init__(self, filename, lineno, msg): |
229 | - self.filename = filename |
230 | - self.lineno = lineno |
231 | - self.msg = msg |
232 | - |
233 | |
234 | def load_rfc822_records(stream, data_cls=dict): |
235 | """ |
236 | |
237 | === modified file 'plainbox/plainbox/impl/runner.py' |
238 | --- plainbox/plainbox/impl/runner.py 2013-04-25 09:23:45 +0000 |
239 | +++ plainbox/plainbox/impl/runner.py 2013-05-08 07:38:29 +0000 |
240 | @@ -298,6 +298,37 @@ |
241 | else: |
242 | return env |
243 | |
244 | + def _get_command_trusted(self, job, config=None): |
245 | + # When the job requires to run as root then elevate our permissions |
246 | + # via pkexec(1). Since pkexec resets environment we need to somehow |
247 | + # pass the extra things we require. To do that we pass the list of |
248 | + # changed environment variables in addition to the job hash. |
249 | + cmd = ['checkbox-trusted-launcher', job.get_checksum()] + [ |
250 | + "{key}={value}".format(key=key, value=value) |
251 | + for key, value in self._get_script_env( |
252 | + job, config, only_changes=True |
253 | + ).items() |
254 | + ] |
255 | + if job.via is not None: |
256 | + cmd += ['--via', job.via] |
257 | + return cmd |
258 | + |
259 | + def _get_command_src(self, job, config=None): |
260 | + # Running PlainBox from source doesn't require the trusted launcher |
261 | + # That's why we use the env(1)' command and pass it the list of |
262 | + # changed environment variables. |
263 | + # The whole pkexec and env part gets prepended to the command |
264 | + # we were supposed to run. |
265 | + cmd = ['env'] |
266 | + cmd += [ |
267 | + "{key}={value}".format(key=key, value=value) |
268 | + for key, value in self._get_script_env( |
269 | + job, only_changes=True |
270 | + ).items() |
271 | + ] |
272 | + cmd += ['bash', '-c', job.command] |
273 | + return cmd |
274 | + |
275 | def _run_command(self, job, config): |
276 | """ |
277 | Run the shell command associated with the specified job. |
278 | @@ -350,26 +381,18 @@ |
279 | # threads although all callbacks will be fired from a single |
280 | # thread (which is _not_ the main thread) |
281 | logger.debug("job[%s] starting command: %s", job.name, job.command) |
282 | - # XXX: sadly using /bin/sh results in broken output |
283 | - # XXX: maybe run it both ways and raise exceptions on differences? |
284 | - cmd = ['bash', '-c', job.command] |
285 | if job.user is not None: |
286 | - # When the job requires to run as root then elevate our permissions |
287 | - # via pkexec(1). Since pkexec resets environment we need to somehow |
288 | - # pass the extra things we require. To do that we use the env(1) |
289 | - # command and pass it the list of changed environment variables. |
290 | - # |
291 | - # The whole pkexec and env part gets prepended to the command we |
292 | - # were supposed to run. |
293 | - cmd = ['pkexec', '--user', job.user, 'env'] + [ |
294 | - "{key}={value}".format(key=key, value=value) |
295 | - for key, value in self._get_script_env( |
296 | - job, config, only_changes=True |
297 | - ).items() |
298 | - ] + cmd |
299 | + if job._checkbox._mode == 'src': |
300 | + cmd = self._get_command_src(job, config) |
301 | + else: |
302 | + cmd = self._get_command_trusted(job, config) |
303 | + cmd = ['pkexec', '--user', job.user] + cmd |
304 | logging.debug("job[%s] executing %r", job.name, cmd) |
305 | return_code = logging_popen.call(cmd) |
306 | else: |
307 | + # XXX: sadly using /bin/sh results in broken output |
308 | + # XXX: maybe run it both ways and raise exceptions on differences? |
309 | + cmd = ['bash', '-c', job.command] |
310 | logging.debug("job[%s] executing %r", job.name, cmd) |
311 | return_code = logging_popen.call( |
312 | cmd, env=self._get_script_env(job, config)) |
313 | |
314 | === added directory 'plainbox/plainbox/impl/secure' |
315 | === added file 'plainbox/plainbox/impl/secure/__init__.py' |
316 | --- plainbox/plainbox/impl/secure/__init__.py 1970-01-01 00:00:00 +0000 |
317 | +++ plainbox/plainbox/impl/secure/__init__.py 2013-05-08 07:38:29 +0000 |
318 | @@ -0,0 +1,27 @@ |
319 | +# This file is part of Checkbox. |
320 | +# |
321 | +# Copyright 2013 Canonical Ltd. |
322 | +# Written by: |
323 | +# Sylvain Pineau <sylvain.pineau@canonical.com> |
324 | +# |
325 | +# Checkbox is free software: you can redistribute it and/or modify |
326 | +# it under the terms of the GNU General Public License as published by |
327 | +# the Free Software Foundation, either version 3 of the License, or |
328 | +# (at your option) any later version. |
329 | +# |
330 | +# Checkbox is distributed in the hope that it will be useful, |
331 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
332 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
333 | +# GNU General Public License for more details. |
334 | +# |
335 | +# You should have received a copy of the GNU General Public License |
336 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
337 | + |
338 | +""" |
339 | +:mod:`plainbox.impl.secure` -- code for external (trusted) launchers |
340 | +==================================================================== |
341 | + |
342 | +.. warning:: |
343 | + |
344 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
345 | +""" |
346 | |
347 | === added file 'plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py' |
348 | --- plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py 1970-01-01 00:00:00 +0000 |
349 | +++ plainbox/plainbox/impl/secure/checkbox_trusted_launcher.py 2013-05-08 07:38:29 +0000 |
350 | @@ -0,0 +1,402 @@ |
351 | +# This file is part of Checkbox. |
352 | +# |
353 | +# Copyright 2013 Canonical Ltd. |
354 | +# Written by: |
355 | +# Sylvain Pineau <sylvain.pineau@canonical.com> |
356 | +# |
357 | +# Checkbox is free software: you can redistribute it and/or modify |
358 | +# it under the terms of the GNU General Public License as published by |
359 | +# the Free Software Foundation, either version 3 of the License, or |
360 | +# (at your option) any later version. |
361 | +# |
362 | +# Checkbox is distributed in the hope that it will be useful, |
363 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
364 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
365 | +# GNU General Public License for more details. |
366 | +# |
367 | +# You should have received a copy of the GNU General Public License |
368 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
369 | + |
370 | +""" |
371 | +:mod:`plainbox.impl.secure.checkbox_trusted_launcher` -- command launcher |
372 | +========================================================================= |
373 | + |
374 | +.. warning:: |
375 | + |
376 | + THIS MODULE DOES NOT HAVE STABLE PUBLIC API |
377 | +""" |
378 | + |
379 | +import argparse |
380 | +import collections |
381 | +import glob |
382 | +import hashlib |
383 | +import json |
384 | +import os |
385 | +import re |
386 | +import subprocess |
387 | +from argparse import _ as argparse_gettext |
388 | +from inspect import cleandoc |
389 | + |
390 | + |
391 | +class BaseJob: |
392 | + """ |
393 | + Base Job definition class. |
394 | + """ |
395 | + |
396 | + @property |
397 | + def plugin(self): |
398 | + return self.__getattr__('plugin') |
399 | + |
400 | + @property |
401 | + def command(self): |
402 | + try: |
403 | + return self.__getattr__('command') |
404 | + except AttributeError: |
405 | + return None |
406 | + |
407 | + @property |
408 | + def environ(self): |
409 | + try: |
410 | + return self.__getattr__('environ') |
411 | + except AttributeError: |
412 | + return None |
413 | + |
414 | + @property |
415 | + def user(self): |
416 | + try: |
417 | + return self.__getattr__('user') |
418 | + except AttributeError: |
419 | + return None |
420 | + |
421 | + def __init__(self, data): |
422 | + self._data = data |
423 | + |
424 | + def __getattr__(self, attr): |
425 | + if attr in self._data: |
426 | + return self._data[attr] |
427 | + raise AttributeError(attr) |
428 | + |
429 | + def get_checksum(self): |
430 | + """ |
431 | + Compute a checksum of the job definition. |
432 | + |
433 | + This method can be used to compute the checksum of the canonical form |
434 | + of the job definition. The canonical form is the UTF-8 encoded JSON |
435 | + serialization of the data that makes up the full definition of the job |
436 | + (all keys and values). The JSON serialization uses no indent and |
437 | + minimal separators. |
438 | + |
439 | + The checksum is defined as the SHA256 hash of the canonical form. |
440 | + """ |
441 | + # Ideally we'd use simplejson.dumps() with sorted keys to get |
442 | + # predictable serialization but that's another dependency. To get |
443 | + # something simple that is equally reliable, just sort all the keys |
444 | + # manually and ask standard json to serialize that.. |
445 | + sorted_data = collections.OrderedDict(sorted(self._data.items())) |
446 | + # Compute the canonical form which is arbitrarily defined as sorted |
447 | + # json text with default indent and separator settings. |
448 | + canonical_form = json.dumps( |
449 | + sorted_data, indent=None, separators=(',', ':')) |
450 | + # Compute the sha256 hash of the UTF-8 encoding of the canonical form |
451 | + # and return the hex digest as the checksum that can be displayed. |
452 | + return hashlib.sha256(canonical_form.encode('UTF-8')).hexdigest() |
453 | + |
454 | + def get_environ_settings(self): |
455 | + """ |
456 | + Return a set of requested environment variables |
457 | + """ |
458 | + if self.environ is not None: |
459 | + return {variable for variable in re.split('[\s,]+', self.environ)} |
460 | + else: |
461 | + return set() |
462 | + |
463 | + def modify_execution_environment(self, environ, packages): |
464 | + """ |
465 | + Compute the environment the script will be executed in |
466 | + """ |
467 | + # Get a proper environment |
468 | + env = dict(os.environ) |
469 | + # Use non-internationalized environment |
470 | + env['LANG'] = 'C.UTF-8' |
471 | + # Create CHECKBOX*_SHARE for every checkbox related packages |
472 | + # Add their respective script directory to the PATH variable |
473 | + # giving precedence to those located in /usr/lib/ |
474 | + for path in packages: |
475 | + basename = os.path.basename(path) |
476 | + env[basename.upper().replace('-', '_') + '_SHARE'] = path |
477 | + # Update PATH so that scripts can be found |
478 | + env['PATH'] = os.pathsep.join([ |
479 | + os.path.join('usr', 'lib', basename, 'bin'), |
480 | + os.path.join(path, 'scripts')] |
481 | + + env.get("PATH", "").split(os.pathsep)) |
482 | + if 'CHECKBOX_DATA' in env: |
483 | + env['CHECKBOX_DATA'] = environ['CHECKBOX_DATA'] |
484 | + # Add new environment variables only if they are defined in the |
485 | + # job environ property |
486 | + for key in self.get_environ_settings(): |
487 | + if key in environ: |
488 | + env[key] = environ[key] |
489 | + return env |
490 | + |
491 | + |
492 | +class BaseRFC822Record: |
493 | + """ |
494 | + Base class for tracking RFC822 records |
495 | + |
496 | + This is a simple container for the dictionary of data. |
497 | + """ |
498 | + |
499 | + def __init__(self, data): |
500 | + self._data = data |
501 | + |
502 | + @property |
503 | + def data(self): |
504 | + """ |
505 | + The data set (dictionary) |
506 | + """ |
507 | + return self._data |
508 | + |
509 | + |
510 | +class RFC822SyntaxError(SyntaxError): |
511 | + """ |
512 | + SyntaxError subclass for RFC822 parsing functions |
513 | + """ |
514 | + |
515 | + def __init__(self, filename, lineno, msg): |
516 | + self.filename = filename |
517 | + self.lineno = lineno |
518 | + self.msg = msg |
519 | + |
520 | + |
521 | +def load_rfc822_records(stream, data_cls=dict): |
522 | + """ |
523 | + Load a sequence of rfc822-like records from a text stream. |
524 | + |
525 | + Each record consists of any number of key-value pairs. Subsequent records |
526 | + are separated by one blank line. A record key may have a multi-line value |
527 | + if the line starts with whitespace character. |
528 | + |
529 | + Returns a list of subsequent values as instances BaseRFC822Record class. If |
530 | + the optional data_cls argument is collections.OrderedDict then the values |
531 | + retain their original ordering. |
532 | + """ |
533 | + return list(gen_rfc822_records(stream, data_cls)) |
534 | + |
535 | + |
536 | +def gen_rfc822_records(stream, data_cls=dict): |
537 | + """ |
538 | + Load a sequence of rfc822-like records from a text stream. |
539 | + |
540 | + Each record consists of any number of key-value pairs. Subsequent records |
541 | + are separated by one blank line. A record key may have a multi-line value |
542 | + if the line starts with whitespace character. |
543 | + |
544 | + Returns a list of subsequent values as instances BaseRFC822Record class. If |
545 | + the optional data_cls argument is collections.OrderedDict then the values |
546 | + retain their original ordering. |
547 | + """ |
548 | + record = None |
549 | + data = None |
550 | + key = None |
551 | + value_list = None |
552 | + |
553 | + def _syntax_error(msg): |
554 | + """ |
555 | + Report a syntax error in the current line |
556 | + """ |
557 | + try: |
558 | + filename = stream.name |
559 | + except AttributeError: |
560 | + filename = None |
561 | + return RFC822SyntaxError(filename, lineno, msg) |
562 | + |
563 | + def _new_record(): |
564 | + """ |
565 | + Reset local state to track new record |
566 | + """ |
567 | + nonlocal key |
568 | + nonlocal value_list |
569 | + nonlocal record |
570 | + nonlocal data |
571 | + key = None |
572 | + value_list = None |
573 | + data = data_cls() |
574 | + record = BaseRFC822Record(data) |
575 | + |
576 | + def _commit_key_value_if_needed(): |
577 | + """ |
578 | + Finalize the most recently seen key: value pair |
579 | + """ |
580 | + nonlocal key |
581 | + if key is not None: |
582 | + data[key] = cleandoc('\n'.join(value_list)) |
583 | + key = None |
584 | + |
585 | + # Start with an empty record |
586 | + _new_record() |
587 | + # Iterate over subsequent lines of the stream |
588 | + for lineno, line in enumerate(stream, start=1): |
589 | + # Treat empty lines as record separators |
590 | + if line.strip() == "": |
591 | + # Commit the current record so that the multi-line value of the |
592 | + # last key, if any, is saved as a string |
593 | + _commit_key_value_if_needed() |
594 | + # If data is non-empty, yield the record, this allows us to safely |
595 | + # use newlines for formatting |
596 | + if data: |
597 | + yield record |
598 | + # Reset local state so that we can build a new record |
599 | + _new_record() |
600 | + # Treat lines staring with whitespace as multi-line continuation of the |
601 | + # most recently seen key-value |
602 | + elif line.startswith(" "): |
603 | + if key is None: |
604 | + # If we have not seen any keys yet then this is a syntax error |
605 | + raise _syntax_error("Unexpected multi-line value") |
606 | + # Append the current line to the list of values of the most recent |
607 | + # key. This prevents quadratic complexity of string concatenation |
608 | + if line == " .\n": |
609 | + value_list.append(" ") |
610 | + elif line == " ..\n": |
611 | + value_list.append(" .") |
612 | + else: |
613 | + value_list.append(line.rstrip()) |
614 | + # Treat lines with a colon as new key-value pairs |
615 | + elif ":" in line: |
616 | + # Since we have a new, key-value pair we need to commit any |
617 | + # previous key that we may have (regardless of multi-line or |
618 | + # single-line values). |
619 | + _commit_key_value_if_needed() |
620 | + # Parse the line by splitting on the colon, get rid of additional |
621 | + # whitespace from both key and the value |
622 | + key, value = line.split(":", 1) |
623 | + key = key.strip() |
624 | + value = value.strip() |
625 | + # Check if the key already exist in this message |
626 | + if key in record.data: |
627 | + raise _syntax_error(( |
628 | + "Job has a duplicate key {!r} " |
629 | + "with old value {!r} and new value {!r}").format( |
630 | + key, record.data[key], value)) |
631 | + # Construct initial value list out of the (only) value that we have |
632 | + # so far. Additional multi-line values will just append to |
633 | + # value_list |
634 | + value_list = [value] |
635 | + # Treat all other lines as syntax errors |
636 | + else: |
637 | + raise _syntax_error("Unexpected non-empty line") |
638 | + # Make sure to commit the last key from the record |
639 | + _commit_key_value_if_needed() |
640 | + # Once we've seen the whole file return the last record, if any |
641 | + if data: |
642 | + yield record |
643 | + |
644 | + |
645 | +class Runner: |
646 | + """ |
647 | + Runner for jobs |
648 | + |
649 | + Executes the command process and pipes back stdout/stderr |
650 | + """ |
651 | + |
652 | + CHECKBOXES = "/usr/share/checkbox*" |
653 | + |
654 | + def __init__(self, builtin_jobs=[], packages=[]): |
655 | + # List of all available jobs in system-wide locations |
656 | + self.builtin_jobs = builtin_jobs |
657 | + # List of all checkbox variants, like checkbox-oem(-.*)? |
658 | + self.packages = packages |
659 | + |
660 | + def path_expand(self, path): |
661 | + for p in glob.glob(path): |
662 | + self.packages.append(p) |
663 | + for dirpath, dirs, filenames in os.walk(os.path.join(p, 'jobs')): |
664 | + for name in filenames: |
665 | + if name.endswith(".txt"): |
666 | + yield os.path.join(dirpath, name) |
667 | + |
668 | + def main(self, argv=None): |
669 | + parser = argparse.ArgumentParser(prog="checkbox-trusted-launcher") |
670 | + # Argh the horrror! |
671 | + # |
672 | + # Since CPython revision cab204a79e09 (landed for python3.3) |
673 | + # http://hg.python.org/cpython/diff/cab204a79e09/Lib/argparse.py |
674 | + # the argparse module behaves differently than it did in python3.2 |
675 | + # |
676 | + # On python3.2, if we didn't use all the Positional objects, there |
677 | + # were too few arg strings supplied and we called parser.error(). |
678 | + # |
679 | + # To compensate, on python3.3 and beyond, when the user just runs |
680 | + # the trusted launcher without specifying arguments, we manually, |
681 | + # explicitly do what python3.2 did: |
682 | + # call parser.error(_('too few arguments')) |
683 | + if not argv: |
684 | + parser.error(argparse_gettext("too few arguments")) |
685 | + parser.add_argument('HASH', metavar='HASH', help='job hash to match') |
686 | + parser.add_argument( |
687 | + 'ENV', metavar='NAME=VALUE', nargs='*', |
688 | + help='Set each NAME to VALUE in the string environment') |
689 | + parser.add_argument( |
690 | + '--via', |
691 | + metavar='LOCAL-JOB-HASH', |
692 | + dest='via_hash', |
693 | + help='Local job hash to use to match the generated job') |
694 | + args = parser.parse_args(argv) |
695 | + |
696 | + for filename in self.path_expand(self.CHECKBOXES): |
697 | + stream = open(filename, "r", encoding="utf-8") |
698 | + for message in load_rfc822_records(stream): |
699 | + self.builtin_jobs.append(BaseJob(message.data)) |
700 | + stream.close() |
701 | + lookup_list = [j for j in self.builtin_jobs if j.user] |
702 | + |
703 | + if args.via_hash is not None: |
704 | + local_list = [j for j in self.builtin_jobs if j.plugin == 'local'] |
705 | + desired_job_list = [j for j in local_list |
706 | + if j.get_checksum() == args.via_hash] |
707 | + if desired_job_list: |
708 | + via_job = desired_job_list.pop() |
709 | + via_job_result = subprocess.Popen( |
710 | + via_job.command, |
711 | + shell=True, |
712 | + universal_newlines=True, |
713 | + stdout=subprocess.PIPE, |
714 | + env=via_job.modify_execution_environment( |
715 | + args.ENV, |
716 | + self.packages) |
717 | + ) |
718 | + try: |
719 | + for message in load_rfc822_records(via_job_result.stdout): |
720 | + message._data['via'] = args.via_hash |
721 | + lookup_list.append(BaseJob(message.data)) |
722 | + finally: |
723 | + # Always call Popen.wait() in order to avoid zombies |
724 | + via_job_result.stdout.close() |
725 | + via_job_result.wait() |
726 | + |
727 | + try: |
728 | + target_job = [j for j in lookup_list |
729 | + if j.get_checksum() == args.HASH][0] |
730 | + except IndexError: |
731 | + return "Job not found" |
732 | + try: |
733 | + os.execve( |
734 | + '/bin/bash', |
735 | + ['bash', '-c', target_job.command], |
736 | + target_job.modify_execution_environment( |
737 | + args.ENV, |
738 | + self.packages) |
739 | + ) |
740 | + # if execv doesn't fail, it never returns... |
741 | + except OSError: |
742 | + return "Fatal error" |
743 | + finally: |
744 | + return "Fatal error" |
745 | + |
746 | + |
747 | +def main(argv=None): |
748 | + """ |
749 | + Entry point for the checkbox trusted launcher |
750 | + """ |
751 | + runner = Runner() |
752 | + raise SystemExit(runner.main(argv)) |
753 | |
754 | === added file 'plainbox/plainbox/impl/secure/test_checkbox_trusted_launcher.py' |
755 | --- plainbox/plainbox/impl/secure/test_checkbox_trusted_launcher.py 1970-01-01 00:00:00 +0000 |
756 | +++ plainbox/plainbox/impl/secure/test_checkbox_trusted_launcher.py 2013-05-08 07:38:29 +0000 |
757 | @@ -0,0 +1,268 @@ |
758 | +# This file is part of Checkbox. |
759 | +# |
760 | +# Copyright 2013 Canonical Ltd. |
761 | +# Written by: |
762 | +# Sylvain Pineau <sylvain.pineau@canonical.com> |
763 | +# |
764 | +# Checkbox is free software: you can redistribute it and/or modify |
765 | +# it under the terms of the GNU General Public License as published by |
766 | +# the Free Software Foundation, either version 3 of the License, or |
767 | +# (at your option) any later version. |
768 | +# |
769 | +# Checkbox is distributed in the hope that it will be useful, |
770 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
771 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
772 | +# GNU General Public License for more details. |
773 | +# |
774 | +# You should have received a copy of the GNU General Public License |
775 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
776 | + |
777 | +""" |
778 | +plainbox.impl.secure.test_checkbox_trusted_launcher |
779 | +=================================================== |
780 | + |
781 | +Test definitions for plainbox.impl.secure.checkbox_trusted_launcher module |
782 | +""" |
783 | + |
784 | +import os |
785 | + |
786 | +from inspect import cleandoc |
787 | +from io import StringIO |
788 | +from mock import Mock, patch |
789 | +from tempfile import NamedTemporaryFile, TemporaryDirectory |
790 | +from unittest import TestCase |
791 | + |
792 | +from plainbox.impl.secure.checkbox_trusted_launcher import BaseJob |
793 | +from plainbox.impl.secure.checkbox_trusted_launcher import load_rfc822_records |
794 | +from plainbox.impl.secure.checkbox_trusted_launcher import main |
795 | +from plainbox.impl.secure.checkbox_trusted_launcher import Runner |
796 | +from plainbox.impl.test_rfc822 import RFC822ParserTestsMixIn |
797 | +from plainbox.testing_utils.io import TestIO |
798 | +from plainbox.testing_utils.testcases import TestCaseWithParameters |
799 | + |
800 | + |
801 | +class TestJobDefinition(TestCase): |
802 | + |
803 | + def setUp(self): |
804 | + self._full_record = { |
805 | + 'plugin': 'plugin', |
806 | + 'command': 'command', |
807 | + 'environ': 'environ', |
808 | + 'user': 'user' |
809 | + } |
810 | + self._min_record = { |
811 | + 'plugin': 'plugin', |
812 | + 'name': 'name', |
813 | + } |
814 | + |
815 | + def test_smoke_full_record(self): |
816 | + job = BaseJob(self._full_record) |
817 | + self.assertEqual(job.plugin, "plugin") |
818 | + self.assertEqual(job.command, "command") |
819 | + self.assertEqual(job.environ, "environ") |
820 | + self.assertEqual(job.user, "user") |
821 | + |
822 | + def test_smoke_min_record(self): |
823 | + job = BaseJob(self._min_record) |
824 | + self.assertEqual(job.plugin, "plugin") |
825 | + self.assertEqual(job.command, None) |
826 | + self.assertEqual(job.environ, None) |
827 | + self.assertEqual(job.user, None) |
828 | + |
829 | + def test_checksum_smoke(self): |
830 | + job1 = BaseJob({'plugin': 'plugin', 'user': 'root'}) |
831 | + identical_to_job1 = BaseJob({'plugin': 'plugin', 'user': 'root'}) |
832 | + # Two distinct but identical jobs have the same checksum |
833 | + self.assertEqual(job1.get_checksum(), identical_to_job1.get_checksum()) |
834 | + job2 = BaseJob({'plugin': 'plugin', 'user': 'anonymous'}) |
835 | + # Two jobs with different definitions have different checksum |
836 | + self.assertNotEqual(job1.get_checksum(), job2.get_checksum()) |
837 | + # The checksum is stable and does not change over time |
838 | + self.assertEqual( |
839 | + job1.get_checksum(), |
840 | + "c47cc3719061e4df0010d061e6f20d3d046071fd467d02d093a03068d2f33400") |
841 | + |
842 | + |
843 | +class ParsingTests(TestCaseWithParameters): |
844 | + |
845 | + parameter_names = ('glue',) |
846 | + parameter_values = ( |
847 | + ('commas',), |
848 | + ('spaces',), |
849 | + ('tabs',), |
850 | + ('newlines',), |
851 | + ('spaces_and_commas',), |
852 | + ('multiple_spaces',), |
853 | + ('multiple_commas',) |
854 | + ) |
855 | + parameters_keymap = { |
856 | + 'commas': ',', |
857 | + 'spaces': ' ', |
858 | + 'tabs': '\t', |
859 | + 'newlines': '\n', |
860 | + 'spaces_and_commas': ', ', |
861 | + 'multiple_spaces': ' ', |
862 | + 'multiple_commas': ',,,,' |
863 | + } |
864 | + |
865 | + def test_environ_parsing_with_various_separators(self): |
866 | + job = BaseJob({ |
867 | + 'name': 'name', |
868 | + 'plugin': 'plugin', |
869 | + 'environ': self.parameters_keymap[ |
870 | + self.parameters.glue].join(['foo', 'bar', 'froz'])}) |
871 | + expected = set({'foo', 'bar', 'froz'}) |
872 | + observed = job.get_environ_settings() |
873 | + self.assertEqual(expected, observed) |
874 | + |
875 | + def test_environ_parsing_empty(self): |
876 | + job = BaseJob({'plugin': 'plugin'}) |
877 | + expected = set() |
878 | + observed = job.get_environ_settings() |
879 | + self.assertEqual(expected, observed) |
880 | + |
881 | + |
882 | +class JobEnvTests(TestCase): |
883 | + |
884 | + def setUp(self): |
885 | + self.job = BaseJob({'plugin': 'plugin', 'environ': 'foo'}) |
886 | + |
887 | + def test_checkbox_env(self): |
888 | + base_env = {"PATH": "", 'foo': 'bar', 'baz': 'qux'} |
889 | + path = '/usr/share/checkbox-lambda' |
890 | + with patch.dict('os.environ', {}): |
891 | + env = self.job.modify_execution_environment(base_env, [path]) |
892 | + self.assertEqual(env['CHECKBOX_LAMBDA_SHARE'], path) |
893 | + self.assertEqual(env['foo'], 'bar') |
894 | + self.assertNotIn('bar', env) |
895 | + |
896 | + def test_checkbox_env_with_data_dir(self): |
897 | + base_env = {"PATH": "", "CHECKBOX_DATA": "DEADBEEF"} |
898 | + path = '/usr/share/checkbox-lambda' |
899 | + with patch.dict('os.environ', {"CHECKBOX_DATA": "DEADBEEF"}): |
900 | + env = self.job.modify_execution_environment(base_env, [path]) |
901 | + self.assertEqual(env['CHECKBOX_LAMBDA_SHARE'], path) |
902 | + self.assertEqual(env['CHECKBOX_DATA'], "DEADBEEF") |
903 | + |
904 | + |
905 | +class RFC822ParserTests(TestCase, RFC822ParserTestsMixIn): |
906 | + |
907 | + @classmethod |
908 | + def setUpClass(cls): |
909 | + cls.loader = load_rfc822_records |
910 | + |
911 | + |
912 | +class TestMain(TestCase): |
913 | + |
914 | + def setUp(self): |
915 | + self.scratch_dir = TemporaryDirectory(prefix='checkbox-') |
916 | + job_dir = os.path.join(self.scratch_dir.name, 'jobs') |
917 | + os.mkdir(job_dir) |
918 | + with NamedTemporaryFile(suffix='.txt', dir=job_dir, delete=False) as f: |
919 | + f.write(b'plugin: plugin\nuser: me\ncommand: true\n') |
920 | + with NamedTemporaryFile( |
921 | + suffix='.txt', dir=job_dir, delete=False) as f: |
922 | + f.write(b'plugin: unknown\nuser: user\n') |
923 | + with NamedTemporaryFile( |
924 | + suffix='.txt', dir=job_dir, delete=False) as f: |
925 | + f.write(b'plugin: local\nuser: you\ncommand: a new job\n') |
926 | + with NamedTemporaryFile( |
927 | + suffix='.conf', dir=job_dir, delete=False) as f: |
928 | + f.write(b'plugin: local\nuser: you\ncommand: a new job\n') |
929 | + |
930 | + def test_help(self): |
931 | + with TestIO(combined=True) as io: |
932 | + with self.assertRaises(SystemExit) as call: |
933 | + main(['--help']) |
934 | + self.assertEqual(call.exception.args, (0,)) |
935 | + self.maxDiff = None |
936 | + expected = """ |
937 | + usage: checkbox-trusted-launcher [-h] [--via LOCAL-JOB-HASH] |
938 | + HASH [NAME=VALUE [NAME=VALUE ...]] |
939 | + |
940 | + positional arguments: |
941 | + HASH job hash to match |
942 | + NAME=VALUE Set each NAME to VALUE in the string environment |
943 | + |
944 | + optional arguments: |
945 | + -h, --help show this help message and exit |
946 | + --via LOCAL-JOB-HASH Local job hash to use to match the generated job |
947 | + """ |
948 | + self.assertEqual(io.combined, cleandoc(expected) + "\n") |
949 | + |
950 | + def test_run_without_args(self): |
951 | + with TestIO(combined=True) as io: |
952 | + with self.assertRaises(SystemExit) as call: |
953 | + main([]) |
954 | + self.assertEqual(call.exception.args, (2,)) |
955 | + expected = """ |
956 | + usage: checkbox-trusted-launcher [-h] |
957 | + checkbox-trusted-launcher: error: too few arguments |
958 | + """ |
959 | + self.assertEqual(io.combined, cleandoc(expected) + "\n") |
960 | + |
961 | + @patch.object(Runner, 'CHECKBOXES', 'foo') |
962 | + def test_run_invalid_hash(self): |
963 | + with TestIO(combined=True) as io: |
964 | + with self.assertRaises(SystemExit) as call: |
965 | + main(['bar']) |
966 | + self.assertEqual(call.exception.args, ('Job not found',)) |
967 | + self.assertEqual(io.combined, '') |
968 | + |
969 | + def test_run_valid_hash(self): |
970 | + self.maxDiff = None |
971 | + with patch.object(Runner, 'CHECKBOXES', self.scratch_dir.name),\ |
972 | + patch('os.execve'): |
973 | + with TestIO(combined=True) as io: |
974 | + with self.assertRaises(SystemExit) as call: |
975 | + main(['9ab0e98cce8866b9a2fa217e87b4e8bb' |
976 | + '739e5f74977ba5fa30822cab2a178c48']) |
977 | + self.assertEqual(call.exception.args, ('Fatal error',)) |
978 | + self.assertEqual(io.combined, '') |
979 | + |
980 | + def test_run_valid_hash_exec_error(self): |
981 | + self.maxDiff = None |
982 | + with patch.object(Runner, 'CHECKBOXES', self.scratch_dir.name),\ |
983 | + patch('os.execve') as mock_execve: |
984 | + mock_execve.side_effect = OSError('foo') |
985 | + with TestIO(combined=True) as io: |
986 | + with self.assertRaises(SystemExit) as call: |
987 | + main(['9ab0e98cce8866b9a2fa217e87b4e8bb' |
988 | + '739e5f74977ba5fa30822cab2a178c48']) |
989 | + self.assertEqual(call.exception.args, ('Fatal error',)) |
990 | + self.assertEqual(io.combined, '') |
991 | + |
992 | + def test_run_valid_hash_invalid_via(self): |
993 | + self.maxDiff = None |
994 | + with patch.object(Runner, 'CHECKBOXES', self.scratch_dir.name),\ |
995 | + patch('os.execve'),\ |
996 | + patch('subprocess.Popen') as mock_popen: |
997 | + mock_iobuffer = Mock() |
998 | + mock_iobuffer.stdout = StringIO('test: me') |
999 | + mock_popen.return_value = mock_iobuffer |
1000 | + with TestIO(combined=True) as io: |
1001 | + with self.assertRaises(SystemExit) as call: |
1002 | + main(['9ab0e98cce8866b9a2fa217e87b4e8bb' |
1003 | + '739e5f74977ba5fa30822cab2a178c48', '--via=maybe']) |
1004 | + self.assertEqual(call.exception.args, ('Fatal error',)) |
1005 | + self.assertEqual(io.combined, '') |
1006 | + |
1007 | + def test_run_valid_hash_valid_via(self): |
1008 | + self.maxDiff = None |
1009 | + with patch.object(Runner, 'CHECKBOXES', self.scratch_dir.name),\ |
1010 | + patch('os.execve'),\ |
1011 | + patch('subprocess.Popen') as mock_popen: |
1012 | + mock_iobuffer = Mock() |
1013 | + mock_iobuffer.stdout = StringIO('test: me') |
1014 | + mock_popen.return_value = mock_iobuffer |
1015 | + with TestIO(combined=True) as io: |
1016 | + with self.assertRaises(SystemExit) as call: |
1017 | + main(['9ab0e98cce8866b9a2fa217e87b4e8bb' |
1018 | + '739e5f74977ba5fa30822cab2a178c48', |
1019 | + '--via=a0dc4a9673b8f2d80b1ae4775c' |
1020 | + '03e8a777eefa061fc7ecf7a3af2f20a33bb177']) |
1021 | + self.assertEqual(call.exception.args, ('Fatal error',)) |
1022 | + self.assertEqual(io.combined, '') |
1023 | + |
1024 | + def tearDown(self): |
1025 | + self.scratch_dir.cleanup() |
1026 | |
1027 | === modified file 'plainbox/plainbox/impl/test_rfc822.py' |
1028 | --- plainbox/plainbox/impl/test_rfc822.py 2013-03-14 10:36:25 +0000 |
1029 | +++ plainbox/plainbox/impl/test_rfc822.py 2013-05-08 07:38:29 +0000 |
1030 | @@ -29,9 +29,9 @@ |
1031 | |
1032 | from plainbox.impl.rfc822 import Origin |
1033 | from plainbox.impl.rfc822 import RFC822Record |
1034 | -from plainbox.impl.rfc822 import RFC822SyntaxError |
1035 | from plainbox.impl.rfc822 import load_rfc822_records |
1036 | from plainbox.impl.rfc822 import dump_rfc822_records |
1037 | +from plainbox.impl.secure.checkbox_trusted_launcher import RFC822SyntaxError |
1038 | |
1039 | |
1040 | class OriginTests(TestCase): |
1041 | @@ -65,16 +65,18 @@ |
1042 | self.assertEqual(record.origin, origin) |
1043 | |
1044 | |
1045 | -class RFC822ParserTests(TestCase): |
1046 | +class RFC822ParserTestsMixIn(): |
1047 | + |
1048 | + loader = load_rfc822_records |
1049 | |
1050 | def test_empty(self): |
1051 | with StringIO("") as stream: |
1052 | - records = load_rfc822_records(stream) |
1053 | + records = type(self).loader(stream) |
1054 | self.assertEqual(len(records), 0) |
1055 | |
1056 | def test_single_record(self): |
1057 | with StringIO("key:value") as stream: |
1058 | - records = load_rfc822_records(stream) |
1059 | + records = type(self).loader(stream) |
1060 | self.assertEqual(len(records), 1) |
1061 | self.assertEqual(records[0].data, {'key': 'value'}) |
1062 | |
1063 | @@ -94,7 +96,7 @@ |
1064 | "\n" |
1065 | ) |
1066 | with StringIO(text) as stream: |
1067 | - records = load_rfc822_records(stream) |
1068 | + records = type(self).loader(stream) |
1069 | self.assertEqual(len(records), 3) |
1070 | self.assertEqual(records[0].data, {'key1': 'value1'}) |
1071 | self.assertEqual(records[1].data, {'key2': 'value2'}) |
1072 | @@ -109,7 +111,7 @@ |
1073 | "key3:value3\n" |
1074 | ) |
1075 | with StringIO(text) as stream: |
1076 | - records = load_rfc822_records(stream) |
1077 | + records = type(self).loader(stream) |
1078 | self.assertEqual(len(records), 3) |
1079 | self.assertEqual(records[0].data, {'key1': 'value1'}) |
1080 | self.assertEqual(records[1].data, {'key2': 'value2'}) |
1081 | @@ -122,7 +124,7 @@ |
1082 | " value\n" |
1083 | ) |
1084 | with StringIO(text) as stream: |
1085 | - records = load_rfc822_records(stream) |
1086 | + records = type(self).loader(stream) |
1087 | self.assertEqual(len(records), 1) |
1088 | self.assertEqual(records[0].data, {'key': 'longer\nvalue'}) |
1089 | |
1090 | @@ -134,7 +136,7 @@ |
1091 | " value\n" |
1092 | ) |
1093 | with StringIO(text) as stream: |
1094 | - records = load_rfc822_records(stream) |
1095 | + records = type(self).loader(stream) |
1096 | self.assertEqual(len(records), 1) |
1097 | self.assertEqual(records[0].data, {'key': 'longer\n\nvalue'}) |
1098 | |
1099 | @@ -146,7 +148,7 @@ |
1100 | " value\n" |
1101 | ) |
1102 | with StringIO(text) as stream: |
1103 | - records = load_rfc822_records(stream) |
1104 | + records = type(self).loader(stream) |
1105 | self.assertEqual(len(records), 1) |
1106 | self.assertEqual(records[0].data, {'key': 'longer\n.\nvalue'}) |
1107 | |
1108 | @@ -161,7 +163,7 @@ |
1109 | " value 2\n" |
1110 | ) |
1111 | with StringIO(text) as stream: |
1112 | - records = load_rfc822_records(stream) |
1113 | + records = type(self).loader(stream) |
1114 | self.assertEqual(len(records), 2) |
1115 | self.assertEqual(records[0].data, {'key1': 'initial\nlonger\nvalue 1'}) |
1116 | self.assertEqual(records[1].data, {'key2': 'longer\nvalue 2'}) |
1117 | @@ -169,7 +171,7 @@ |
1118 | def test_irrelevant_whitespace(self): |
1119 | text = "key : value " |
1120 | with StringIO(text) as stream: |
1121 | - records = load_rfc822_records(stream) |
1122 | + records = type(self).loader(stream) |
1123 | self.assertEqual(len(records), 1) |
1124 | self.assertEqual(records[0].data, {'key': 'value'}) |
1125 | |
1126 | @@ -179,7 +181,7 @@ |
1127 | " value\n" |
1128 | ) |
1129 | with StringIO(text) as stream: |
1130 | - records = load_rfc822_records(stream) |
1131 | + records = type(self).loader(stream) |
1132 | self.assertEqual(len(records), 1) |
1133 | self.assertEqual(records[0].data, {'key': 'value'}) |
1134 | |
1135 | @@ -187,21 +189,21 @@ |
1136 | text = " extra value" |
1137 | with StringIO(text) as stream: |
1138 | with self.assertRaises(RFC822SyntaxError) as call: |
1139 | - load_rfc822_records(stream) |
1140 | + type(self).loader(stream) |
1141 | self.assertEqual(call.exception.msg, "Unexpected multi-line value") |
1142 | |
1143 | def test_garbage(self): |
1144 | text = "garbage" |
1145 | with StringIO(text) as stream: |
1146 | with self.assertRaises(RFC822SyntaxError) as call: |
1147 | - load_rfc822_records(stream) |
1148 | + type(self).loader(stream) |
1149 | self.assertEqual(call.exception.msg, "Unexpected non-empty line") |
1150 | |
1151 | def test_syntax_error(self): |
1152 | text = "key1 = value1" |
1153 | with StringIO(text) as stream: |
1154 | with self.assertRaises(RFC822SyntaxError) as call: |
1155 | - load_rfc822_records(stream) |
1156 | + type(self).loader(stream) |
1157 | self.assertEqual(call.exception.msg, "Unexpected non-empty line") |
1158 | |
1159 | def test_duplicate_error(self): |
1160 | @@ -211,12 +213,17 @@ |
1161 | ) |
1162 | with StringIO(text) as stream: |
1163 | with self.assertRaises(RFC822SyntaxError) as call: |
1164 | - load_rfc822_records(stream) |
1165 | + type(self).loader(stream) |
1166 | self.assertEqual(call.exception.msg, ( |
1167 | "Job has a duplicate key 'key1' with old value 'value1'" |
1168 | " and new value 'value2'")) |
1169 | |
1170 | |
1171 | +class RFC822ParserTests(TestCase, RFC822ParserTestsMixIn): |
1172 | + |
1173 | + pass |
1174 | + |
1175 | + |
1176 | class RFC822WriterTests(TestCase): |
1177 | |
1178 | def test_single_record(self): |
1179 | |
1180 | === modified file 'plainbox/setup.py' |
1181 | --- plainbox/setup.py 2013-04-25 20:43:24 +0000 |
1182 | +++ plainbox/setup.py 2013-05-08 07:38:29 +0000 |
1183 | @@ -40,9 +40,15 @@ |
1184 | 'lxml >= 2.3', |
1185 | 'requests >= 1.0', |
1186 | ], |
1187 | + data_files=[ |
1188 | + ("share/polkit-1/actions", |
1189 | + ["plainbox/data/org.freedesktop.policykit.pkexec.policy"]) |
1190 | + ], |
1191 | entry_points={ |
1192 | 'console_scripts': [ |
1193 | 'plainbox=plainbox.public:main', |
1194 | + 'checkbox-trusted-launcher=' |
1195 | + 'plainbox.impl.secure.checkbox_trusted_launcher:main', |
1196 | ], |
1197 | 'plainbox.exporter': [ |
1198 | 'text=plainbox.impl.exporter.text:TextSessionStateExporter', |
1199 | @@ -51,7 +57,8 @@ |
1200 | 'xml=plainbox.impl.exporter.xml:XMLSessionStateExporter', |
1201 | ], |
1202 | 'plainbox.transport': [ |
1203 | - 'certification=plainbox.impl.transport.certification:CertificationTransport', |
1204 | + 'certification=' |
1205 | + 'plainbox.impl.transport.certification:CertificationTransport', |
1206 | ], |
1207 | }, |
1208 | include_package_data=True) |
The standalone launcher looks pretty good. I need to look at the other bits and read it in more detail.
I'll send a second review after fixing trunk now.