Merge lp:~snappy-dev/click/default-apparmor into lp:~snappy-dev/click/snappy
- default-apparmor
- Merge into snappy
Status: | Needs review |
---|---|
Proposed branch: | lp:~snappy-dev/click/default-apparmor |
Merge into: | lp:~snappy-dev/click/snappy |
Diff against target: |
345 lines (+254/-25) 2 files modified
click/build.py (+33/-5) click/tests/test_build.py (+221/-20) |
To merge this branch: | bzr merge lp:~snappy-dev/click/default-apparmor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Vogt (community) | Approve | ||
Jamie Strandboge | Pending | ||
Review via email: mp+243609@code.launchpad.net |
Commit message
Description of the change
snappy build needs to generate default apparmor profile automatically
Jamie Strandboge (jdstrand) wrote : | # |
Do you have debs available for testing?
Barry Warsaw (barry) wrote : | # |
> Without testing the code, this looks reasonable. I can say that the hardcoding
> of the policy_version is something we won't want long term. We'll need to have
> click-apparmor expose that some how so it can be consumed by click.
Will the rest of the file contents potentially change, or just the policy_version? If it's just the latter and we can determine the version at build-time, we can easily substitute that in. Actually, if the entire file contents is available somewhere we can pretty easily read that in at build time.
Barry Warsaw (barry) wrote : | # |
> Do you have debs available for testing?
Not yet. I was hoping that once Michael reviews he can set that up.
Michael Vogt (mvo) wrote : | # |
Looks great, thanks! And extra credits for the tests, yay! I merged it into the yaml-manifest branch and it will go to the PPA next, I did a tiny change to name APPARMOR_
Michael Vogt (mvo) wrote : | # |
I just uploaded the new click to the ppa:snappy-
Jamie Strandboge (jdstrand) wrote : | # |
On 12/03/2014 06:41 PM, Barry Warsaw wrote:
>> Without testing the code, this looks reasonable. I can say that the hardcoding
>> of the policy_version is something we won't want long term. We'll need to have
>> click-apparmor expose that some how so it can be consumed by click.
>
> Will the rest of the file contents potentially change, or just the policy_version?
> If it's just the latter and we can determine the version at build-time, we can
> easily substitute that in. Actually, if the entire file contents is available
> somewhere we can pretty easily read that in at build time.
Well,the contents of the file are up to us-- we can define the defaults. Dealing
with the policy_version requires further thought and discussion since it has
been changing based on the Ubuntu release (eg, 1.2 is 14.10, 1.1 14.04). That is
just convention though-- there is no reason why it has to be that way, but it
will definitely change if we want to remove policy groups or templates, or if
new policy groups and templates need to use new apparmor syntax.
--
Jamie Strandboge | http://
Unmerged revisions
- 574. By Barry Warsaw
-
Create default apparmor profiles if the yaml doesn't explicitly name
integration:<app>:apparmor{ ,-profile} With tests!
Preview Diff
1 | === modified file 'click/build.py' | |||
2 | --- click/build.py 2014-12-03 15:46:34 +0000 | |||
3 | +++ click/build.py 2014-12-03 23:21:56 +0000 | |||
4 | @@ -62,6 +62,18 @@ | |||
5 | 62 | ) | 62 | ) |
6 | 63 | 63 | ||
7 | 64 | 64 | ||
8 | 65 | APPARMOR_DEFAULT_TEMPLATE = """\ | ||
9 | 66 | { | ||
10 | 67 | "template": "default", | ||
11 | 68 | "policy_groups": [ | ||
12 | 69 | "networking" | ||
13 | 70 | ], | ||
14 | 71 | "policy_vendor": "ubuntu-snappy", | ||
15 | 72 | "policy_version": 1.3 | ||
16 | 73 | } | ||
17 | 74 | """ | ||
18 | 75 | |||
19 | 76 | |||
20 | 65 | @contextlib.contextmanager | 77 | @contextlib.contextmanager |
21 | 66 | def make_temp_dir(): | 78 | def make_temp_dir(): |
22 | 67 | temp_dir = tempfile.mkdtemp(prefix="click") | 79 | temp_dir = tempfile.mkdtemp(prefix="click") |
23 | @@ -89,6 +101,7 @@ | |||
24 | 89 | class ClickBuilderBase: | 101 | class ClickBuilderBase: |
25 | 90 | def __init__(self): | 102 | def __init__(self): |
26 | 91 | self.file_map = {} | 103 | self.file_map = {} |
27 | 104 | self.autogen_apparmor = [] | ||
28 | 92 | 105 | ||
29 | 93 | def add_file(self, source_path, dest_path): | 106 | def add_file(self, source_path, dest_path): |
30 | 94 | self.file_map[source_path] = dest_path | 107 | self.file_map[source_path] = dest_path |
31 | @@ -136,18 +149,28 @@ | |||
32 | 136 | # deal with new "services" field | 149 | # deal with new "services" field |
33 | 137 | hooks = self.manifest.get("hooks", {}) | 150 | hooks = self.manifest.get("hooks", {}) |
34 | 138 | for service in self.manifest.get("services", []): | 151 | for service in self.manifest.get("services", []): |
37 | 139 | current_hook = hooks.get(service["name"], {}) | 152 | name = service["name"] |
38 | 140 | snappy_systemd = "%s.snappy-systemd" % service["name"] | 153 | current_hook = hooks.get(name, {}) |
39 | 154 | snappy_systemd = "{}.snappy-systemd".format(name) | ||
40 | 141 | description = service.get( | 155 | description = service.get( |
41 | 142 | "description", | 156 | "description", |
42 | 143 | self.manifest.get("description", "no description")) | 157 | self.manifest.get("description", "no description")) |
45 | 144 | with open(os.path.join( | 158 | systemd_path = os.path.join( |
46 | 145 | os.path.dirname(manifest_path), snappy_systemd), "w") as fp: | 159 | os.path.dirname(manifest_path), snappy_systemd) |
47 | 160 | with open(systemd_path, "w") as fp: | ||
48 | 146 | yaml.dump({"description": description, | 161 | yaml.dump({"description": description, |
49 | 147 | "start": service["start"], | 162 | "start": service["start"], |
50 | 148 | }, fp) | 163 | }, fp) |
51 | 149 | current_hook["snappy-systemd"] = "meta/"+snappy_systemd | 164 | current_hook["snappy-systemd"] = "meta/"+snappy_systemd |
53 | 150 | hooks[service["name"]] = current_hook | 165 | hooks[name] = current_hook |
54 | 166 | # Is there an explicit apparmor hook? If so, use it otherwise | ||
55 | 167 | # generate a default one. | ||
56 | 168 | if ( 'apparmor' not in current_hook and | ||
57 | 169 | 'apparmor-profile' not in current_hook): | ||
58 | 170 | template_path = os.path.join( | ||
59 | 171 | 'meta', '{}.apparmor'.format(name)) | ||
60 | 172 | current_hook['apparmor'] = template_path | ||
61 | 173 | self.autogen_apparmor.append(template_path) | ||
62 | 151 | self.manifest["hooks"] = hooks | 174 | self.manifest["hooks"] = hooks |
63 | 152 | # cleanup | 175 | # cleanup |
64 | 153 | if "services" in self.manifest: | 176 | if "services" in self.manifest: |
65 | @@ -316,6 +339,11 @@ | |||
66 | 316 | if "framework" in self.manifest: | 339 | if "framework" in self.manifest: |
67 | 317 | self._validate_framework(self.manifest["framework"]) | 340 | self._validate_framework(self.manifest["framework"]) |
68 | 318 | 341 | ||
69 | 342 | for template_path in self.autogen_apparmor: | ||
70 | 343 | path = os.path.join(root_path, template_path) | ||
71 | 344 | with io.open(path, 'w', encoding='utf-8') as fp: | ||
72 | 345 | fp.write(APPARMOR_DEFAULT_TEMPLATE) | ||
73 | 346 | |||
74 | 319 | du_output = subprocess.check_output( | 347 | du_output = subprocess.check_output( |
75 | 320 | ["du", "-k", "-s", "--apparent-size", "."], | 348 | ["du", "-k", "-s", "--apparent-size", "."], |
76 | 321 | cwd=temp_dir, universal_newlines=True).rstrip("\n") | 349 | cwd=temp_dir, universal_newlines=True).rstrip("\n") |
77 | 322 | 350 | ||
78 | === modified file 'click/tests/test_build.py' | |||
79 | --- click/tests/test_build.py 2014-12-02 15:34:43 +0000 | |||
80 | +++ click/tests/test_build.py 2014-12-03 23:21:56 +0000 | |||
81 | @@ -342,31 +342,82 @@ | |||
82 | 342 | self.assertTrue(tar.getmember("./bin/foo").isfile()) | 342 | self.assertTrue(tar.getmember("./bin/foo").isfile()) |
83 | 343 | 343 | ||
84 | 344 | 344 | ||
85 | 345 | SIMPLE_TEMPLATE = """\ | ||
86 | 346 | name: test-yaml | ||
87 | 347 | icon: data/icon.svg | ||
88 | 348 | version: 1.0 | ||
89 | 349 | maintainer: Foo Bar <foo@example.org> | ||
90 | 350 | architecture: all | ||
91 | 351 | framework: ubuntu-core-15.04 | ||
92 | 352 | integration: | ||
93 | 353 | app1: | ||
94 | 354 | hookname: hook-file-name | ||
95 | 355 | services: | ||
96 | 356 | - name: app1 | ||
97 | 357 | description: some description | ||
98 | 358 | start: start-something | ||
99 | 359 | stop: stop-something | ||
100 | 360 | binaries: | ||
101 | 361 | - name: path/to/cli1 | ||
102 | 362 | """ | ||
103 | 363 | |||
104 | 364 | APPARMOR_1_TEMPLATE = """\ | ||
105 | 365 | name: test-yaml | ||
106 | 366 | icon: data/icon.svg | ||
107 | 367 | version: 1.0 | ||
108 | 368 | maintainer: Foo Bar <foo@example.org> | ||
109 | 369 | architecture: all | ||
110 | 370 | framework: ubuntu-core-15.04 | ||
111 | 371 | integration: | ||
112 | 372 | app1: | ||
113 | 373 | hookname: hook-file-name | ||
114 | 374 | apparmor: path/to/app1.apparmor | ||
115 | 375 | services: | ||
116 | 376 | - name: app1 | ||
117 | 377 | description: some description | ||
118 | 378 | start: start-something | ||
119 | 379 | stop: stop-something | ||
120 | 380 | - name: app2 | ||
121 | 381 | description: some description | ||
122 | 382 | start: start-something | ||
123 | 383 | stop: stop-something | ||
124 | 384 | binaries: | ||
125 | 385 | - name: path/to/cli1 | ||
126 | 386 | """ | ||
127 | 387 | |||
128 | 388 | APPARMOR_2_TEMPLATE = """\ | ||
129 | 389 | name: test-yaml | ||
130 | 390 | icon: data/icon.svg | ||
131 | 391 | version: 1.0 | ||
132 | 392 | maintainer: Foo Bar <foo@example.org> | ||
133 | 393 | architecture: all | ||
134 | 394 | framework: ubuntu-core-15.04 | ||
135 | 395 | integration: | ||
136 | 396 | app1: | ||
137 | 397 | hookname: hook-file-name | ||
138 | 398 | apparmor-profile: path/to/app1.apparmor-profile | ||
139 | 399 | services: | ||
140 | 400 | - name: app1 | ||
141 | 401 | description: some description | ||
142 | 402 | start: start-something | ||
143 | 403 | stop: stop-something | ||
144 | 404 | - name: app2 | ||
145 | 405 | description: some description | ||
146 | 406 | start: start-something | ||
147 | 407 | stop: stop-something | ||
148 | 408 | binaries: | ||
149 | 409 | - name: path/to/cli1 | ||
150 | 410 | """ | ||
151 | 411 | |||
152 | 412 | |||
153 | 345 | class TestClickBuildYaml(TestCase): | 413 | class TestClickBuildYaml(TestCase): |
154 | 346 | def setUp(self): | 414 | def setUp(self): |
155 | 347 | super(TestClickBuildYaml, self).setUp() | 415 | super(TestClickBuildYaml, self).setUp() |
156 | 348 | self.builder = ClickBuilder() | 416 | self.builder = ClickBuilder() |
157 | 349 | 417 | ||
159 | 350 | def _make_metadata(self, scratch): | 418 | def _make_metadata(self, scratch, template=SIMPLE_TEMPLATE): |
160 | 351 | with mkfile(os.path.join(scratch, "meta", "package.yaml")) as f: | 419 | with mkfile(os.path.join(scratch, "meta", "package.yaml")) as f: |
179 | 352 | f.write(dedent("""\ | 420 | f.write(dedent(template)) |
162 | 353 | name: test-yaml | ||
163 | 354 | icon: data/icon.svg | ||
164 | 355 | version: 1.0 | ||
165 | 356 | maintainer: Foo Bar <foo@example.org> | ||
166 | 357 | architecture: all | ||
167 | 358 | framework: ubuntu-core-15.04 | ||
168 | 359 | integration: | ||
169 | 360 | app1: | ||
170 | 361 | hookname: hook-file-name | ||
171 | 362 | services: | ||
172 | 363 | - name: app1 | ||
173 | 364 | description: some description | ||
174 | 365 | start: start-something | ||
175 | 366 | stop: stop-something | ||
176 | 367 | binaries: | ||
177 | 368 | - name: path/to/cli1 | ||
178 | 369 | """)) | ||
180 | 370 | with mkfile(os.path.join(scratch, "meta", "readme.md")) as f: | 421 | with mkfile(os.path.join(scratch, "meta", "readme.md")) as f: |
181 | 371 | f.write(dedent("""test title | 422 | f.write(dedent("""test title |
182 | 372 | 423 | ||
183 | @@ -401,7 +452,8 @@ | |||
184 | 401 | click_contents = subprocess.check_output( | 452 | click_contents = subprocess.check_output( |
185 | 402 | ["dpkg-deb", "-c", actual_path], universal_newlines=True) | 453 | ["dpkg-deb", "-c", actual_path], universal_newlines=True) |
186 | 403 | for file_path in ("./data/icon.svg", "./meta/package.yaml", | 454 | for file_path in ("./data/icon.svg", "./meta/package.yaml", |
188 | 404 | "./meta/readme.md", "./bin/foo"): | 455 | "./meta/readme.md", "./bin/foo", |
189 | 456 | "./meta/app1.apparmor"): | ||
190 | 405 | self.assertIn(file_path, click_contents) | 457 | self.assertIn(file_path, click_contents) |
191 | 406 | 458 | ||
192 | 407 | def test_read_manifest(self): | 459 | def test_read_manifest(self): |
193 | @@ -422,3 +474,152 @@ | |||
194 | 422 | self.assertEqual( | 474 | self.assertEqual( |
195 | 423 | "path/to/cli1", | 475 | "path/to/cli1", |
196 | 424 | self.builder.manifest["hooks"]["cli1"]["bin-path"]) | 476 | self.builder.manifest["hooks"]["cli1"]["bin-path"]) |
197 | 477 | self.assertEqual( | ||
198 | 478 | "meta/app1.apparmor", | ||
199 | 479 | self.builder.manifest["hooks"]["app1"]["apparmor"]) | ||
200 | 480 | |||
201 | 481 | def test_one_explicit_apparmor(self): | ||
202 | 482 | self.use_temp_dir() | ||
203 | 483 | self._make_metadata(self.temp_dir, APPARMOR_1_TEMPLATE) | ||
204 | 484 | self.builder.read_manifest(self.temp_dir + "/meta/package.yaml") | ||
205 | 485 | self.assertEqual( | ||
206 | 486 | "path/to/app1.apparmor", | ||
207 | 487 | self.builder.manifest["hooks"]["app1"]["apparmor"]) | ||
208 | 488 | self.assertEqual( | ||
209 | 489 | "meta/app2.apparmor", | ||
210 | 490 | self.builder.manifest["hooks"]["app2"]["apparmor"]) | ||
211 | 491 | |||
212 | 492 | def test_one_explicit_apparmor_profile(self): | ||
213 | 493 | self.use_temp_dir() | ||
214 | 494 | self._make_metadata(self.temp_dir, APPARMOR_2_TEMPLATE) | ||
215 | 495 | self.builder.read_manifest(self.temp_dir + "/meta/package.yaml") | ||
216 | 496 | self.assertEqual( | ||
217 | 497 | "path/to/app1.apparmor-profile", | ||
218 | 498 | self.builder.manifest["hooks"]["app1"]["apparmor-profile"]) | ||
219 | 499 | self.assertIsNone( | ||
220 | 500 | self.builder.manifest["hooks"]["app1"].get('apparmor')) | ||
221 | 501 | self.assertEqual( | ||
222 | 502 | "meta/app2.apparmor", | ||
223 | 503 | self.builder.manifest["hooks"]["app2"]["apparmor"]) | ||
224 | 504 | |||
225 | 505 | def test_one_explicit_apparmor_file(self): | ||
226 | 506 | self.use_temp_dir() | ||
227 | 507 | scratch = os.path.join(self.temp_dir, "scratch") | ||
228 | 508 | touch(os.path.join(scratch, "bin", "foo")) | ||
229 | 509 | touch(os.path.join(scratch, "data", "icon.svg")) | ||
230 | 510 | apparmor_path = os.path.join(scratch, 'path', 'to', 'app1.apparmor') | ||
231 | 511 | os.makedirs(os.path.dirname(apparmor_path)) | ||
232 | 512 | with open(apparmor_path, 'w', encoding='utf-8') as fp: | ||
233 | 513 | fp.write(dedent("""\ | ||
234 | 514 | { | ||
235 | 515 | "template": "default", | ||
236 | 516 | "policy_groups": [ | ||
237 | 517 | "networking" | ||
238 | 518 | ], | ||
239 | 519 | "policy_vendor": "ubuntu-snappy", | ||
240 | 520 | "policy_version": 99.99 | ||
241 | 521 | } | ||
242 | 522 | """)) | ||
243 | 523 | self._make_metadata(scratch, APPARMOR_1_TEMPLATE) | ||
244 | 524 | self.builder.add_file(scratch, "./") | ||
245 | 525 | # FIXME: make it a .snap | ||
246 | 526 | expected_path = os.path.join(self.temp_dir, "test-yaml_1.0_all.click") | ||
247 | 527 | actual_path = self.builder.build(self.temp_dir, "meta/package.yaml") | ||
248 | 528 | self.assertEqual(expected_path, actual_path) | ||
249 | 529 | self.assertTrue(os.path.exists(actual_path)) | ||
250 | 530 | contents = os.path.join(self.temp_dir, 'contents') | ||
251 | 531 | subprocess.check_output( | ||
252 | 532 | ["dpkg-deb", "-R", actual_path, contents], universal_newlines=True) | ||
253 | 533 | self.assertEqual(sorted(os.listdir(contents)), | ||
254 | 534 | ['DEBIAN', 'bin', 'data', 'meta', 'path']) | ||
255 | 535 | self.assertEqual( | ||
256 | 536 | sorted(os.listdir(os.path.join(contents, 'path', 'to'))), | ||
257 | 537 | ['app1.apparmor']) | ||
258 | 538 | with open(os.path.join(contents, 'path', 'to', 'app1.apparmor'), | ||
259 | 539 | encoding='utf-8') as fp: | ||
260 | 540 | data = fp.read() | ||
261 | 541 | self.assertEqual(data, dedent("""\ | ||
262 | 542 | { | ||
263 | 543 | "template": "default", | ||
264 | 544 | "policy_groups": [ | ||
265 | 545 | "networking" | ||
266 | 546 | ], | ||
267 | 547 | "policy_vendor": "ubuntu-snappy", | ||
268 | 548 | "policy_version": 99.99 | ||
269 | 549 | } | ||
270 | 550 | """)) | ||
271 | 551 | with open(os.path.join(contents, 'meta', 'app2.apparmor'), | ||
272 | 552 | encoding='utf-8') as fp: | ||
273 | 553 | data = fp.read() | ||
274 | 554 | self.assertEqual(data, dedent("""\ | ||
275 | 555 | { | ||
276 | 556 | "template": "default", | ||
277 | 557 | "policy_groups": [ | ||
278 | 558 | "networking" | ||
279 | 559 | ], | ||
280 | 560 | "policy_vendor": "ubuntu-snappy", | ||
281 | 561 | "policy_version": 1.3 | ||
282 | 562 | } | ||
283 | 563 | """)) | ||
284 | 564 | |||
285 | 565 | def test_one_explicit_apparmor_profile_file(self): | ||
286 | 566 | self.use_temp_dir() | ||
287 | 567 | scratch = os.path.join(self.temp_dir, "scratch") | ||
288 | 568 | touch(os.path.join(scratch, "bin", "foo")) | ||
289 | 569 | touch(os.path.join(scratch, "data", "icon.svg")) | ||
290 | 570 | apparmor_path = os.path.join( | ||
291 | 571 | scratch, 'path', 'to', 'app1.apparmor-profile') | ||
292 | 572 | os.makedirs(os.path.dirname(apparmor_path)) | ||
293 | 573 | with open(apparmor_path, 'w', encoding='utf-8') as fp: | ||
294 | 574 | fp.write(dedent("""\ | ||
295 | 575 | { | ||
296 | 576 | "template": "default", | ||
297 | 577 | "policy_groups": [ | ||
298 | 578 | "networking" | ||
299 | 579 | ], | ||
300 | 580 | "policy_vendor": "ubuntu-snappy", | ||
301 | 581 | "policy_version": 88.88 | ||
302 | 582 | } | ||
303 | 583 | """)) | ||
304 | 584 | self._make_metadata(scratch, APPARMOR_2_TEMPLATE) | ||
305 | 585 | self.builder.add_file(scratch, "./") | ||
306 | 586 | # FIXME: make it a .snap | ||
307 | 587 | expected_path = os.path.join(self.temp_dir, "test-yaml_1.0_all.click") | ||
308 | 588 | actual_path = self.builder.build(self.temp_dir, "meta/package.yaml") | ||
309 | 589 | self.assertEqual(expected_path, actual_path) | ||
310 | 590 | self.assertTrue(os.path.exists(actual_path)) | ||
311 | 591 | contents = os.path.join(self.temp_dir, 'contents') | ||
312 | 592 | subprocess.check_output( | ||
313 | 593 | ["dpkg-deb", "-R", actual_path, contents], universal_newlines=True) | ||
314 | 594 | self.assertEqual(sorted(os.listdir(contents)), | ||
315 | 595 | ['DEBIAN', 'bin', 'data', 'meta', 'path']) | ||
316 | 596 | self.assertEqual( | ||
317 | 597 | sorted(os.listdir(os.path.join(contents, 'path', 'to'))), | ||
318 | 598 | ['app1.apparmor-profile']) | ||
319 | 599 | with open( | ||
320 | 600 | os.path.join(contents, 'path', 'to', 'app1.apparmor-profile'), | ||
321 | 601 | encoding='utf-8') as fp: | ||
322 | 602 | data = fp.read() | ||
323 | 603 | self.assertEqual(data, dedent("""\ | ||
324 | 604 | { | ||
325 | 605 | "template": "default", | ||
326 | 606 | "policy_groups": [ | ||
327 | 607 | "networking" | ||
328 | 608 | ], | ||
329 | 609 | "policy_vendor": "ubuntu-snappy", | ||
330 | 610 | "policy_version": 88.88 | ||
331 | 611 | } | ||
332 | 612 | """)) | ||
333 | 613 | with open(os.path.join(contents, 'meta', 'app2.apparmor'), | ||
334 | 614 | encoding='utf-8') as fp: | ||
335 | 615 | data = fp.read() | ||
336 | 616 | self.assertEqual(data, dedent("""\ | ||
337 | 617 | { | ||
338 | 618 | "template": "default", | ||
339 | 619 | "policy_groups": [ | ||
340 | 620 | "networking" | ||
341 | 621 | ], | ||
342 | 622 | "policy_vendor": "ubuntu-snappy", | ||
343 | 623 | "policy_version": 1.3 | ||
344 | 624 | } | ||
345 | 625 | """)) |
Without testing the code, this looks reasonable. I can say that the hardcoding of the policy_version is something we won't want long term. We'll need to have click-apparmor expose that some how so it can be consumed by click.