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
32 import pytest # type: ignore
40 TEST_DIR = os.environ.get("TEST_DIR", ".")
42 BASIC_ACL_TEXT = "u::rw,g::r,o::-"
44 # Permset permission information
46 posix1e.ACL_READ: ("read", posix1e.Permset.read),
47 posix1e.ACL_WRITE: ("write", posix1e.Permset.write),
48 posix1e.ACL_EXECUTE: ("execute", posix1e.Permset.execute),
52 (posix1e.ACL_USER, "user"),
53 (posix1e.ACL_GROUP, "group"),
54 (posix1e.ACL_USER_OBJ, "user object"),
55 (posix1e.ACL_GROUP_OBJ, "group object"),
56 (posix1e.ACL_MASK, "mask"),
57 (posix1e.ACL_OTHER, "other"),
60 ALL_TAG_VALUES = [i[0] for i in ALL_TAGS]
61 ALL_TAG_DESCS = [i[1] for i in ALL_TAGS]
63 # Fixtures and helpers
65 def ignore_ioerror(errnum, fn, *args, **kwargs):
66 """Call a function while ignoring some IOErrors.
68 This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL)
69 when doing certain operations on an invalid ACL.
74 except IOError as err:
75 if err.errno == errnum:
81 """per-test temp dir based in TEST_DIR"""
82 with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname:
86 fh, fname = tempfile.mkstemp(".test", "xattr-", path)
89 @contextlib.contextmanager
90 def get_file_name(path):
91 fh, fname = get_file(path)
95 @contextlib.contextmanager
96 def get_file_fd(path):
97 fd = get_file(path)[0]
101 @contextlib.contextmanager
102 def get_file_object(path):
103 fd = get_file(path)[0]
104 with os.fdopen(fd) as f:
107 @contextlib.contextmanager
109 yield tempfile.mkdtemp(".test", "xattr-", path)
111 def get_symlink(path, dangling=True):
112 """create a symlink"""
113 fh, fname = get_file(path)
117 sname = fname + ".symlink"
118 os.symlink(fname, sname)
121 @contextlib.contextmanager
122 def get_valid_symlink(path):
123 yield get_symlink(path, dangling=False)[1]
125 @contextlib.contextmanager
126 def get_dangling_symlink(path):
127 yield get_symlink(path, dangling=True)[1]
129 @contextlib.contextmanager
130 def get_file_and_symlink(path):
131 yield get_symlink(path, dangling=False)
133 @contextlib.contextmanager
134 def get_file_and_fobject(path):
135 fh, fname = get_file(path)
136 with os.fdopen(fh) as fo:
139 # Wrappers that build upon existing values
141 def as_wrapper(call, fn, closer=None):
142 @contextlib.contextmanager
144 with call(path) as r:
147 if closer is not None:
152 return as_wrapper(call, lambda r: r.encode())
155 return as_wrapper(call, pathlib.PurePath)
157 def as_iostream(call):
158 opener = lambda f: io.open(f, "r")
159 closer = lambda r: r.close()
160 return as_wrapper(call, opener, closer)
162 NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)",
164 NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'",
167 require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE")
168 require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK")
169 require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY")
170 require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK")
171 require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE")
172 require_copy_ext = pytest.mark.skipif("not HAS_COPY_EXT")
174 # Note: ACLs are valid only for files/directories, not symbolic links
175 # themselves, so we only create valid symlinks.
178 as_bytes(get_file_name),
179 pytest.param(as_fspath(get_file_name),
180 marks=[NOT_BEFORE_36, NOT_PYPY]),
183 pytest.param(as_fspath(get_dir),
184 marks=[NOT_BEFORE_36, NOT_PYPY]),
186 as_bytes(get_valid_symlink),
187 pytest.param(as_fspath(get_valid_symlink),
188 marks=[NOT_BEFORE_36, NOT_PYPY]),
199 "file via symlink (bytes)",
200 "file via symlink (path)",
206 as_iostream(get_file_name),
215 ALL_P = FILE_P + FD_P
216 ALL_D = FILE_D + FD_D
218 @pytest.fixture(params=FILE_P, ids=FILE_D)
219 def file_subject(testdir, request):
220 with request.param(testdir) as value:
223 @pytest.fixture(params=FD_P, ids=FD_D)
224 def fd_subject(testdir, request):
225 with request.param(testdir) as value:
228 @pytest.fixture(params=ALL_P, ids=ALL_D)
229 def subject(testdir, request):
230 with request.param(testdir) as value:
235 """Load/create tests"""
236 def test_from_file(self, testdir):
237 """Test loading ACLs from a file"""
238 _, fname = get_file(testdir)
239 acl1 = posix1e.ACL(file=fname)
242 def test_from_dir(self, testdir):
243 """Test loading ACLs from a directory"""
244 with get_dir(testdir) as dname:
245 acl1 = posix1e.ACL(file=dname)
246 acl2 = posix1e.ACL(filedef=dname)
248 # default ACLs might or might not be valid; missing ones are
249 # not valid, so we don't test acl2 for validity
251 def test_from_fd(self, testdir):
252 """Test loading ACLs from a file descriptor"""
253 fd, _ = get_file(testdir)
254 acl1 = posix1e.ACL(fd=fd)
257 def test_from_nonexisting(self, testdir):
258 _, fname = get_file(testdir)
259 with pytest.raises(IOError):
260 posix1e.ACL(file="fname"+".no-such-file")
262 def test_from_invalid_fd(self, testdir):
263 fd, _ = get_file(testdir)
265 with pytest.raises(IOError):
268 def test_from_empty_invalid(self):
269 """Test creating an empty ACL"""
271 assert not acl1.valid()
273 def test_from_text(self):
274 """Test creating an ACL from text"""
275 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
278 def test_from_acl(self):
279 """Test creating an ACL from an existing ACL"""
281 acl2 = posix1e.ACL(acl=acl1)
284 def test_invalid_creation_params(self, testdir):
285 """Test that creating an ACL from multiple objects fails"""
286 fd, _ = get_file(testdir)
287 with pytest.raises(ValueError):
288 posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd)
290 def test_invalid_value_creation(self):
291 """Test that creating an ACL from wrong specification fails"""
292 with pytest.raises(EnvironmentError):
293 posix1e.ACL(text="foobar")
294 with pytest.raises(TypeError):
295 posix1e.ACL(foo="bar")
297 def test_double_init(self):
298 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
300 acl1.__init__(text=BASIC_ACL_TEXT)
303 class TestAclExtensions:
304 """ACL extensions checks"""
306 @require_acl_from_mode
307 def test_from_mode(self):
308 """Test loading ACLs from an octal mode"""
309 acl1 = posix1e.ACL(mode=0o644)
313 def test_acl_check(self):
314 """Test the acl_check method"""
315 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
316 assert not acl1.check()
319 assert c == (ACL_MISS_ERROR, 0)
320 assert isinstance(c, tuple)
321 assert c[0] == ACL_MISS_ERROR
324 assert c == (ACL_ENTRY_ERROR, 0)
326 def test_applyto(self, subject):
327 """Test the apply_to function"""
328 # TODO: add read/compare with before, once ACL can be init'ed
330 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
331 basic_acl.applyto(subject)
332 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
333 assert enhanced_acl.valid()
334 enhanced_acl.applyto(subject)
336 def test_apply_to_with_wrong_object(self):
337 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
339 with pytest.raises(TypeError):
340 acl1.applyto(object())
341 with pytest.raises(TypeError):
342 acl1.applyto(object(), object())
344 def test_apply_to_fail(self, testdir):
345 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
347 fd, fname = get_file(testdir)
349 with pytest.raises(IOError):
351 with pytest.raises(IOError, match="no-such-file"):
352 acl1.applyto(fname+".no-such-file")
354 @require_extended_check
355 def test_applyto_extended(self, subject):
356 """Test the acl_extended function"""
357 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
358 basic_acl.applyto(subject)
359 assert not has_extended(subject)
360 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
361 assert enhanced_acl.valid()
362 enhanced_acl.applyto(subject)
363 assert has_extended(subject)
365 @require_extended_check
366 @pytest.mark.parametrize(
367 "gen", [ get_file_and_symlink, get_file_and_fobject ])
368 def test_applyto_extended_mixed(self, testdir, gen):
369 """Test the acl_extended function"""
370 with gen(testdir) as (a, b):
371 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
374 assert not has_extended(item)
375 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
376 assert enhanced_acl.valid()
377 enhanced_acl.applyto(b)
379 assert has_extended(item)
381 @require_extended_check
382 def test_extended_fail(self, testdir):
383 fd, fname = get_file(testdir)
385 with pytest.raises(IOError):
387 with pytest.raises(IOError, match="no-such-file"):
388 has_extended(fname+".no-such-file")
390 @require_extended_check
391 def test_extended_arg_handling(self):
392 with pytest.raises(TypeError):
394 with pytest.raises(TypeError):
395 has_extended(object())
398 def test_equiv_mode(self):
399 """Test the equiv_mode function"""
400 if HAS_ACL_FROM_MODE:
401 for mode in 0o644, 0o755:
402 acl = posix1e.ACL(mode=mode)
403 assert acl.equiv_mode() == mode
404 acl = posix1e.ACL(text="u::rw,g::r,o::r")
405 assert acl.equiv_mode() == 0o644
406 acl = posix1e.ACL(text="u::rx,g::-,o::-")
407 assert acl.equiv_mode() == 0o500
410 def test_to_any_text(self):
411 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
413 acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
414 assert b"user::" in acl.to_any_text()
417 def test_to_any_text_wrong_args(self):
418 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
419 with pytest.raises(TypeError):
420 acl.to_any_text(foo="bar")
424 def test_rich_compare(self):
425 acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
426 acl2 = posix1e.ACL(acl=acl1)
427 acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
430 with pytest.raises(TypeError):
432 with pytest.raises(TypeError):
435 assert not (acl1 == 1)
436 with pytest.raises(TypeError):
440 def test_acl_iterator(self):
441 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
443 assert entry.parent is acl
446 def test_acl_copy_ext(self):
447 a = posix1e.ACL(text=BASIC_ACL_TEXT)
449 c = posix1e.ACL(acl=b)
452 state = a.__getstate__()
453 b.__setstate__(state)
461 def test_delete_default(self, testdir):
462 """Test removing the default ACL"""
463 with get_dir(testdir) as dname:
464 posix1e.delete_default(dname)
466 def test_delete_default_fail(self, testdir):
467 """Test removing the default ACL"""
468 with get_file_name(testdir) as fname:
469 with pytest.raises(IOError, match="no-such-file"):
470 posix1e.delete_default(fname+".no-such-file")
473 def test_delete_default_wrong_arg(self):
474 with pytest.raises(TypeError):
475 posix1e.delete_default(object())
477 def test_reapply(self, testdir):
478 """Test re-applying an ACL"""
479 fd, fname = get_file(testdir)
480 acl1 = posix1e.ACL(fd=fd)
483 with get_dir(testdir) as dname:
484 acl2 = posix1e.ACL(file=fname)
490 class TestModification:
491 """ACL modification tests"""
493 def checkRef(self, obj):
494 """Checks if a given obj has a 'sane' refcount"""
495 if platform.python_implementation() == "PyPy":
497 ref_cnt = sys.getrefcount(obj)
498 # FIXME: hardcoded value for the max ref count... but I've
499 # seen it overflow on bad reference counting, so it's better
501 if ref_cnt < 2 or ref_cnt > 1024:
502 pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
506 """Test str() of an ACL."""
507 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
509 self.checkRef(str_acl)
511 def test_append(self):
512 """Test append a new Entry to the ACL"""
515 e.tag_type = posix1e.ACL_OTHER
516 ignore_ioerror(errno.EINVAL, acl.calc_mask)
518 self.checkRef(str_format)
520 ignore_ioerror(errno.EINVAL, acl.calc_mask)
521 assert not acl.valid()
523 def test_wrong_append(self):
524 """Test append a new Entry to the ACL based on wrong object type"""
526 with pytest.raises(TypeError):
529 def test_entry_creation(self):
531 e = posix1e.Entry(acl)
532 ignore_ioerror(errno.EINVAL, acl.calc_mask)
534 self.checkRef(str_format)
536 def test_entry_failed_creation(self):
537 # Checks for partial initialisation and deletion on error
539 with pytest.raises(TypeError):
540 posix1e.Entry(object())
542 def test_entry_reinitialisations(self):
547 with pytest.raises(ValueError, match="different parent"):
551 def test_entry_reinit_leaks_refcount(self):
554 ref = sys.getrefcount(acl)
556 assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..."
558 def test_delete(self):
559 """Test delete Entry from the ACL"""
562 e.tag_type = posix1e.ACL_OTHER
563 ignore_ioerror(errno.EINVAL, acl.calc_mask)
565 ignore_ioerror(errno.EINVAL, acl.calc_mask)
567 def test_double_delete(self):
568 """Test delete Entry from the ACL"""
569 # This is not entirely valid/correct, since the entry object
570 # itself is invalid after the first deletion, so we're
571 # actually testing deleting an invalid object, not a
572 # non-existing entry...
575 e.tag_type = posix1e.ACL_OTHER
576 ignore_ioerror(errno.EINVAL, acl.calc_mask)
578 ignore_ioerror(errno.EINVAL, acl.calc_mask)
579 with pytest.raises(EnvironmentError):
582 # This currently fails as this deletion seems to be accepted :/
583 @pytest.mark.xfail(reason="Entry deletion is unreliable")
584 def testDeleteInvalidEntry(self):
585 """Test delete foreign Entry from the ACL"""
589 e.tag_type = posix1e.ACL_OTHER
590 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
591 with pytest.raises(EnvironmentError):
594 def test_delete_invalid_object(self):
595 """Test delete a non-Entry from the ACL"""
597 with pytest.raises(TypeError):
598 acl.delete_entry(object())
600 def test_double_entries(self):
601 """Test double entries"""
602 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
604 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
607 e.tag_type = tag_type
609 assert not acl.valid(), ("ACL containing duplicate entries"
610 " should not be valid")
613 def test_multiple_good_entries(self):
614 """Test multiple valid entries"""
615 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
617 for tag_type in (posix1e.ACL_USER,
619 for obj_id in range(5):
621 e.tag_type = tag_type
625 assert acl.valid(), ("ACL should be able to hold multiple"
626 " user/group entries")
628 def test_multiple_bad_entries(self):
629 """Test multiple invalid entries"""
630 for tag_type in (posix1e.ACL_USER,
632 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
635 e1.tag_type = tag_type
639 assert acl.valid(), ("ACL should be able to add a"
642 e2.tag_type = tag_type
645 ignore_ioerror(errno.EINVAL, acl.calc_mask)
646 assert not acl.valid(), ("ACL should not validate when"
647 " containing two duplicate entries")
649 # FreeBSD trips over itself here and can't delete the
650 # entry, even though it still exists.
651 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
656 e1.tag_type = ACL_USER
662 e2.tag_type = ACL_GROUP
669 assert e1.tag_type == e2.tag_type
671 def test_copy_wrong_arg(self):
674 with pytest.raises(TypeError):
677 def test_set_permset(self):
680 e1.tag_type = ACL_USER
686 e2.tag_type = ACL_GROUP
692 assert e2.permset.write
693 assert e2.tag_type == ACL_GROUP
695 def test_set_permset_wrong_arg(self):
698 with pytest.raises(TypeError):
701 def test_permset_creation(self):
708 def test_permset_creation_wrong_arg(self):
709 with pytest.raises(TypeError):
712 def test_permset_reinitialisations(self):
718 with pytest.raises(ValueError, match="different parent"):
722 def test_permset_reinit_leaks_refcount(self):
726 ref = sys.getrefcount(e)
728 assert ref == sys.getrefcount(e), "Uh-oh, ref leaks..."
730 def test_permset(self):
731 """Test permissions"""
737 self.checkRef(str_ps)
738 for perm in PERMSETS:
740 txt = PERMSETS[perm][0]
741 self.checkRef(str_ps)
742 assert not ps.test(perm), ("Empty permission set should not"
743 " have permission '%s'" % txt)
745 assert ps.test(perm), ("Permission '%s' should exist"
746 " after addition" % txt)
748 self.checkRef(str_ps)
750 assert not ps.test(perm), ("Permission '%s' should not exist"
751 " after deletion" % txt)
753 def test_permset_via_accessors(self):
754 """Test permissions"""
760 self.checkRef(str_ps)
762 return PERMSETS[perm][1].__get__(ps)
763 def setter(parm, value):
764 return PERMSETS[perm][1].__set__(ps, value)
765 for perm in PERMSETS:
767 self.checkRef(str_ps)
768 txt = PERMSETS[perm][0]
769 assert not getter(perm), ("Empty permission set should not"
770 " have permission '%s'" % txt)
772 assert ps.test(perm), ("Permission '%s' should exist"
773 " after addition" % txt)
774 assert getter(perm), ("Permission '%s' should exist"
775 " after addition" % txt)
777 self.checkRef(str_ps)
779 assert not ps.test(perm), ("Permission '%s' should not exist"
780 " after deletion" % txt)
781 assert not getter(perm), ("Permission '%s' should not exist"
782 " after deletion" % txt)
784 def test_permset_invalid_type(self):
789 with pytest.raises(TypeError):
791 with pytest.raises(TypeError):
793 with pytest.raises(TypeError):
795 with pytest.raises(ValueError):
798 def test_qualifier_values(self):
799 """Tests qualifier correct store/retrieval"""
802 # work around deprecation warnings
803 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
807 if tag == posix1e.ACL_USER:
808 regex = re.compile("user with uid %d" % qualifier)
810 regex = re.compile("group with gid %d" % qualifier)
812 e.qualifier = qualifier
813 except OverflowError:
814 # reached overflow condition, break
816 assert e.qualifier == qualifier
817 assert regex.search(str(e)) is not None
820 def test_qualifier_overflow(self):
821 """Tests qualifier overflow handling"""
824 qualifier = sys.maxsize * 2
825 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
827 with pytest.raises(OverflowError):
828 e.qualifier = qualifier
830 def test_negative_qualifier(self):
831 """Tests negative qualifier handling"""
832 # Note: this presumes that uid_t/gid_t in C are unsigned...
835 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
837 for qualifier in [-10, -5, -1]:
838 with pytest.raises(OverflowError):
839 e.qualifier = qualifier
841 def test_invalid_qualifier(self):
842 """Tests invalid qualifier handling"""
845 with pytest.raises(TypeError):
846 e.qualifier = object()
847 with pytest.raises((TypeError, AttributeError)):
850 def test_qualifier_on_wrong_tag(self):
851 """Tests qualifier setting on wrong tag"""
854 e.tag_type = posix1e.ACL_OTHER
855 with pytest.raises(TypeError):
857 with pytest.raises(TypeError):
860 @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
861 def test_tag_types(self, tag):
862 """Tests tag type correct set/get"""
866 assert e.tag_type == tag
867 # check we can show all tag types without breaking
870 @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
871 @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
872 def test_tag_overwrite(self, src_tag, dst_tag):
873 """Tests tag type correct set/get"""
877 assert e.tag_type == src_tag
880 assert e.tag_type == dst_tag
883 def test_invalid_tags(self):
884 """Tests tag type incorrect set/get"""
887 with pytest.raises(TypeError):
888 e.tag_type = object()
889 e.tag_type = posix1e.ACL_USER_OBJ
890 # For some reason, PyPy raises AttributeError. Strange...
891 with pytest.raises((TypeError, AttributeError)):
894 def test_tag_wrong_overwrite(self):
897 e.tag_type = posix1e.ACL_USER_OBJ
898 tag = max(ALL_TAG_VALUES) + 1
899 with pytest.raises(EnvironmentError):
901 # Check tag is still valid.
902 assert e.tag_type == posix1e.ACL_USER_OBJ
904 if __name__ == "__main__":