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
43 TEST_DIR = os.environ.get("TEST_DIR", ".")
45 BASIC_ACL_TEXT = "u::rw,g::r,o::-"
47 # This is to workaround python 2/3 differences at syntactic level
48 # (which can't be worked around via if's)
49 M0500 = 320 # octal 0500
50 M0644 = 420 # octal 0644
51 M0755 = 493 # octal 755
53 # Permset permission information
55 posix1e.ACL_READ: ("read", posix1e.Permset.read),
56 posix1e.ACL_WRITE: ("write", posix1e.Permset.write),
57 posix1e.ACL_EXECUTE: ("execute", posix1e.Permset.execute),
61 # Check if running under Python 3
62 IS_PY_3K = sys.hexversion >= 0x03000000
64 # Fixtures and helpers
66 def ignore_ioerror(errnum, fn, *args, **kwargs):
67 """Call a function while ignoring some IOErrors.
69 This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL)
70 when doing certain operations on an invalid ACL.
76 err = sys.exc_info()[1]
77 if err.errno == errnum:
82 """Encode a string if needed (under Python 3)"""
90 """per-test temp dir based in TEST_DIR"""
91 with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname:
95 fh, fname = tempfile.mkstemp(".test", "xattr-", path)
98 @contextlib.contextmanager
99 def get_file_name(path):
100 fh, fname = get_file(path)
104 @contextlib.contextmanager
105 def get_file_fd(path):
106 fd = get_file(path)[0]
110 @contextlib.contextmanager
111 def get_file_object(path):
112 fd = get_file(path)[0]
113 with os.fdopen(fd) as f:
116 @contextlib.contextmanager
118 yield tempfile.mkdtemp(".test", "xattr-", path)
120 def get_symlink(path, dangling=True):
121 """create a symlink"""
122 fh, fname = get_file(path)
126 sname = fname + ".symlink"
127 os.symlink(fname, sname)
130 @contextlib.contextmanager
131 def get_valid_symlink(path):
132 yield get_symlink(path, dangling=False)[1]
134 @contextlib.contextmanager
135 def get_dangling_symlink(path):
136 yield get_symlink(path, dangling=True)[1]
138 @contextlib.contextmanager
139 def get_file_and_symlink(path):
140 yield get_symlink(path, dangling=False)
142 @contextlib.contextmanager
143 def get_file_and_fobject(path):
144 fh, fname = get_file(path)
145 with os.fdopen(fh) as fo:
148 # Wrappers that build upon existing values
150 def as_wrapper(call, fn, closer=None):
151 @contextlib.contextmanager
153 with call(path) as r:
156 if closer is not None:
161 return as_wrapper(call, lambda r: r.encode())
164 return as_wrapper(call, pathlib.PurePath)
166 def as_iostream(call):
167 opener = lambda f: io.open(f, "r")
168 closer = lambda r: r.close()
169 return as_wrapper(call, opener, closer)
171 NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)",
173 NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'",
176 require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE")
177 require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK")
178 require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY")
179 require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK")
180 require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE")
183 """Support functions ACLs"""
186 """set up function"""
191 """tear down function"""
192 for fname in self.rmfiles:
194 for dname in self.rmdirs:
198 """create a temp file"""
199 fh, fname = tempfile.mkstemp(".test", "xattr-", TEST_DIR)
200 self.rmfiles.append(fname)
204 """create a temp dir"""
205 dname = tempfile.mkdtemp(".test", "xattr-", TEST_DIR)
206 self.rmdirs.append(dname)
209 def _getsymlink(self):
210 """create a symlink"""
211 fh, fname = self._getfile()
214 os.symlink(fname + ".non-existent", fname)
219 """Load/create tests"""
220 def test_from_file(self, testdir):
221 """Test loading ACLs from a file"""
222 _, fname = get_file(testdir)
223 acl1 = posix1e.ACL(file=fname)
226 def test_from_dir(self, testdir):
227 """Test loading ACLs from a directory"""
228 with get_dir(testdir) as dname:
229 acl1 = posix1e.ACL(file=dname)
230 acl2 = posix1e.ACL(filedef=dname)
232 # default ACLs might or might not be valid; missing ones are
233 # not valid, so we don't test acl2 for validity
235 def test_from_fd(self, testdir):
236 """Test loading ACLs from a file descriptor"""
237 fd, _ = get_file(testdir)
238 acl1 = posix1e.ACL(fd=fd)
241 def test_from_empty_invalid(self):
242 """Test creating an empty ACL"""
244 assert not acl1.valid()
246 def test_from_text(self):
247 """Test creating an ACL from text"""
248 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
251 def test_from_acl(self):
252 """Test creating an ACL from an existing ACL"""
254 acl2 = posix1e.ACL(acl=acl1)
257 def test_invalid_creation_params(self, testdir):
258 """Test that creating an ACL from multiple objects fails"""
259 fd, _ = get_file(testdir)
260 with pytest.raises(ValueError):
261 posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd)
263 def test_invalid_value_creation(self):
264 """Test that creating an ACL from wrong specification fails"""
265 with pytest.raises(EnvironmentError):
266 posix1e.ACL(text="foobar")
267 with pytest.raises(TypeError):
268 posix1e.ACL(foo="bar")
270 def test_double_init(self):
271 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
273 acl1.__init__(text=BASIC_ACL_TEXT)
276 class TestAclExtensions:
277 """ACL extensions checks"""
279 @require_acl_from_mode
280 def test_from_mode(self):
281 """Test loading ACLs from an octal mode"""
282 acl1 = posix1e.ACL(mode=M0644)
286 def test_acl_check(self):
287 """Test the acl_check method"""
288 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
289 assert not acl1.check()
293 @require_extended_check
294 def test_extended(self, testdir):
295 """Test the acl_extended function"""
296 fd, fname = get_file(testdir)
297 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
298 basic_acl.applyto(fd)
299 for item in fd, fname:
300 assert not has_extended(item)
301 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
302 assert enhanced_acl.valid()
303 enhanced_acl.applyto(fd)
304 for item in fd, fname:
305 assert has_extended(item)
307 @require_extended_check
308 def test_extended_arg_handling(self):
309 with pytest.raises(TypeError):
311 with pytest.raises(TypeError):
312 has_extended(object())
315 def test_equiv_mode(self):
316 """Test the equiv_mode function"""
317 if HAS_ACL_FROM_MODE:
318 for mode in M0644, M0755:
319 acl = posix1e.ACL(mode=mode)
320 assert acl.equiv_mode() == mode
321 acl = posix1e.ACL(text="u::rw,g::r,o::r")
322 assert acl.equiv_mode() == 0o644
323 acl = posix1e.ACL(text="u::rx,g::-,o::-")
324 assert acl.equiv_mode() == 0o500
327 def test_to_any_text(self):
328 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
329 assert encode("u::") in \
330 acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
331 assert encode("user::") in acl.to_any_text()
334 def test_to_any_text_wrong_args(self):
335 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
336 with pytest.raises(TypeError):
337 acl.to_any_text(foo="bar")
341 def test_rich_compare(self):
342 acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
343 acl2 = posix1e.ACL(acl=acl1)
344 acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
347 with pytest.raises(TypeError):
349 with pytest.raises(TypeError):
352 assert not (acl1 == 1)
353 with pytest.raises(TypeError):
356 @pytest.mark.skipif(not hasattr(posix1e.ACL, "__cmp__"), reason="__cmp__ is missing")
357 @pytest.mark.skipif(__pypy__ is not None, reason="Disabled under pypy")
360 with pytest.raises(TypeError):
363 def test_apply_to_with_wrong_object(self):
364 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
366 with pytest.raises(TypeError):
367 acl1.applyto(object())
368 with pytest.raises(TypeError):
369 acl1.applyto(object(), object())
372 def test_acl_iterator(self):
373 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
375 assert entry.parent is acl
381 def test_delete_default(self, testdir):
382 """Test removing the default ACL"""
383 with get_dir(testdir) as dname:
384 posix1e.delete_default(dname)
386 @pytest.mark.skipif(__pypy__, reason="Disabled under pypy")
387 def test_delete_default_wrong_arg(self):
388 with pytest.raises(TypeError):
389 posix1e.delete_default(object())
391 def test_reapply(self, testdir):
392 """Test re-applying an ACL"""
393 fd, fname = get_file(testdir)
394 acl1 = posix1e.ACL(fd=fd)
397 with get_dir(testdir) as dname:
398 acl2 = posix1e.ACL(file=fname)
402 @unittest.skipUnless(HAS_ACL_ENTRY, "ACL entries not supported")
403 class ModificationTests(aclTest, unittest.TestCase):
404 """ACL modification tests"""
406 def checkRef(self, obj):
407 """Checks if a given obj has a 'sane' refcount"""
408 if platform.python_implementation() == "PyPy":
410 ref_cnt = sys.getrefcount(obj)
411 # FIXME: hardcoded value for the max ref count... but I've
412 # seen it overflow on bad reference counting, so it's better
414 if ref_cnt < 2 or ref_cnt > 1024:
415 self.fail("Wrong reference count, expected 2-1024 and got %d" %
419 """Test str() of an ACL."""
420 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
422 self.checkRef(str_acl)
424 def testAppend(self):
425 """Test append a new Entry to the ACL"""
428 e.tag_type = posix1e.ACL_OTHER
429 ignore_ioerror(errno.EINVAL, acl.calc_mask)
431 self.checkRef(str_format)
433 ignore_ioerror(errno.EINVAL, acl.calc_mask)
434 self.assertFalse(acl.valid())
436 def testWrongAppend(self):
437 """Test append a new Entry to the ACL based on wrong object type"""
439 self.assertRaises(TypeError, acl.append, object())
441 def testEntryCreation(self):
443 e = posix1e.Entry(acl)
444 ignore_ioerror(errno.EINVAL, acl.calc_mask)
446 self.checkRef(str_format)
448 def testEntryFailedCreation(self):
449 # Checks for partial initialisation and deletion on error
451 self.assertRaises(TypeError, posix1e.Entry, object())
453 def testDelete(self):
454 """Test delete Entry from the ACL"""
457 e.tag_type = posix1e.ACL_OTHER
458 ignore_ioerror(errno.EINVAL, acl.calc_mask)
460 ignore_ioerror(errno.EINVAL, acl.calc_mask)
462 def testDoubleDelete(self):
463 """Test delete Entry from the ACL"""
464 # This is not entirely valid/correct, since the entry object
465 # itself is invalid after the first deletion, so we're
466 # actually testing deleting an invalid object, not a
467 # non-existing entry...
470 e.tag_type = posix1e.ACL_OTHER
471 ignore_ioerror(errno.EINVAL, acl.calc_mask)
473 ignore_ioerror(errno.EINVAL, acl.calc_mask)
474 self.assertRaises(EnvironmentError, acl.delete_entry, e)
476 # This currently fails as this deletion seems to be accepted :/
477 @unittest.skip("Entry deletion is unreliable")
478 def testDeleteInvalidEntry(self):
479 """Test delete foreign Entry from the ACL"""
483 e.tag_type = posix1e.ACL_OTHER
484 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
485 self.assertRaises(EnvironmentError, acl2.delete_entry, e)
487 def testDeleteInvalidObject(self):
488 """Test delete a non-Entry from the ACL"""
490 self.assertRaises(TypeError, acl.delete_entry, object())
492 def testDoubleEntries(self):
493 """Test double entries"""
494 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
495 self.assertTrue(acl.valid(), "ACL is not valid")
496 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
499 e.tag_type = tag_type
501 self.assertFalse(acl.valid(),
502 "ACL containing duplicate entries"
503 " should not be valid")
506 def testMultipleGoodEntries(self):
507 """Test multiple valid entries"""
508 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
509 self.assertTrue(acl.valid(), "ACL is not valid")
510 for tag_type in (posix1e.ACL_USER,
512 for obj_id in range(5):
514 e.tag_type = tag_type
518 self.assertTrue(acl.valid(),
519 "ACL should be able to hold multiple"
520 " user/group entries")
522 def testMultipleBadEntries(self):
523 """Test multiple invalid entries"""
524 for tag_type in (posix1e.ACL_USER,
526 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
527 self.assertTrue(acl.valid(), "ACL built from standard description"
530 e1.tag_type = tag_type
534 self.assertTrue(acl.valid(), "ACL should be able to add a"
537 e2.tag_type = tag_type
540 ignore_ioerror(errno.EINVAL, acl.calc_mask)
541 self.assertFalse(acl.valid(), "ACL should not validate when"
542 " containing two duplicate entries")
544 # FreeBSD trips over itself here and can't delete the
545 # entry, even though it still exists.
546 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
551 e1.tag_type = ACL_USER
557 e2.tag_type = ACL_GROUP
561 self.assertFalse(p2.write)
563 self.assertTrue(p2.write)
564 self.assertEqual(e1.tag_type, e2.tag_type)
566 def testCopyWrongArg(self):
569 self.assertRaises(TypeError, e.copy, object())
571 def testSetPermset(self):
574 e1.tag_type = ACL_USER
580 e2.tag_type = ACL_GROUP
584 self.assertFalse(p2.write)
586 self.assertTrue(e2.permset.write)
587 self.assertEqual(e2.tag_type, ACL_GROUP)
589 def testSetPermsetWrongArg(self):
594 self.assertRaises(TypeError, setter, object())
596 def testPermsetCreation(self):
601 #self.assertEqual(p1, p2)
603 def testPermsetCreationWrongArg(self):
604 self.assertRaises(TypeError, Permset, object())
606 def testPermset(self):
607 """Test permissions"""
613 self.checkRef(str_ps)
614 for perm in PERMSETS:
616 txt = PERMSETS[perm][0]
617 self.checkRef(str_ps)
618 self.assertFalse(ps.test(perm), "Empty permission set should not"
619 " have permission '%s'" % txt)
621 self.assertTrue(ps.test(perm), "Permission '%s' should exist"
622 " after addition" % txt)
624 self.checkRef(str_ps)
626 self.assertFalse(ps.test(perm), "Permission '%s' should not exist"
627 " after deletion" % txt)
629 def testPermsetViaAccessors(self):
630 """Test permissions"""
636 self.checkRef(str_ps)
638 return PERMSETS[perm][1].__get__(ps)
639 def setter(parm, value):
640 return PERMSETS[perm][1].__set__(ps, value)
641 for perm in PERMSETS:
643 self.checkRef(str_ps)
644 txt = PERMSETS[perm][0]
645 self.assertFalse(getter(perm), "Empty permission set should not"
646 " have permission '%s'" % txt)
648 self.assertTrue(ps.test(perm), "Permission '%s' should exist"
649 " after addition" % txt)
650 self.assertTrue(getter(perm), "Permission '%s' should exist"
651 " after addition" % txt)
653 self.checkRef(str_ps)
655 self.assertFalse(ps.test(perm), "Permission '%s' should not exist"
656 " after deletion" % txt)
657 self.assertFalse(getter(perm), "Permission '%s' should not exist"
658 " after deletion" % txt)
660 def testPermsetInvalidType(self):
667 self.assertRaises(TypeError, ps.add, "foobar")
668 self.assertRaises(TypeError, ps.delete, "foobar")
669 self.assertRaises(TypeError, ps.test, "foobar")
670 self.assertRaises(ValueError, setter)
672 @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3")
673 def testQualifierValues(self):
674 """Tests qualifier correct store/retrieval"""
677 # work around deprecation warnings
678 if hasattr(self, 'assertRegex'):
679 fn = self.assertRegex
681 fn = self.assertRegexpMatches
682 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
686 if tag == posix1e.ACL_USER:
687 regex = re.compile("user with uid %d" % qualifier)
689 regex = re.compile("group with gid %d" % qualifier)
691 e.qualifier = qualifier
692 except OverflowError:
693 # reached overflow condition, break
695 self.assertEqual(e.qualifier, qualifier)
699 @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3")
700 def testQualifierOverflow(self):
701 """Tests qualifier overflow handling"""
704 qualifier = sys.maxsize * 2
705 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
707 with self.assertRaises(OverflowError):
708 e.qualifier = qualifier
710 @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3")
711 def testNegativeQualifier(self):
712 """Tests negative qualifier handling"""
713 # Note: this presumes that uid_t/gid_t in C are unsigned...
716 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
718 for qualifier in [-10, -5, -1]:
719 with self.assertRaises(OverflowError):
720 e.qualifier = qualifier
722 def testInvalidQualifier(self):
723 """Tests invalid qualifier handling"""
730 self.assertRaises(TypeError, set_qual, object())
731 self.assertRaises((TypeError, AttributeError), del_qual)
733 def testQualifierOnWrongTag(self):
734 """Tests qualifier setting on wrong tag"""
737 e.tag_type = posix1e.ACL_OTHER
742 self.assertRaises(TypeError, set_qual, 1)
743 self.assertRaises(TypeError, get_qual)
746 def testTagTypes(self):
747 """Tests tag type correct set/get"""
750 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP, posix1e.ACL_USER_OBJ,
751 posix1e.ACL_GROUP_OBJ, posix1e.ACL_MASK,
754 self.assertEqual(e.tag_type, tag)
755 # check we can show all tag types without breaking
756 self.assertTrue(str(e))
758 def testInvalidTags(self):
759 """Tests tag type incorrect set/get"""
764 self.assertRaises(TypeError, set_tag, object())
767 # For some reason, PyPy raises AttributeError. Strange...
768 self.assertRaises((TypeError, AttributeError), delete_tag)
770 e.tag_type = posix1e.ACL_USER_OBJ
771 tag = max([posix1e.ACL_USER, posix1e.ACL_GROUP, posix1e.ACL_USER_OBJ,
772 posix1e.ACL_GROUP_OBJ, posix1e.ACL_MASK,
773 posix1e.ACL_OTHER]) + 1
774 self.assertRaises(EnvironmentError, set_tag, tag)
775 # Check tag is still valid.
776 self.assertEqual(e.tag_type, posix1e.ACL_USER_OBJ)
778 if __name__ == "__main__":