2
# ------------------------------------------------------------------
4
# Copyright (C) 2013-2015 Canonical Ltd.
5
# Authors: Steve Beattie <steve@nxnw.org>
6
# Tyler Hicks <tyhicks@canonical.com>
8
# This program is free software; you can redistribute it and/or
9
# modify it under the terms of version 2 of the GNU General Public
10
# License published by the Free Software Foundation.
12
# ------------------------------------------------------------------
15
# - check cache not used if parser in $PATH is newer
16
# - check cache used for force-complain, disable symlink, etc.
18
from argparse import ArgumentParser
27
ABSTRACTION_CONTENTS = '''
28
# Simple example abstraction
31
ABSTRACTION = 'suid-abstraction'
33
PROFILE_CONTENTS = '''
34
# Simple example profile for caching tests
45
PROFILE = 'sbin.pingy'
49
class AAParserCachingCommon(testlib.AATestTemplate):
53
'''setup for each test'''
56
# REPORT ALL THE OUTPUT
59
# skip all the things if apparmor securityfs isn't mounted
60
if not os.path.exists("/sys/kernel/security/apparmor"):
61
raise unittest.SkipTest("WARNING: /sys/kernel/security/apparmor does not exist. "
64
self.tmp_dir = tempfile.mkdtemp(prefix='aa-caching-')
65
os.chmod(self.tmp_dir, 0o755)
67
# create directory for cached blobs
68
self.cache_dir = os.path.join(self.tmp_dir, 'cache')
69
os.mkdir(self.cache_dir)
71
# default path of the output cache file
72
self.cache_file = os.path.join(self.cache_dir, PROFILE)
74
# write our sample abstraction and profile out
75
self.abstraction = testlib.write_file(self.tmp_dir, ABSTRACTION, ABSTRACTION_CONTENTS)
76
self.profile = testlib.write_file(self.tmp_dir, PROFILE, PROFILE_CONTENTS)
79
self.do_cleanup = False
82
self.cmd_prefix = [config.parser, '--base', self.tmp_dir, '--skip-kernel-load']
85
'''teardown for each test'''
87
if not self.do_cleanup:
88
print("\n===> Skipping cleanup, leaving testfiles behind in '%s'" % (self.tmp_dir))
90
if os.path.exists(self.tmp_dir):
91
shutil.rmtree(self.tmp_dir)
93
def assert_path_exists(self, path, expected=True):
95
self.assertTrue(os.path.exists(path),
96
'test did not create file %s, when it was expected to do so' % path)
98
self.assertFalse(os.path.exists(path),
99
'test created file %s, when it was not expected to do so' % path)
101
def compare_features_file(self, features_path, expected=True):
102
# compare features contents
103
expected_output = testlib.read_features_dir('/sys/kernel/security/apparmor/features')
104
with open(features_path) as f:
107
self.assertEquals(expected_output, features,
108
"features contents differ, expected:\n%s\nresult:\n%s" % (expected_output, features))
110
self.assertNotEquals(expected_output, features,
111
"features contents equal, expected:\n%s\nresult:\n%s" % (expected_output, features))
114
class AAParserBasicCachingTests(AAParserCachingCommon):
117
super(AAParserBasicCachingTests, self).setUp()
119
def test_no_cache_by_default(self):
120
'''test profiles are not cached by default'''
122
cmd = list(self.cmd_prefix)
123
cmd.extend(['-q', '-r', self.profile])
124
self.run_cmd_check(cmd)
125
self.assert_path_exists(os.path.join(self.cache_dir, PROFILE), expected=False)
127
def test_no_cache_w_skip_cache(self):
128
'''test profiles are not cached with --skip-cache'''
130
cmd = list(self.cmd_prefix)
131
cmd.extend(['-q', '--write-cache', '--skip-cache', '-r', self.profile])
132
self.run_cmd_check(cmd)
133
self.assert_path_exists(os.path.join(self.cache_dir, PROFILE), expected=False)
135
def test_cache_when_requested(self):
136
'''test profiles are cached when requested'''
138
cmd = list(self.cmd_prefix)
139
cmd.extend(['-q', '--write-cache', '-r', self.profile])
140
self.run_cmd_check(cmd)
141
self.assert_path_exists(os.path.join(self.cache_dir, PROFILE))
143
def test_write_features_when_caching(self):
144
'''test features file is written when caching'''
146
cmd = list(self.cmd_prefix)
147
cmd.extend(['-q', '--write-cache', '-r', self.profile])
148
self.run_cmd_check(cmd)
149
self.assert_path_exists(os.path.join(self.cache_dir, PROFILE))
150
self.assert_path_exists(os.path.join(self.cache_dir, '.features'))
152
def test_features_match_when_caching(self):
153
'''test features file is written when caching'''
155
cmd = list(self.cmd_prefix)
156
cmd.extend(['-q', '--write-cache', '-r', self.profile])
157
self.run_cmd_check(cmd)
158
self.assert_path_exists(os.path.join(self.cache_dir, PROFILE))
159
self.assert_path_exists(os.path.join(self.cache_dir, '.features'))
161
self.compare_features_file(os.path.join(self.cache_dir, '.features'))
164
class AAParserAltCacheBasicTests(AAParserBasicCachingTests):
165
'''Same tests as above, but with an alternate cache location specified on the command line'''
168
super(AAParserAltCacheBasicTests, self).setUp()
170
alt_cache_dir = tempfile.mkdtemp(prefix='aa-alt-cache', dir=self.tmp_dir)
171
os.chmod(alt_cache_dir, 0o755)
173
self.unused_cache_dir = self.cache_dir
174
self.cache_dir = alt_cache_dir
175
self.cmd_prefix.extend(['--cache-loc', alt_cache_dir])
178
if len(os.listdir(self.unused_cache_dir)) > 0:
179
self.fail('original cache dir \'%s\' not empty' % self.unused_cache_dir)
180
super(AAParserAltCacheBasicTests, self).tearDown()
183
class AAParserCreateCacheBasicTestsCacheExists(AAParserBasicCachingTests):
184
'''Same tests as above, but with create cache option on the command line and the cache already exists'''
187
super(AAParserCreateCacheBasicTestsCacheExists, self).setUp()
188
self.cmd_prefix.append('--create-cache-dir')
191
class AAParserCreateCacheBasicTestsCacheNotExist(AAParserBasicCachingTests):
192
'''Same tests as above, but with create cache option on the command line and cache dir removed'''
195
super(AAParserCreateCacheBasicTestsCacheNotExist, self).setUp()
196
shutil.rmtree(self.cache_dir)
197
self.cmd_prefix.append('--create-cache-dir')
200
class AAParserCreateCacheAltCacheTestsCacheNotExist(AAParserBasicCachingTests):
201
'''Same tests as above, but with create cache option on the command line,
202
alt cache specified, and cache dir removed'''
205
super(AAParserCreateCacheAltCacheTestsCacheNotExist, self).setUp()
206
shutil.rmtree(self.cache_dir)
207
self.cmd_prefix.append('--create-cache-dir')
210
class AAParserCachingTests(AAParserCachingCommon):
213
super(AAParserCachingTests, self).setUp()
215
r = testlib.filesystem_time_resolution()
216
self.mtime_res = r[1]
218
def _generate_cache_file(self):
220
cmd = list(self.cmd_prefix)
221
cmd.extend(['-q', '--write-cache', '-r', self.profile])
222
self.run_cmd_check(cmd)
223
self.assert_path_exists(self.cache_file)
225
def _set_mtime(self, path, mtime):
226
atime = os.stat(path).st_atime
227
os.utime(path, (atime, mtime))
228
self.assertEquals(os.stat(path).st_mtime, mtime)
230
def test_cache_loaded_when_exists(self):
231
'''test cache is loaded when it exists, is newer than profile, and features match'''
233
self._generate_cache_file()
235
cmd = list(self.cmd_prefix)
236
cmd.extend(['-v', '-r', self.profile])
237
self.run_cmd_check(cmd, expected_string='Cached reload succeeded')
239
def test_cache_not_loaded_when_skip_arg(self):
240
'''test cache is not loaded when --skip-cache is passed'''
242
self._generate_cache_file()
244
cmd = list(self.cmd_prefix)
245
cmd.extend(['-v', '--skip-cache', '-r', self.profile])
246
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
248
def test_cache_not_loaded_when_skip_read_arg(self):
249
'''test cache is not loaded when --skip-read-cache is passed'''
251
self._generate_cache_file()
253
cmd = list(self.cmd_prefix)
254
cmd.extend(['-v', '--skip-read-cache', '-r', self.profile])
255
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
257
def test_cache_not_loaded_when_features_differ(self):
258
'''test cache is not loaded when features file differs'''
260
self._generate_cache_file()
262
testlib.write_file(self.cache_dir, '.features', 'monkey\n')
264
cmd = list(self.cmd_prefix)
265
cmd.extend(['-v', '-r', self.profile])
266
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
268
def test_cache_writing_does_not_overwrite_features_when_features_differ(self):
269
'''test cache writing does not overwrite the features files when it differs and --skip-bad-cache is given'''
271
features_file = testlib.write_file(self.cache_dir, '.features', 'monkey\n')
273
cmd = list(self.cmd_prefix)
274
cmd.extend(['-v', '--write-cache', '--skip-bad-cache', '-r', self.profile])
275
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
276
self.assert_path_exists(features_file)
277
# ensure that the features does *not* match the current features set
278
self.compare_features_file(features_file, expected=False)
280
def test_cache_writing_skipped_when_features_differ(self):
281
'''test cache writing is skipped when features file differs'''
283
testlib.write_file(self.cache_dir, '.features', 'monkey\n')
285
cmd = list(self.cmd_prefix)
286
cmd.extend(['-v', '--write-cache', '--skip-bad-cache', '-r', self.profile])
287
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
288
self.assert_path_exists(self.cache_file, expected=False)
290
def test_cache_writing_updates_features(self):
291
'''test cache writing updates features'''
293
features_file = testlib.write_file(self.cache_dir, '.features', 'monkey\n')
295
cmd = list(self.cmd_prefix)
296
cmd.extend(['-v', '--write-cache', '-r', self.profile])
297
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
298
self.assert_path_exists(features_file)
299
self.compare_features_file(features_file)
301
def test_cache_writing_updates_cache_file(self):
302
'''test cache writing updates cache file'''
304
cache_file = testlib.write_file(self.cache_dir, PROFILE, 'monkey\n')
305
orig_stat = os.stat(cache_file)
307
cmd = list(self.cmd_prefix)
308
cmd.extend(['-v', '--write-cache', '-r', self.profile])
309
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
310
self.assert_path_exists(cache_file)
311
stat = os.stat(cache_file)
312
# We check sizes here rather than whether the string monkey is
313
# in cache_contents because of the difficulty coercing cache
314
# file bytes into strings in python3
315
self.assertNotEquals(orig_stat.st_size, stat.st_size, 'Expected cache file to be updated, size is not changed.')
316
self.assertEquals(os.stat(self.profile).st_mtime, stat.st_mtime)
318
def test_cache_writing_clears_all_files(self):
319
'''test cache writing clears all cache files'''
321
check_file = testlib.write_file(self.cache_dir, 'monkey', 'monkey\n')
323
cmd = list(self.cmd_prefix)
324
cmd.extend(['-v', '--write-cache', '-r', self.profile])
325
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
326
self.assert_path_exists(check_file, expected=False)
328
def test_profile_mtime_preserved(self):
329
'''test profile mtime is preserved when it is newest'''
331
self._set_mtime(self.abstraction, 0)
332
self._set_mtime(self.profile, expected)
333
self._generate_cache_file()
334
self.assertEquals(expected, os.stat(self.cache_file).st_mtime)
336
def test_abstraction_mtime_preserved(self):
337
'''test abstraction mtime is preserved when it is newest'''
339
self._set_mtime(self.profile, 0)
340
self._set_mtime(self.abstraction, expected)
341
self._generate_cache_file()
342
self.assertEquals(expected, os.stat(self.cache_file).st_mtime)
344
def test_equal_mtimes_preserved(self):
345
'''test equal profile and abstraction mtimes are preserved'''
346
expected = 10000 + self.mtime_res
347
self._set_mtime(self.profile, expected)
348
self._set_mtime(self.abstraction, expected)
349
self._generate_cache_file()
350
self.assertEquals(expected, os.stat(self.cache_file).st_mtime)
352
def test_profile_newer_skips_cache(self):
353
'''test cache is skipped if profile is newer'''
355
self._generate_cache_file()
356
profile_mtime = os.stat(self.cache_file).st_mtime + self.mtime_res
357
self._set_mtime(self.profile, profile_mtime)
359
orig_stat = os.stat(self.cache_file)
361
cmd = list(self.cmd_prefix)
362
cmd.extend(['-v', '-r', self.profile])
363
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
365
stat = os.stat(self.cache_file)
366
self.assertEquals(orig_stat.st_size, stat.st_size)
367
self.assertEquals(orig_stat.st_ino, stat.st_ino)
368
self.assertEquals(orig_stat.st_mtime, stat.st_mtime)
370
def test_abstraction_newer_skips_cache(self):
371
'''test cache is skipped if abstraction is newer'''
373
self._generate_cache_file()
374
abstraction_mtime = os.stat(self.cache_file).st_mtime + self.mtime_res
375
self._set_mtime(self.abstraction, abstraction_mtime)
377
orig_stat = os.stat(self.cache_file)
379
cmd = list(self.cmd_prefix)
380
cmd.extend(['-v', '-r', self.profile])
381
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
383
stat = os.stat(self.cache_file)
384
self.assertEquals(orig_stat.st_size, stat.st_size)
385
self.assertEquals(orig_stat.st_ino, stat.st_ino)
386
self.assertEquals(orig_stat.st_mtime, stat.st_mtime)
388
def test_profile_newer_rewrites_cache(self):
389
'''test cache is rewritten if profile is newer'''
391
self._generate_cache_file()
392
profile_mtime = os.stat(self.cache_file).st_mtime + self.mtime_res
393
self._set_mtime(self.profile, profile_mtime)
395
orig_stat = os.stat(self.cache_file)
397
cmd = list(self.cmd_prefix)
398
cmd.extend(['-v', '-r', '-W', self.profile])
399
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
401
stat = os.stat(self.cache_file)
402
self.assertNotEquals(orig_stat.st_ino, stat.st_ino)
403
self.assertEquals(profile_mtime, stat.st_mtime)
405
def test_abstraction_newer_rewrites_cache(self):
406
'''test cache is rewritten if abstraction is newer'''
408
self._generate_cache_file()
409
abstraction_mtime = os.stat(self.cache_file).st_mtime + self.mtime_res
410
self._set_mtime(self.abstraction, abstraction_mtime)
412
orig_stat = os.stat(self.cache_file)
414
cmd = list(self.cmd_prefix)
415
cmd.extend(['-v', '-r', '-W', self.profile])
416
self.run_cmd_check(cmd, expected_string='Replacement succeeded for')
418
stat = os.stat(self.cache_file)
419
self.assertNotEquals(orig_stat.st_ino, stat.st_ino)
420
self.assertEquals(abstraction_mtime, stat.st_mtime)
422
def test_parser_newer_uses_cache(self):
423
'''test cache is not skipped if parser is newer'''
425
self._generate_cache_file()
428
os.mkdir(os.path.join(self.tmp_dir, 'parser'))
429
new_parser = os.path.join(self.tmp_dir, 'parser', 'apparmor_parser')
430
shutil.copy(config.parser, new_parser)
431
self._set_mtime(new_parser, os.stat(self.cache_file).st_mtime + self.mtime_res)
433
cmd = list(self.cmd_prefix)
435
cmd.extend(['-v', '-r', self.profile])
436
self.run_cmd_check(cmd, expected_string='Cached reload succeeded for')
438
def _purge_cache_test(self, location):
440
cache_file = testlib.write_file(self.cache_dir, location, 'monkey\n')
442
cmd = list(self.cmd_prefix)
443
cmd.extend(['-v', '--purge-cache', '-r', self.profile])
444
self.run_cmd_check(cmd)
445
# no message is output
446
self.assert_path_exists(cache_file, expected=False)
448
def test_cache_purge_removes_features_file(self):
449
'''test cache --purge-cache removes .features file'''
450
self._purge_cache_test('.features')
452
def test_cache_purge_removes_cache_file(self):
453
'''test cache --purge-cache removes profile cache file'''
454
self._purge_cache_test(PROFILE)
456
def test_cache_purge_removes_other_cache_files(self):
457
'''test cache --purge-cache removes other cache files'''
458
self._purge_cache_test('monkey')
461
class AAParserAltCacheTests(AAParserCachingTests):
462
'''Same tests as above, but with an alternate cache location specified on the command line'''
463
check_orig_cache = True
466
super(AAParserAltCacheTests, self).setUp()
468
alt_cache_dir = tempfile.mkdtemp(prefix='aa-alt-cache', dir=self.tmp_dir)
469
os.chmod(alt_cache_dir, 0o755)
471
self.orig_cache_dir = self.cache_dir
472
self.cache_dir = alt_cache_dir
473
self.cache_file = os.path.join(self.cache_dir, PROFILE)
474
self.cmd_prefix.extend(['--cache-loc', alt_cache_dir])
477
if self.check_orig_cache and len(os.listdir(self.orig_cache_dir)) > 0:
478
self.fail('original cache dir \'%s\' not empty' % self.orig_cache_dir)
479
super(AAParserAltCacheTests, self).tearDown()
481
def test_cache_purge_leaves_original_cache_alone(self):
482
'''test cache purging only touches alt cache'''
484
# skip tearDown check to ensure non-alt cache is empty
485
self.check_orig_cache = False
486
filelist = [PROFILE, '.features', 'monkey']
489
testlib.write_file(self.orig_cache_dir, f, 'monkey\n')
491
self._purge_cache_test(PROFILE)
494
if not os.path.exists(os.path.join(self.orig_cache_dir, f)):
495
self.fail('cache purge removed %s, was not supposed to' % (os.path.join(self.orig_cache_dir, f)))
501
p.add_argument('-p', '--parser', default=testlib.DEFAULT_PARSER, action="store", dest='parser')
502
p.add_argument('-v', '--verbose', action="store_true", dest="verbose")
503
p.add_argument('-d', '--debug', action="store_true", dest="debug")
504
config = p.parse_args()
510
test_suite = unittest.TestSuite()
511
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AAParserBasicCachingTests))
512
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AAParserAltCacheBasicTests))
513
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AAParserCreateCacheBasicTestsCacheExists))
514
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AAParserCreateCacheBasicTestsCacheNotExist))
515
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AAParserCreateCacheAltCacheTestsCacheNotExist))
516
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AAParserCachingTests))
517
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AAParserAltCacheTests))
520
result = unittest.TextTestRunner(verbosity=verbosity).run(test_suite)
521
if not result.wasSuccessful():
528
if __name__ == "__main__":