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 (posix1e.ACL_USER, "user"),
57 (posix1e.ACL_GROUP, "group"),
58 (posix1e.ACL_USER_OBJ, "user object"),
59 (posix1e.ACL_GROUP_OBJ, "group object"),
60 (posix1e.ACL_MASK, "mask"),
61 (posix1e.ACL_OTHER, "other"),
64 ALL_TAG_VALUES = [i[0] for i in ALL_TAGS]
65 ALL_TAG_DESCS = [i[1] for i in ALL_TAGS]
67 # Check if running under Python 3
68 IS_PY_3K = sys.hexversion >= 0x03000000
70 # Fixtures and helpers
72 def ignore_ioerror(errnum, fn, *args, **kwargs):
73 """Call a function while ignoring some IOErrors.
75 This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL)
76 when doing certain operations on an invalid ACL.
82 err = sys.exc_info()[1]
83 if err.errno == errnum:
88 """Encode a string if needed (under Python 3)"""
96 """per-test temp dir based in TEST_DIR"""
97 with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname:
101 fh, fname = tempfile.mkstemp(".test", "xattr-", path)
104 @contextlib.contextmanager
105 def get_file_name(path):
106 fh, fname = get_file(path)
110 @contextlib.contextmanager
111 def get_file_fd(path):
112 fd = get_file(path)[0]
116 @contextlib.contextmanager
117 def get_file_object(path):
118 fd = get_file(path)[0]
119 with os.fdopen(fd) as f:
122 @contextlib.contextmanager
124 yield tempfile.mkdtemp(".test", "xattr-", path)
126 def get_symlink(path, dangling=True):
127 """create a symlink"""
128 fh, fname = get_file(path)
132 sname = fname + ".symlink"
133 os.symlink(fname, sname)
136 @contextlib.contextmanager
137 def get_valid_symlink(path):
138 yield get_symlink(path, dangling=False)[1]
140 @contextlib.contextmanager
141 def get_dangling_symlink(path):
142 yield get_symlink(path, dangling=True)[1]
144 @contextlib.contextmanager
145 def get_file_and_symlink(path):
146 yield get_symlink(path, dangling=False)
148 @contextlib.contextmanager
149 def get_file_and_fobject(path):
150 fh, fname = get_file(path)
151 with os.fdopen(fh) as fo:
154 # Wrappers that build upon existing values
156 def as_wrapper(call, fn, closer=None):
157 @contextlib.contextmanager
159 with call(path) as r:
162 if closer is not None:
167 return as_wrapper(call, lambda r: r.encode())
170 return as_wrapper(call, pathlib.PurePath)
172 def as_iostream(call):
173 opener = lambda f: io.open(f, "r")
174 closer = lambda r: r.close()
175 return as_wrapper(call, opener, closer)
177 NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)",
179 NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'",
182 require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE")
183 require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK")
184 require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY")
185 require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK")
186 require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE")
189 """Load/create tests"""
190 def test_from_file(self, testdir):
191 """Test loading ACLs from a file"""
192 _, fname = get_file(testdir)
193 acl1 = posix1e.ACL(file=fname)
196 def test_from_dir(self, testdir):
197 """Test loading ACLs from a directory"""
198 with get_dir(testdir) as dname:
199 acl1 = posix1e.ACL(file=dname)
200 acl2 = posix1e.ACL(filedef=dname)
202 # default ACLs might or might not be valid; missing ones are
203 # not valid, so we don't test acl2 for validity
205 def test_from_fd(self, testdir):
206 """Test loading ACLs from a file descriptor"""
207 fd, _ = get_file(testdir)
208 acl1 = posix1e.ACL(fd=fd)
211 def test_from_empty_invalid(self):
212 """Test creating an empty ACL"""
214 assert not acl1.valid()
216 def test_from_text(self):
217 """Test creating an ACL from text"""
218 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
221 def test_from_acl(self):
222 """Test creating an ACL from an existing ACL"""
224 acl2 = posix1e.ACL(acl=acl1)
227 def test_invalid_creation_params(self, testdir):
228 """Test that creating an ACL from multiple objects fails"""
229 fd, _ = get_file(testdir)
230 with pytest.raises(ValueError):
231 posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd)
233 def test_invalid_value_creation(self):
234 """Test that creating an ACL from wrong specification fails"""
235 with pytest.raises(EnvironmentError):
236 posix1e.ACL(text="foobar")
237 with pytest.raises(TypeError):
238 posix1e.ACL(foo="bar")
240 def test_double_init(self):
241 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
243 acl1.__init__(text=BASIC_ACL_TEXT)
246 class TestAclExtensions:
247 """ACL extensions checks"""
249 @require_acl_from_mode
250 def test_from_mode(self):
251 """Test loading ACLs from an octal mode"""
252 acl1 = posix1e.ACL(mode=M0644)
256 def test_acl_check(self):
257 """Test the acl_check method"""
258 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
259 assert not acl1.check()
263 @require_extended_check
264 def test_extended(self, testdir):
265 """Test the acl_extended function"""
266 fd, fname = get_file(testdir)
267 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
268 basic_acl.applyto(fd)
269 for item in fd, fname:
270 assert not has_extended(item)
271 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
272 assert enhanced_acl.valid()
273 enhanced_acl.applyto(fd)
274 for item in fd, fname:
275 assert has_extended(item)
277 @require_extended_check
278 def test_extended_arg_handling(self):
279 with pytest.raises(TypeError):
281 with pytest.raises(TypeError):
282 has_extended(object())
285 def test_equiv_mode(self):
286 """Test the equiv_mode function"""
287 if HAS_ACL_FROM_MODE:
288 for mode in M0644, M0755:
289 acl = posix1e.ACL(mode=mode)
290 assert acl.equiv_mode() == mode
291 acl = posix1e.ACL(text="u::rw,g::r,o::r")
292 assert acl.equiv_mode() == 0o644
293 acl = posix1e.ACL(text="u::rx,g::-,o::-")
294 assert acl.equiv_mode() == 0o500
297 def test_to_any_text(self):
298 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
299 assert encode("u::") in \
300 acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
301 assert encode("user::") in acl.to_any_text()
304 def test_to_any_text_wrong_args(self):
305 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
306 with pytest.raises(TypeError):
307 acl.to_any_text(foo="bar")
311 def test_rich_compare(self):
312 acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
313 acl2 = posix1e.ACL(acl=acl1)
314 acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
317 with pytest.raises(TypeError):
319 with pytest.raises(TypeError):
322 assert not (acl1 == 1)
323 with pytest.raises(TypeError):
326 @pytest.mark.skipif(not hasattr(posix1e.ACL, "__cmp__"), reason="__cmp__ is missing")
330 with pytest.raises(TypeError):
333 def test_apply_to_with_wrong_object(self):
334 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
336 with pytest.raises(TypeError):
337 acl1.applyto(object())
338 with pytest.raises(TypeError):
339 acl1.applyto(object(), object())
342 def test_acl_iterator(self):
343 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
345 assert entry.parent is acl
351 def test_delete_default(self, testdir):
352 """Test removing the default ACL"""
353 with get_dir(testdir) as dname:
354 posix1e.delete_default(dname)
357 def test_delete_default_wrong_arg(self):
358 with pytest.raises(TypeError):
359 posix1e.delete_default(object())
361 def test_reapply(self, testdir):
362 """Test re-applying an ACL"""
363 fd, fname = get_file(testdir)
364 acl1 = posix1e.ACL(fd=fd)
367 with get_dir(testdir) as dname:
368 acl2 = posix1e.ACL(file=fname)
374 class TestModification:
375 """ACL modification tests"""
377 def checkRef(self, obj):
378 """Checks if a given obj has a 'sane' refcount"""
379 if platform.python_implementation() == "PyPy":
381 ref_cnt = sys.getrefcount(obj)
382 # FIXME: hardcoded value for the max ref count... but I've
383 # seen it overflow on bad reference counting, so it's better
385 if ref_cnt < 2 or ref_cnt > 1024:
386 pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
390 """Test str() of an ACL."""
391 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
393 self.checkRef(str_acl)
395 def test_append(self):
396 """Test append a new Entry to the ACL"""
399 e.tag_type = posix1e.ACL_OTHER
400 ignore_ioerror(errno.EINVAL, acl.calc_mask)
402 self.checkRef(str_format)
404 ignore_ioerror(errno.EINVAL, acl.calc_mask)
405 assert not acl.valid()
407 def test_wrong_append(self):
408 """Test append a new Entry to the ACL based on wrong object type"""
410 with pytest.raises(TypeError):
413 def test_entry_creation(self):
415 e = posix1e.Entry(acl)
416 ignore_ioerror(errno.EINVAL, acl.calc_mask)
418 self.checkRef(str_format)
420 def test_entry_failed_creation(self):
421 # Checks for partial initialisation and deletion on error
423 with pytest.raises(TypeError):
424 posix1e.Entry(object())
426 def test_delete(self):
427 """Test delete Entry from the ACL"""
430 e.tag_type = posix1e.ACL_OTHER
431 ignore_ioerror(errno.EINVAL, acl.calc_mask)
433 ignore_ioerror(errno.EINVAL, acl.calc_mask)
435 def test_double_delete(self):
436 """Test delete Entry from the ACL"""
437 # This is not entirely valid/correct, since the entry object
438 # itself is invalid after the first deletion, so we're
439 # actually testing deleting an invalid object, not a
440 # non-existing entry...
443 e.tag_type = posix1e.ACL_OTHER
444 ignore_ioerror(errno.EINVAL, acl.calc_mask)
446 ignore_ioerror(errno.EINVAL, acl.calc_mask)
447 with pytest.raises(EnvironmentError):
450 # This currently fails as this deletion seems to be accepted :/
451 @pytest.mark.xfail(reason="Entry deletion is unreliable")
452 def testDeleteInvalidEntry(self):
453 """Test delete foreign Entry from the ACL"""
457 e.tag_type = posix1e.ACL_OTHER
458 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
459 with pytest.raises(EnvironmentError):
462 def test_delete_invalid_object(self):
463 """Test delete a non-Entry from the ACL"""
465 with pytest.raises(TypeError):
466 acl.delete_entry(object())
468 def test_double_entries(self):
469 """Test double entries"""
470 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
472 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
475 e.tag_type = tag_type
477 assert not acl.valid(), ("ACL containing duplicate entries"
478 " should not be valid")
481 def test_multiple_good_entries(self):
482 """Test multiple valid entries"""
483 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
485 for tag_type in (posix1e.ACL_USER,
487 for obj_id in range(5):
489 e.tag_type = tag_type
493 assert acl.valid(), ("ACL should be able to hold multiple"
494 " user/group entries")
496 def test_multiple_bad_entries(self):
497 """Test multiple invalid entries"""
498 for tag_type in (posix1e.ACL_USER,
500 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
503 e1.tag_type = tag_type
507 assert acl.valid(), ("ACL should be able to add a"
510 e2.tag_type = tag_type
513 ignore_ioerror(errno.EINVAL, acl.calc_mask)
514 assert not acl.valid(), ("ACL should not validate when"
515 " containing two duplicate entries")
517 # FreeBSD trips over itself here and can't delete the
518 # entry, even though it still exists.
519 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
524 e1.tag_type = ACL_USER
530 e2.tag_type = ACL_GROUP
537 assert e1.tag_type == e2.tag_type
539 def test_copy_wrong_arg(self):
542 with pytest.raises(TypeError):
545 def test_set_permset(self):
548 e1.tag_type = ACL_USER
554 e2.tag_type = ACL_GROUP
560 assert e2.permset.write
561 assert e2.tag_type == ACL_GROUP
563 def test_set_permset_wrong_arg(self):
566 with pytest.raises(TypeError):
569 def test_permset_creation(self):
574 #self.assertEqual(p1, p2)
576 def test_permset_creation_wrong_arg(self):
577 with pytest.raises(TypeError):
580 def test_permset(self):
581 """Test permissions"""
587 self.checkRef(str_ps)
588 for perm in PERMSETS:
590 txt = PERMSETS[perm][0]
591 self.checkRef(str_ps)
592 assert not ps.test(perm), ("Empty permission set should not"
593 " have permission '%s'" % txt)
595 assert ps.test(perm), ("Permission '%s' should exist"
596 " after addition" % txt)
598 self.checkRef(str_ps)
600 assert not ps.test(perm), ("Permission '%s' should not exist"
601 " after deletion" % txt)
603 def test_permset_via_accessors(self):
604 """Test permissions"""
610 self.checkRef(str_ps)
612 return PERMSETS[perm][1].__get__(ps)
613 def setter(parm, value):
614 return PERMSETS[perm][1].__set__(ps, value)
615 for perm in PERMSETS:
617 self.checkRef(str_ps)
618 txt = PERMSETS[perm][0]
619 assert not getter(perm), ("Empty permission set should not"
620 " have permission '%s'" % txt)
622 assert ps.test(perm), ("Permission '%s' should exist"
623 " after addition" % txt)
624 assert getter(perm), ("Permission '%s' should exist"
625 " after addition" % txt)
627 self.checkRef(str_ps)
629 assert not ps.test(perm), ("Permission '%s' should not exist"
630 " after deletion" % txt)
631 assert not getter(perm), ("Permission '%s' should not exist"
632 " after deletion" % txt)
634 def test_permset_invalid_type(self):
639 with pytest.raises(TypeError):
641 with pytest.raises(TypeError):
643 with pytest.raises(TypeError):
645 with pytest.raises(ValueError):
648 def test_qualifier_values(self):
649 """Tests qualifier correct store/retrieval"""
652 # work around deprecation warnings
653 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
657 if tag == posix1e.ACL_USER:
658 regex = re.compile("user with uid %d" % qualifier)
660 regex = re.compile("group with gid %d" % qualifier)
662 e.qualifier = qualifier
663 except OverflowError:
664 # reached overflow condition, break
666 assert e.qualifier == qualifier
667 assert regex.search(str(e)) is not None
670 def test_qualifier_overflow(self):
671 """Tests qualifier overflow handling"""
674 qualifier = sys.maxsize * 2
675 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
677 with pytest.raises(OverflowError):
678 e.qualifier = qualifier
680 def test_negative_qualifier(self):
681 """Tests negative qualifier handling"""
682 # Note: this presumes that uid_t/gid_t in C are unsigned...
685 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
687 for qualifier in [-10, -5, -1]:
688 with pytest.raises(OverflowError):
689 e.qualifier = qualifier
691 def test_invalid_qualifier(self):
692 """Tests invalid qualifier handling"""
695 with pytest.raises(TypeError):
696 e.qualifier = object()
697 with pytest.raises((TypeError, AttributeError)):
700 def test_qualifier_on_wrong_tag(self):
701 """Tests qualifier setting on wrong tag"""
704 e.tag_type = posix1e.ACL_OTHER
705 with pytest.raises(TypeError):
707 with pytest.raises(TypeError):
710 @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
711 def test_tag_types(self, tag):
712 """Tests tag type correct set/get"""
716 assert e.tag_type == tag
717 # check we can show all tag types without breaking
720 @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
721 @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
722 def test_tag_overwrite(self, src_tag, dst_tag):
723 """Tests tag type correct set/get"""
727 assert e.tag_type == src_tag
730 assert e.tag_type == dst_tag
733 def test_invalid_tags(self):
734 """Tests tag type incorrect set/get"""
737 with pytest.raises(TypeError):
738 e.tag_type = object()
739 e.tag_type = posix1e.ACL_USER_OBJ
740 # For some reason, PyPy raises AttributeError. Strange...
741 with pytest.raises((TypeError, AttributeError)):
744 def test_tag_wrong_overwrite(self):
747 e.tag_type = posix1e.ACL_USER_OBJ
748 tag = max(ALL_TAG_VALUES) + 1
749 with pytest.raises(EnvironmentError):
751 # Check tag is still valid.
752 assert e.tag_type == posix1e.ACL_USER_OBJ
754 if __name__ == "__main__":