4 """Unittests for the posix1e module"""
6 # Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop <iustin@k1024.org>
8 # This library is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU Lesser General Public
10 # License as published by the Free Software Foundation; either
11 # version 2.1 of the License, or (at your option) any later version.
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 # Lesser General Public License for more details.
18 # You should have received a copy of the GNU Lesser General Public
19 # License along with this library; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
38 TEST_DIR = os.environ.get("TEST_DIR", ".")
40 BASIC_ACL_TEXT = "u::rw,g::r,o::-"
42 # This is to workaround python 2/3 differences at syntactic level
43 # (which can't be worked around via if's)
44 M0500 = 320 # octal 0500
45 M0644 = 420 # octal 0644
46 M0755 = 493 # octal 755
48 # Permset permission information
50 posix1e.ACL_READ: ("read", posix1e.Permset.read),
51 posix1e.ACL_WRITE: ("write", posix1e.Permset.write),
52 posix1e.ACL_EXECUTE: ("execute", posix1e.Permset.execute),
56 # Check if running under Python 3
57 IS_PY_3K = sys.hexversion >= 0x03000000
59 # Fixtures and helpers
61 def ignore_ioerror(errnum, fn, *args, **kwargs):
62 """Call a function while ignoring some IOErrors.
64 This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL)
65 when doing certain operations on an invalid ACL.
71 err = sys.exc_info()[1]
72 if err.errno == errnum:
77 """Encode a string if needed (under Python 3)"""
85 """per-test temp dir based in TEST_DIR"""
86 with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname:
90 fh, fname = tempfile.mkstemp(".test", "xattr-", path)
93 @contextlib.contextmanager
94 def get_file_name(path):
95 fh, fname = get_file(path)
99 @contextlib.contextmanager
100 def get_file_fd(path):
101 fd = get_file(path)[0]
105 @contextlib.contextmanager
106 def get_file_object(path):
107 fd = get_file(path)[0]
108 with os.fdopen(fd) as f:
111 @contextlib.contextmanager
113 yield tempfile.mkdtemp(".test", "xattr-", path)
115 def get_symlink(path, dangling=True):
116 """create a symlink"""
117 fh, fname = get_file(path)
121 sname = fname + ".symlink"
122 os.symlink(fname, sname)
125 @contextlib.contextmanager
126 def get_valid_symlink(path):
127 yield get_symlink(path, dangling=False)[1]
129 @contextlib.contextmanager
130 def get_dangling_symlink(path):
131 yield get_symlink(path, dangling=True)[1]
133 @contextlib.contextmanager
134 def get_file_and_symlink(path):
135 yield get_symlink(path, dangling=False)
137 @contextlib.contextmanager
138 def get_file_and_fobject(path):
139 fh, fname = get_file(path)
140 with os.fdopen(fh) as fo:
143 # Wrappers that build upon existing values
145 def as_wrapper(call, fn, closer=None):
146 @contextlib.contextmanager
148 with call(path) as r:
151 if closer is not None:
156 return as_wrapper(call, lambda r: r.encode())
159 return as_wrapper(call, pathlib.PurePath)
161 def as_iostream(call):
162 opener = lambda f: io.open(f, "r")
163 closer = lambda r: r.close()
164 return as_wrapper(call, opener, closer)
166 NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)",
168 NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'",
171 require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE")
172 require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK")
173 require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY")
174 require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK")
175 require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE")
178 """Support functions ACLs"""
181 """set up function"""
186 """tear down function"""
187 for fname in self.rmfiles:
189 for dname in self.rmdirs:
193 """create a temp file"""
194 fh, fname = tempfile.mkstemp(".test", "xattr-", TEST_DIR)
195 self.rmfiles.append(fname)
199 """create a temp dir"""
200 dname = tempfile.mkdtemp(".test", "xattr-", TEST_DIR)
201 self.rmdirs.append(dname)
204 def _getsymlink(self):
205 """create a symlink"""
206 fh, fname = self._getfile()
209 os.symlink(fname + ".non-existent", fname)
214 """Load/create tests"""
215 def test_from_file(self, testdir):
216 """Test loading ACLs from a file"""
217 _, fname = get_file(testdir)
218 acl1 = posix1e.ACL(file=fname)
221 def test_from_dir(self, testdir):
222 """Test loading ACLs from a directory"""
223 with get_dir(testdir) as dname:
224 acl1 = posix1e.ACL(file=dname)
225 acl2 = posix1e.ACL(filedef=dname)
227 # default ACLs might or might not be valid; missing ones are
228 # not valid, so we don't test acl2 for validity
230 def test_from_fd(self, testdir):
231 """Test loading ACLs from a file descriptor"""
232 fd, _ = get_file(testdir)
233 acl1 = posix1e.ACL(fd=fd)
236 def test_from_empty_invalid(self):
237 """Test creating an empty ACL"""
239 assert not acl1.valid()
241 def test_from_text(self):
242 """Test creating an ACL from text"""
243 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
246 def test_from_acl(self):
247 """Test creating an ACL from an existing ACL"""
249 acl2 = posix1e.ACL(acl=acl1)
252 def test_invalid_creation_params(self, testdir):
253 """Test that creating an ACL from multiple objects fails"""
254 fd, _ = get_file(testdir)
255 with pytest.raises(ValueError):
256 posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd)
258 def test_invalid_value_creation(self):
259 """Test that creating an ACL from wrong specification fails"""
260 with pytest.raises(EnvironmentError):
261 posix1e.ACL(text="foobar")
262 with pytest.raises(TypeError):
263 posix1e.ACL(foo="bar")
265 def test_double_init(self):
266 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
268 acl1.__init__(text=BASIC_ACL_TEXT)
271 class TestAclExtensions:
272 """ACL extensions checks"""
274 @require_acl_from_mode
275 def test_from_mode(self):
276 """Test loading ACLs from an octal mode"""
277 acl1 = posix1e.ACL(mode=M0644)
281 def test_acl_check(self):
282 """Test the acl_check method"""
283 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
284 assert not acl1.check()
288 @require_extended_check
289 def test_extended(self, testdir):
290 """Test the acl_extended function"""
291 fd, fname = get_file(testdir)
292 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
293 basic_acl.applyto(fd)
294 for item in fd, fname:
295 assert not has_extended(item)
296 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
297 assert enhanced_acl.valid()
298 enhanced_acl.applyto(fd)
299 for item in fd, fname:
300 assert has_extended(item)
302 @require_extended_check
303 def test_extended_arg_handling(self):
304 with pytest.raises(TypeError):
306 with pytest.raises(TypeError):
307 has_extended(object())
310 def test_equiv_mode(self):
311 """Test the equiv_mode function"""
312 if HAS_ACL_FROM_MODE:
313 for mode in M0644, M0755:
314 acl = posix1e.ACL(mode=mode)
315 assert acl.equiv_mode() == mode
316 acl = posix1e.ACL(text="u::rw,g::r,o::r")
317 assert acl.equiv_mode() == 0o644
318 acl = posix1e.ACL(text="u::rx,g::-,o::-")
319 assert acl.equiv_mode() == 0o500
322 def test_to_any_text(self):
323 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
324 assert encode("u::") in \
325 acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
326 assert encode("user::") in acl.to_any_text()
329 def test_to_any_text_wrong_args(self):
330 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
331 with pytest.raises(TypeError):
332 acl.to_any_text(foo="bar")
336 def test_rich_compare(self):
337 acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
338 acl2 = posix1e.ACL(acl=acl1)
339 acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
342 with pytest.raises(TypeError):
344 with pytest.raises(TypeError):
347 assert not (acl1 == 1)
348 with pytest.raises(TypeError):
351 @pytest.mark.skipif(not hasattr(posix1e.ACL, "__cmp__"), reason="__cmp__ is missing")
355 with pytest.raises(TypeError):
358 def test_apply_to_with_wrong_object(self):
359 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
361 with pytest.raises(TypeError):
362 acl1.applyto(object())
363 with pytest.raises(TypeError):
364 acl1.applyto(object(), object())
367 def test_acl_iterator(self):
368 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
370 assert entry.parent is acl
376 def test_delete_default(self, testdir):
377 """Test removing the default ACL"""
378 with get_dir(testdir) as dname:
379 posix1e.delete_default(dname)
382 def test_delete_default_wrong_arg(self):
383 with pytest.raises(TypeError):
384 posix1e.delete_default(object())
386 def test_reapply(self, testdir):
387 """Test re-applying an ACL"""
388 fd, fname = get_file(testdir)
389 acl1 = posix1e.ACL(fd=fd)
392 with get_dir(testdir) as dname:
393 acl2 = posix1e.ACL(file=fname)
397 @unittest.skipUnless(HAS_ACL_ENTRY, "ACL entries not supported")
398 class ModificationTests(aclTest, unittest.TestCase):
399 """ACL modification tests"""
401 def checkRef(self, obj):
402 """Checks if a given obj has a 'sane' refcount"""
403 if platform.python_implementation() == "PyPy":
405 ref_cnt = sys.getrefcount(obj)
406 # FIXME: hardcoded value for the max ref count... but I've
407 # seen it overflow on bad reference counting, so it's better
409 if ref_cnt < 2 or ref_cnt > 1024:
410 self.fail("Wrong reference count, expected 2-1024 and got %d" %
414 """Test str() of an ACL."""
415 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
417 self.checkRef(str_acl)
419 def testAppend(self):
420 """Test append a new Entry to the ACL"""
423 e.tag_type = posix1e.ACL_OTHER
424 ignore_ioerror(errno.EINVAL, acl.calc_mask)
426 self.checkRef(str_format)
428 ignore_ioerror(errno.EINVAL, acl.calc_mask)
429 self.assertFalse(acl.valid())
431 def testWrongAppend(self):
432 """Test append a new Entry to the ACL based on wrong object type"""
434 self.assertRaises(TypeError, acl.append, object())
436 def testEntryCreation(self):
438 e = posix1e.Entry(acl)
439 ignore_ioerror(errno.EINVAL, acl.calc_mask)
441 self.checkRef(str_format)
443 def testEntryFailedCreation(self):
444 # Checks for partial initialisation and deletion on error
446 self.assertRaises(TypeError, posix1e.Entry, object())
448 def testDelete(self):
449 """Test delete Entry from the ACL"""
452 e.tag_type = posix1e.ACL_OTHER
453 ignore_ioerror(errno.EINVAL, acl.calc_mask)
455 ignore_ioerror(errno.EINVAL, acl.calc_mask)
457 def testDoubleDelete(self):
458 """Test delete Entry from the ACL"""
459 # This is not entirely valid/correct, since the entry object
460 # itself is invalid after the first deletion, so we're
461 # actually testing deleting an invalid object, not a
462 # non-existing entry...
465 e.tag_type = posix1e.ACL_OTHER
466 ignore_ioerror(errno.EINVAL, acl.calc_mask)
468 ignore_ioerror(errno.EINVAL, acl.calc_mask)
469 self.assertRaises(EnvironmentError, acl.delete_entry, e)
471 # This currently fails as this deletion seems to be accepted :/
472 @unittest.skip("Entry deletion is unreliable")
473 def testDeleteInvalidEntry(self):
474 """Test delete foreign Entry from the ACL"""
478 e.tag_type = posix1e.ACL_OTHER
479 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
480 self.assertRaises(EnvironmentError, acl2.delete_entry, e)
482 def testDeleteInvalidObject(self):
483 """Test delete a non-Entry from the ACL"""
485 self.assertRaises(TypeError, acl.delete_entry, object())
487 def testDoubleEntries(self):
488 """Test double entries"""
489 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
490 self.assertTrue(acl.valid(), "ACL is not valid")
491 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
494 e.tag_type = tag_type
496 self.assertFalse(acl.valid(),
497 "ACL containing duplicate entries"
498 " should not be valid")
501 def testMultipleGoodEntries(self):
502 """Test multiple valid entries"""
503 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
504 self.assertTrue(acl.valid(), "ACL is not valid")
505 for tag_type in (posix1e.ACL_USER,
507 for obj_id in range(5):
509 e.tag_type = tag_type
513 self.assertTrue(acl.valid(),
514 "ACL should be able to hold multiple"
515 " user/group entries")
517 def testMultipleBadEntries(self):
518 """Test multiple invalid entries"""
519 for tag_type in (posix1e.ACL_USER,
521 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
522 self.assertTrue(acl.valid(), "ACL built from standard description"
525 e1.tag_type = tag_type
529 self.assertTrue(acl.valid(), "ACL should be able to add a"
532 e2.tag_type = tag_type
535 ignore_ioerror(errno.EINVAL, acl.calc_mask)
536 self.assertFalse(acl.valid(), "ACL should not validate when"
537 " containing two duplicate entries")
539 # FreeBSD trips over itself here and can't delete the
540 # entry, even though it still exists.
541 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
546 e1.tag_type = ACL_USER
552 e2.tag_type = ACL_GROUP
556 self.assertFalse(p2.write)
558 self.assertTrue(p2.write)
559 self.assertEqual(e1.tag_type, e2.tag_type)
561 def testCopyWrongArg(self):
564 self.assertRaises(TypeError, e.copy, object())
566 def testSetPermset(self):
569 e1.tag_type = ACL_USER
575 e2.tag_type = ACL_GROUP
579 self.assertFalse(p2.write)
581 self.assertTrue(e2.permset.write)
582 self.assertEqual(e2.tag_type, ACL_GROUP)
584 def testSetPermsetWrongArg(self):
589 self.assertRaises(TypeError, setter, object())
591 def testPermsetCreation(self):
596 #self.assertEqual(p1, p2)
598 def testPermsetCreationWrongArg(self):
599 self.assertRaises(TypeError, Permset, object())
601 def testPermset(self):
602 """Test permissions"""
608 self.checkRef(str_ps)
609 for perm in PERMSETS:
611 txt = PERMSETS[perm][0]
612 self.checkRef(str_ps)
613 self.assertFalse(ps.test(perm), "Empty permission set should not"
614 " have permission '%s'" % txt)
616 self.assertTrue(ps.test(perm), "Permission '%s' should exist"
617 " after addition" % txt)
619 self.checkRef(str_ps)
621 self.assertFalse(ps.test(perm), "Permission '%s' should not exist"
622 " after deletion" % txt)
624 def testPermsetViaAccessors(self):
625 """Test permissions"""
631 self.checkRef(str_ps)
633 return PERMSETS[perm][1].__get__(ps)
634 def setter(parm, value):
635 return PERMSETS[perm][1].__set__(ps, value)
636 for perm in PERMSETS:
638 self.checkRef(str_ps)
639 txt = PERMSETS[perm][0]
640 self.assertFalse(getter(perm), "Empty permission set should not"
641 " have permission '%s'" % txt)
643 self.assertTrue(ps.test(perm), "Permission '%s' should exist"
644 " after addition" % txt)
645 self.assertTrue(getter(perm), "Permission '%s' should exist"
646 " after addition" % txt)
648 self.checkRef(str_ps)
650 self.assertFalse(ps.test(perm), "Permission '%s' should not exist"
651 " after deletion" % txt)
652 self.assertFalse(getter(perm), "Permission '%s' should not exist"
653 " after deletion" % txt)
655 def testPermsetInvalidType(self):
662 self.assertRaises(TypeError, ps.add, "foobar")
663 self.assertRaises(TypeError, ps.delete, "foobar")
664 self.assertRaises(TypeError, ps.test, "foobar")
665 self.assertRaises(ValueError, setter)
667 @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3")
668 def testQualifierValues(self):
669 """Tests qualifier correct store/retrieval"""
672 # work around deprecation warnings
673 if hasattr(self, 'assertRegex'):
674 fn = self.assertRegex
676 fn = self.assertRegexpMatches
677 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
681 if tag == posix1e.ACL_USER:
682 regex = re.compile("user with uid %d" % qualifier)
684 regex = re.compile("group with gid %d" % qualifier)
686 e.qualifier = qualifier
687 except OverflowError:
688 # reached overflow condition, break
690 self.assertEqual(e.qualifier, qualifier)
694 @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3")
695 def testQualifierOverflow(self):
696 """Tests qualifier overflow handling"""
699 qualifier = sys.maxsize * 2
700 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
702 with self.assertRaises(OverflowError):
703 e.qualifier = qualifier
705 @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3")
706 def testNegativeQualifier(self):
707 """Tests negative qualifier handling"""
708 # Note: this presumes that uid_t/gid_t in C are unsigned...
711 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
713 for qualifier in [-10, -5, -1]:
714 with self.assertRaises(OverflowError):
715 e.qualifier = qualifier
717 def testInvalidQualifier(self):
718 """Tests invalid qualifier handling"""
725 self.assertRaises(TypeError, set_qual, object())
726 self.assertRaises((TypeError, AttributeError), del_qual)
728 def testQualifierOnWrongTag(self):
729 """Tests qualifier setting on wrong tag"""
732 e.tag_type = posix1e.ACL_OTHER
737 self.assertRaises(TypeError, set_qual, 1)
738 self.assertRaises(TypeError, get_qual)
741 def testTagTypes(self):
742 """Tests tag type correct set/get"""
745 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP, posix1e.ACL_USER_OBJ,
746 posix1e.ACL_GROUP_OBJ, posix1e.ACL_MASK,
749 self.assertEqual(e.tag_type, tag)
750 # check we can show all tag types without breaking
751 self.assertTrue(str(e))
753 def testInvalidTags(self):
754 """Tests tag type incorrect set/get"""
759 self.assertRaises(TypeError, set_tag, object())
762 # For some reason, PyPy raises AttributeError. Strange...
763 self.assertRaises((TypeError, AttributeError), delete_tag)
765 e.tag_type = posix1e.ACL_USER_OBJ
766 tag = max([posix1e.ACL_USER, posix1e.ACL_GROUP, posix1e.ACL_USER_OBJ,
767 posix1e.ACL_GROUP_OBJ, posix1e.ACL_MASK,
768 posix1e.ACL_OTHER]) + 1
769 self.assertRaises(EnvironmentError, set_tag, tag)
770 # Check tag is still valid.
771 self.assertEqual(e.tag_type, posix1e.ACL_USER_OBJ)
773 if __name__ == "__main__":