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'",
177 """Support functions ACLs"""
180 """set up function"""
185 """tear down function"""
186 for fname in self.rmfiles:
188 for dname in self.rmdirs:
192 """create a temp file"""
193 fh, fname = tempfile.mkstemp(".test", "xattr-", TEST_DIR)
194 self.rmfiles.append(fname)
198 """create a temp dir"""
199 dname = tempfile.mkdtemp(".test", "xattr-", TEST_DIR)
200 self.rmdirs.append(dname)
203 def _getsymlink(self):
204 """create a symlink"""
205 fh, fname = self._getfile()
208 os.symlink(fname + ".non-existent", fname)
212 class LoadTests(aclTest, unittest.TestCase):
213 """Load/create tests"""
214 def testFromFile(self):
215 """Test loading ACLs from a file"""
216 _, fname = self._getfile()
217 acl1 = posix1e.ACL(file=fname)
218 self.assertTrue(acl1.valid(), "ACL read from file should be valid")
220 def testFromDir(self):
221 """Test loading ACLs from a directory"""
222 dname = self._getdir()
223 acl1 = posix1e.ACL(file=dname)
224 acl2 = posix1e.ACL(filedef=dname)
225 self.assertTrue(acl1.valid(),
226 "ACL read from directory should be valid")
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 testFromFd(self):
231 """Test loading ACLs from a file descriptor"""
232 fd, _ = self._getfile()
233 acl1 = posix1e.ACL(fd=fd)
234 self.assertTrue(acl1.valid(), "ACL read from fd should be valid")
236 def testFromEmpty(self):
237 """Test creating an empty ACL"""
239 self.assertFalse(acl1.valid(), "Empty ACL should not be valid")
241 def testFromText(self):
242 """Test creating an ACL from text"""
243 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
244 self.assertTrue(acl1.valid(),
245 "ACL based on standard description should be valid")
247 def testFromACL(self):
248 """Test creating an ACL from an existing ACL"""
250 acl2 = posix1e.ACL(acl=acl1)
252 def testInvalidCreationParams(self):
253 """Test that creating an ACL from multiple objects fails"""
254 fd, _ = self._getfile()
255 self.assertRaises(ValueError, posix1e.ACL, text=BASIC_ACL_TEXT, fd=fd)
257 def testInvalidValueCreation(self):
258 """Test that creating an ACL from wrong specification fails"""
259 self.assertRaises(EnvironmentError, posix1e.ACL, text="foobar")
260 self.assertRaises(TypeError, posix1e.ACL, foo="bar")
262 def testDoubleInit(self):
263 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
264 self.assertTrue(acl1.valid())
265 acl1.__init__(text=BASIC_ACL_TEXT)
266 self.assertTrue(acl1.valid())
268 class AclExtensions(aclTest, unittest.TestCase):
269 """ACL extensions checks"""
271 @unittest.skipUnless(HAS_ACL_FROM_MODE, "Missing HAS_ACL_FROM_MODE")
272 def testFromMode(self):
273 """Test loading ACLs from an octal mode"""
274 acl1 = posix1e.ACL(mode=M0644)
275 self.assertTrue(acl1.valid(),
276 "ACL created via octal mode shoule be valid")
278 @unittest.skipUnless(HAS_ACL_CHECK, "ACL check not supported")
279 def testAclCheck(self):
280 """Test the acl_check method"""
281 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
282 self.assertFalse(acl1.check(), "ACL is not valid")
284 self.assertTrue(acl2.check(), "Empty ACL should not be valid")
286 @unittest.skipUnless(HAS_EXTENDED_CHECK, "Extended ACL check not supported")
287 def testExtended(self):
288 """Test the acl_extended function"""
289 fd, fname = self._getfile()
290 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
291 basic_acl.applyto(fd)
292 for item in fd, fname:
293 self.assertFalse(has_extended(item),
294 "A simple ACL should not be reported as extended")
295 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
296 self.assertTrue(enhanced_acl.valid(),
297 "Failure to build an extended ACL")
298 enhanced_acl.applyto(fd)
299 for item in fd, fname:
300 self.assertTrue(has_extended(item),
301 "An extended ACL should be reported as such")
303 @unittest.skipUnless(HAS_EXTENDED_CHECK, "Extended ACL check not supported")
304 def testExtendedArgHandling(self):
305 self.assertRaises(TypeError, has_extended)
306 self.assertRaises(TypeError, has_extended, object())
308 @unittest.skipUnless(HAS_EQUIV_MODE, "equiv_mode not supported")
309 def testEquivMode(self):
310 """Test the equiv_mode function"""
311 if HAS_ACL_FROM_MODE:
312 for mode in M0644, M0755:
313 acl = posix1e.ACL(mode=mode)
314 self.assertEqual(acl.equiv_mode(), mode)
315 acl = posix1e.ACL(text="u::rw,g::r,o::r")
316 self.assertEqual(acl.equiv_mode(), M0644)
317 acl = posix1e.ACL(text="u::rx,g::-,o::-")
318 self.assertEqual(acl.equiv_mode(), M0500)
320 @unittest.skipUnless(HAS_ACL_CHECK, "ACL check not supported")
321 def testToAnyText(self):
322 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
323 self.assertIn(encode("u::"),
324 acl.to_any_text(options=posix1e.TEXT_ABBREVIATE))
325 self.assertIn(encode("user::"), acl.to_any_text())
327 @unittest.skipUnless(HAS_ACL_CHECK, "ACL check not supported")
328 def testToAnyTextWrongArgs(self):
329 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
330 self.assertRaises(TypeError, acl.to_any_text, foo="bar")
333 @unittest.skipUnless(HAS_ACL_CHECK, "ACL check not supported")
334 def testRichCompare(self):
335 acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
336 acl2 = posix1e.ACL(acl=acl1)
337 acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
338 self.assertEqual(acl1, acl2)
339 self.assertNotEqual(acl1, acl3)
340 self.assertRaises(TypeError, operator.lt, acl1, acl2)
341 self.assertRaises(TypeError, operator.ge, acl1, acl3)
342 self.assertTrue(acl1 != True)
343 self.assertFalse(acl1 == 1)
344 self.assertRaises(TypeError, operator.gt, acl1, True)
346 @unittest.skipUnless(hasattr(posix1e.ACL, "__cmp__"), "__cmp__ is missing")
347 @unittest.skipUnless(__pypy__ is None, "Disabled under pypy")
350 self.assertRaises(TypeError, acl1.__cmp__, acl1)
352 def testApplyToWithWrongObject(self):
353 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
354 self.assertTrue(acl1.valid())
355 self.assertRaises(TypeError, acl1.applyto, object())
356 self.assertRaises(TypeError, acl1.applyto, object(), object())
358 @unittest.skipUnless(HAS_ACL_ENTRY, "ACL entries not supported")
359 def testAclIterator(self):
360 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
361 #self.assertEqual(len(acl), 3)
363 self.assertIs(entry.parent, acl)
366 class WriteTests(aclTest, unittest.TestCase):
369 def testDeleteDefault(self):
370 """Test removing the default ACL"""
371 dname = self._getdir()
372 posix1e.delete_default(dname)
374 @unittest.skipUnless(__pypy__ is None, "Disabled under pypy")
375 def testDeleteDefaultWrongArg(self):
376 self.assertRaises(TypeError, posix1e.delete_default, object())
378 def testReapply(self):
379 """Test re-applying an ACL"""
380 fd, fname = self._getfile()
381 acl1 = posix1e.ACL(fd=fd)
384 dname = self._getdir()
385 acl2 = posix1e.ACL(file=fname)
389 @unittest.skipUnless(HAS_ACL_ENTRY, "ACL entries not supported")
390 class ModificationTests(aclTest, unittest.TestCase):
391 """ACL modification tests"""
393 def checkRef(self, obj):
394 """Checks if a given obj has a 'sane' refcount"""
395 if platform.python_implementation() == "PyPy":
397 ref_cnt = sys.getrefcount(obj)
398 # FIXME: hardcoded value for the max ref count... but I've
399 # seen it overflow on bad reference counting, so it's better
401 if ref_cnt < 2 or ref_cnt > 1024:
402 self.fail("Wrong reference count, expected 2-1024 and got %d" %
406 """Test str() of an ACL."""
407 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
409 self.checkRef(str_acl)
411 def testAppend(self):
412 """Test append a new Entry to the ACL"""
415 e.tag_type = posix1e.ACL_OTHER
416 ignore_ioerror(errno.EINVAL, acl.calc_mask)
418 self.checkRef(str_format)
420 ignore_ioerror(errno.EINVAL, acl.calc_mask)
421 self.assertFalse(acl.valid())
423 def testWrongAppend(self):
424 """Test append a new Entry to the ACL based on wrong object type"""
426 self.assertRaises(TypeError, acl.append, object())
428 def testEntryCreation(self):
430 e = posix1e.Entry(acl)
431 ignore_ioerror(errno.EINVAL, acl.calc_mask)
433 self.checkRef(str_format)
435 def testEntryFailedCreation(self):
436 # Checks for partial initialisation and deletion on error
438 self.assertRaises(TypeError, posix1e.Entry, object())
440 def testDelete(self):
441 """Test delete Entry from the ACL"""
444 e.tag_type = posix1e.ACL_OTHER
445 ignore_ioerror(errno.EINVAL, acl.calc_mask)
447 ignore_ioerror(errno.EINVAL, acl.calc_mask)
449 def testDoubleDelete(self):
450 """Test delete Entry from the ACL"""
451 # This is not entirely valid/correct, since the entry object
452 # itself is invalid after the first deletion, so we're
453 # actually testing deleting an invalid object, not a
454 # non-existing entry...
457 e.tag_type = posix1e.ACL_OTHER
458 ignore_ioerror(errno.EINVAL, acl.calc_mask)
460 ignore_ioerror(errno.EINVAL, acl.calc_mask)
461 self.assertRaises(EnvironmentError, acl.delete_entry, e)
463 # This currently fails as this deletion seems to be accepted :/
464 @unittest.skip("Entry deletion is unreliable")
465 def testDeleteInvalidEntry(self):
466 """Test delete foreign Entry from the ACL"""
470 e.tag_type = posix1e.ACL_OTHER
471 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
472 self.assertRaises(EnvironmentError, acl2.delete_entry, e)
474 def testDeleteInvalidObject(self):
475 """Test delete a non-Entry from the ACL"""
477 self.assertRaises(TypeError, acl.delete_entry, object())
479 def testDoubleEntries(self):
480 """Test double entries"""
481 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
482 self.assertTrue(acl.valid(), "ACL is not valid")
483 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
486 e.tag_type = tag_type
488 self.assertFalse(acl.valid(),
489 "ACL containing duplicate entries"
490 " should not be valid")
493 def testMultipleGoodEntries(self):
494 """Test multiple valid entries"""
495 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
496 self.assertTrue(acl.valid(), "ACL is not valid")
497 for tag_type in (posix1e.ACL_USER,
499 for obj_id in range(5):
501 e.tag_type = tag_type
505 self.assertTrue(acl.valid(),
506 "ACL should be able to hold multiple"
507 " user/group entries")
509 def testMultipleBadEntries(self):
510 """Test multiple invalid entries"""
511 for tag_type in (posix1e.ACL_USER,
513 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
514 self.assertTrue(acl.valid(), "ACL built from standard description"
517 e1.tag_type = tag_type
521 self.assertTrue(acl.valid(), "ACL should be able to add a"
524 e2.tag_type = tag_type
527 ignore_ioerror(errno.EINVAL, acl.calc_mask)
528 self.assertFalse(acl.valid(), "ACL should not validate when"
529 " containing two duplicate entries")
531 # FreeBSD trips over itself here and can't delete the
532 # entry, even though it still exists.
533 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
538 e1.tag_type = ACL_USER
544 e2.tag_type = ACL_GROUP
548 self.assertFalse(p2.write)
550 self.assertTrue(p2.write)
551 self.assertEqual(e1.tag_type, e2.tag_type)
553 def testCopyWrongArg(self):
556 self.assertRaises(TypeError, e.copy, object())
558 def testSetPermset(self):
561 e1.tag_type = ACL_USER
567 e2.tag_type = ACL_GROUP
571 self.assertFalse(p2.write)
573 self.assertTrue(e2.permset.write)
574 self.assertEqual(e2.tag_type, ACL_GROUP)
576 def testSetPermsetWrongArg(self):
581 self.assertRaises(TypeError, setter, object())
583 def testPermsetCreation(self):
588 #self.assertEqual(p1, p2)
590 def testPermsetCreationWrongArg(self):
591 self.assertRaises(TypeError, Permset, object())
593 def testPermset(self):
594 """Test permissions"""
600 self.checkRef(str_ps)
601 for perm in PERMSETS:
603 txt = PERMSETS[perm][0]
604 self.checkRef(str_ps)
605 self.assertFalse(ps.test(perm), "Empty permission set should not"
606 " have permission '%s'" % txt)
608 self.assertTrue(ps.test(perm), "Permission '%s' should exist"
609 " after addition" % txt)
611 self.checkRef(str_ps)
613 self.assertFalse(ps.test(perm), "Permission '%s' should not exist"
614 " after deletion" % txt)
616 def testPermsetViaAccessors(self):
617 """Test permissions"""
623 self.checkRef(str_ps)
625 return PERMSETS[perm][1].__get__(ps)
626 def setter(parm, value):
627 return PERMSETS[perm][1].__set__(ps, value)
628 for perm in PERMSETS:
630 self.checkRef(str_ps)
631 txt = PERMSETS[perm][0]
632 self.assertFalse(getter(perm), "Empty permission set should not"
633 " have permission '%s'" % txt)
635 self.assertTrue(ps.test(perm), "Permission '%s' should exist"
636 " after addition" % txt)
637 self.assertTrue(getter(perm), "Permission '%s' should exist"
638 " after addition" % txt)
640 self.checkRef(str_ps)
642 self.assertFalse(ps.test(perm), "Permission '%s' should not exist"
643 " after deletion" % txt)
644 self.assertFalse(getter(perm), "Permission '%s' should not exist"
645 " after deletion" % txt)
647 def testPermsetInvalidType(self):
654 self.assertRaises(TypeError, ps.add, "foobar")
655 self.assertRaises(TypeError, ps.delete, "foobar")
656 self.assertRaises(TypeError, ps.test, "foobar")
657 self.assertRaises(ValueError, setter)
659 @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3")
660 def testQualifierValues(self):
661 """Tests qualifier correct store/retrieval"""
664 # work around deprecation warnings
665 if hasattr(self, 'assertRegex'):
666 fn = self.assertRegex
668 fn = self.assertRegexpMatches
669 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
673 if tag == posix1e.ACL_USER:
674 regex = re.compile("user with uid %d" % qualifier)
676 regex = re.compile("group with gid %d" % qualifier)
678 e.qualifier = qualifier
679 except OverflowError:
680 # reached overflow condition, break
682 self.assertEqual(e.qualifier, qualifier)
686 @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3")
687 def testQualifierOverflow(self):
688 """Tests qualifier overflow handling"""
691 qualifier = sys.maxsize * 2
692 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
694 with self.assertRaises(OverflowError):
695 e.qualifier = qualifier
697 @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3")
698 def testNegativeQualifier(self):
699 """Tests negative qualifier handling"""
700 # Note: this presumes that uid_t/gid_t in C are unsigned...
703 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
705 for qualifier in [-10, -5, -1]:
706 with self.assertRaises(OverflowError):
707 e.qualifier = qualifier
709 def testInvalidQualifier(self):
710 """Tests invalid qualifier handling"""
717 self.assertRaises(TypeError, set_qual, object())
718 self.assertRaises((TypeError, AttributeError), del_qual)
720 def testQualifierOnWrongTag(self):
721 """Tests qualifier setting on wrong tag"""
724 e.tag_type = posix1e.ACL_OTHER
729 self.assertRaises(TypeError, set_qual, 1)
730 self.assertRaises(TypeError, get_qual)
733 def testTagTypes(self):
734 """Tests tag type correct set/get"""
737 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP, posix1e.ACL_USER_OBJ,
738 posix1e.ACL_GROUP_OBJ, posix1e.ACL_MASK,
741 self.assertEqual(e.tag_type, tag)
742 # check we can show all tag types without breaking
743 self.assertTrue(str(e))
745 def testInvalidTags(self):
746 """Tests tag type incorrect set/get"""
751 self.assertRaises(TypeError, set_tag, object())
754 # For some reason, PyPy raises AttributeError. Strange...
755 self.assertRaises((TypeError, AttributeError), delete_tag)
757 e.tag_type = posix1e.ACL_USER_OBJ
758 tag = max([posix1e.ACL_USER, posix1e.ACL_GROUP, posix1e.ACL_USER_OBJ,
759 posix1e.ACL_GROUP_OBJ, posix1e.ACL_MASK,
760 posix1e.ACL_OTHER]) + 1
761 self.assertRaises(EnvironmentError, set_tag, tag)
762 # Check tag is still valid.
763 self.assertEqual(e.tag_type, posix1e.ACL_USER_OBJ)
765 if __name__ == "__main__":