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
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()
320 def test_applyto(self, subject):
321 """Test the apply_to function"""
322 # TODO: add read/compare with before, once ACL can be init'ed
324 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
325 basic_acl.applyto(subject)
326 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
327 assert enhanced_acl.valid()
328 enhanced_acl.applyto(subject)
330 def test_apply_to_with_wrong_object(self):
331 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
333 with pytest.raises(TypeError):
334 acl1.applyto(object())
335 with pytest.raises(TypeError):
336 acl1.applyto(object(), object())
338 def test_apply_to_fail(self, testdir):
339 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
341 fd, fname = get_file(testdir)
343 with pytest.raises(IOError):
345 with pytest.raises(IOError, match="no-such-file"):
346 acl1.applyto(fname+".no-such-file")
348 @require_extended_check
349 def test_applyto_extended(self, subject):
350 """Test the acl_extended function"""
351 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
352 basic_acl.applyto(subject)
353 assert not has_extended(subject)
354 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
355 assert enhanced_acl.valid()
356 enhanced_acl.applyto(subject)
357 assert has_extended(subject)
359 @require_extended_check
360 @pytest.mark.parametrize(
361 "gen", [ get_file_and_symlink, get_file_and_fobject ])
362 def test_applyto_extended_mixed(self, testdir, gen):
363 """Test the acl_extended function"""
364 with gen(testdir) as (a, b):
365 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
368 assert not has_extended(item)
369 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
370 assert enhanced_acl.valid()
371 enhanced_acl.applyto(b)
373 assert has_extended(item)
375 @require_extended_check
376 def test_extended_fail(self, testdir):
377 fd, fname = get_file(testdir)
379 with pytest.raises(IOError):
381 with pytest.raises(IOError, match="no-such-file"):
382 has_extended(fname+".no-such-file")
384 @require_extended_check
385 def test_extended_arg_handling(self):
386 with pytest.raises(TypeError):
388 with pytest.raises(TypeError):
389 has_extended(object())
392 def test_equiv_mode(self):
393 """Test the equiv_mode function"""
394 if HAS_ACL_FROM_MODE:
395 for mode in 0o644, 0o755:
396 acl = posix1e.ACL(mode=mode)
397 assert acl.equiv_mode() == mode
398 acl = posix1e.ACL(text="u::rw,g::r,o::r")
399 assert acl.equiv_mode() == 0o644
400 acl = posix1e.ACL(text="u::rx,g::-,o::-")
401 assert acl.equiv_mode() == 0o500
404 def test_to_any_text(self):
405 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
407 acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
408 assert b"user::" in acl.to_any_text()
411 def test_to_any_text_wrong_args(self):
412 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
413 with pytest.raises(TypeError):
414 acl.to_any_text(foo="bar")
418 def test_rich_compare(self):
419 acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
420 acl2 = posix1e.ACL(acl=acl1)
421 acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
424 with pytest.raises(TypeError):
426 with pytest.raises(TypeError):
429 assert not (acl1 == 1)
430 with pytest.raises(TypeError):
434 def test_acl_iterator(self):
435 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
437 assert entry.parent is acl
440 def test_acl_copy_ext(self):
441 a = posix1e.ACL(text=BASIC_ACL_TEXT)
443 c = posix1e.ACL(acl=b)
446 state = a.__getstate__()
447 b.__setstate__(state)
455 def test_delete_default(self, testdir):
456 """Test removing the default ACL"""
457 with get_dir(testdir) as dname:
458 posix1e.delete_default(dname)
460 def test_delete_default_fail(self, testdir):
461 """Test removing the default ACL"""
462 with get_file_name(testdir) as fname:
463 with pytest.raises(IOError, match="no-such-file"):
464 posix1e.delete_default(fname+".no-such-file")
467 def test_delete_default_wrong_arg(self):
468 with pytest.raises(TypeError):
469 posix1e.delete_default(object())
471 def test_reapply(self, testdir):
472 """Test re-applying an ACL"""
473 fd, fname = get_file(testdir)
474 acl1 = posix1e.ACL(fd=fd)
477 with get_dir(testdir) as dname:
478 acl2 = posix1e.ACL(file=fname)
484 class TestModification:
485 """ACL modification tests"""
487 def checkRef(self, obj):
488 """Checks if a given obj has a 'sane' refcount"""
489 if platform.python_implementation() == "PyPy":
491 ref_cnt = sys.getrefcount(obj)
492 # FIXME: hardcoded value for the max ref count... but I've
493 # seen it overflow on bad reference counting, so it's better
495 if ref_cnt < 2 or ref_cnt > 1024:
496 pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
500 """Test str() of an ACL."""
501 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
503 self.checkRef(str_acl)
505 def test_append(self):
506 """Test append a new Entry to the ACL"""
509 e.tag_type = posix1e.ACL_OTHER
510 ignore_ioerror(errno.EINVAL, acl.calc_mask)
512 self.checkRef(str_format)
514 ignore_ioerror(errno.EINVAL, acl.calc_mask)
515 assert not acl.valid()
517 def test_wrong_append(self):
518 """Test append a new Entry to the ACL based on wrong object type"""
520 with pytest.raises(TypeError):
523 def test_entry_creation(self):
525 e = posix1e.Entry(acl)
526 ignore_ioerror(errno.EINVAL, acl.calc_mask)
528 self.checkRef(str_format)
530 def test_entry_failed_creation(self):
531 # Checks for partial initialisation and deletion on error
533 with pytest.raises(TypeError):
534 posix1e.Entry(object())
536 def test_entry_reinitialisations(self):
541 with pytest.raises(ValueError, match="different parent"):
545 def test_entry_reinit_leaks_refcount(self):
548 ref = sys.getrefcount(acl)
550 assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..."
552 def test_delete(self):
553 """Test delete Entry from the ACL"""
556 e.tag_type = posix1e.ACL_OTHER
557 ignore_ioerror(errno.EINVAL, acl.calc_mask)
559 ignore_ioerror(errno.EINVAL, acl.calc_mask)
561 def test_double_delete(self):
562 """Test delete Entry from the ACL"""
563 # This is not entirely valid/correct, since the entry object
564 # itself is invalid after the first deletion, so we're
565 # actually testing deleting an invalid object, not a
566 # non-existing entry...
569 e.tag_type = posix1e.ACL_OTHER
570 ignore_ioerror(errno.EINVAL, acl.calc_mask)
572 ignore_ioerror(errno.EINVAL, acl.calc_mask)
573 with pytest.raises(EnvironmentError):
576 # This currently fails as this deletion seems to be accepted :/
577 @pytest.mark.xfail(reason="Entry deletion is unreliable")
578 def testDeleteInvalidEntry(self):
579 """Test delete foreign Entry from the ACL"""
583 e.tag_type = posix1e.ACL_OTHER
584 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
585 with pytest.raises(EnvironmentError):
588 def test_delete_invalid_object(self):
589 """Test delete a non-Entry from the ACL"""
591 with pytest.raises(TypeError):
592 acl.delete_entry(object())
594 def test_double_entries(self):
595 """Test double entries"""
596 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
598 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
601 e.tag_type = tag_type
603 assert not acl.valid(), ("ACL containing duplicate entries"
604 " should not be valid")
607 def test_multiple_good_entries(self):
608 """Test multiple valid entries"""
609 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
611 for tag_type in (posix1e.ACL_USER,
613 for obj_id in range(5):
615 e.tag_type = tag_type
619 assert acl.valid(), ("ACL should be able to hold multiple"
620 " user/group entries")
622 def test_multiple_bad_entries(self):
623 """Test multiple invalid entries"""
624 for tag_type in (posix1e.ACL_USER,
626 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
629 e1.tag_type = tag_type
633 assert acl.valid(), ("ACL should be able to add a"
636 e2.tag_type = tag_type
639 ignore_ioerror(errno.EINVAL, acl.calc_mask)
640 assert not acl.valid(), ("ACL should not validate when"
641 " containing two duplicate entries")
643 # FreeBSD trips over itself here and can't delete the
644 # entry, even though it still exists.
645 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
650 e1.tag_type = ACL_USER
656 e2.tag_type = ACL_GROUP
663 assert e1.tag_type == e2.tag_type
665 def test_copy_wrong_arg(self):
668 with pytest.raises(TypeError):
671 def test_set_permset(self):
674 e1.tag_type = ACL_USER
680 e2.tag_type = ACL_GROUP
686 assert e2.permset.write
687 assert e2.tag_type == ACL_GROUP
689 def test_set_permset_wrong_arg(self):
692 with pytest.raises(TypeError):
695 def test_permset_creation(self):
700 #self.assertEqual(p1, p2)
702 def test_permset_creation_wrong_arg(self):
703 with pytest.raises(TypeError):
706 def test_permset(self):
707 """Test permissions"""
713 self.checkRef(str_ps)
714 for perm in PERMSETS:
716 txt = PERMSETS[perm][0]
717 self.checkRef(str_ps)
718 assert not ps.test(perm), ("Empty permission set should not"
719 " have permission '%s'" % txt)
721 assert ps.test(perm), ("Permission '%s' should exist"
722 " after addition" % txt)
724 self.checkRef(str_ps)
726 assert not ps.test(perm), ("Permission '%s' should not exist"
727 " after deletion" % txt)
729 def test_permset_via_accessors(self):
730 """Test permissions"""
736 self.checkRef(str_ps)
738 return PERMSETS[perm][1].__get__(ps)
739 def setter(parm, value):
740 return PERMSETS[perm][1].__set__(ps, value)
741 for perm in PERMSETS:
743 self.checkRef(str_ps)
744 txt = PERMSETS[perm][0]
745 assert not getter(perm), ("Empty permission set should not"
746 " have permission '%s'" % txt)
748 assert ps.test(perm), ("Permission '%s' should exist"
749 " after addition" % txt)
750 assert getter(perm), ("Permission '%s' should exist"
751 " after addition" % txt)
753 self.checkRef(str_ps)
755 assert not ps.test(perm), ("Permission '%s' should not exist"
756 " after deletion" % txt)
757 assert not getter(perm), ("Permission '%s' should not exist"
758 " after deletion" % txt)
760 def test_permset_invalid_type(self):
765 with pytest.raises(TypeError):
767 with pytest.raises(TypeError):
769 with pytest.raises(TypeError):
771 with pytest.raises(ValueError):
774 def test_qualifier_values(self):
775 """Tests qualifier correct store/retrieval"""
778 # work around deprecation warnings
779 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
783 if tag == posix1e.ACL_USER:
784 regex = re.compile("user with uid %d" % qualifier)
786 regex = re.compile("group with gid %d" % qualifier)
788 e.qualifier = qualifier
789 except OverflowError:
790 # reached overflow condition, break
792 assert e.qualifier == qualifier
793 assert regex.search(str(e)) is not None
796 def test_qualifier_overflow(self):
797 """Tests qualifier overflow handling"""
800 qualifier = sys.maxsize * 2
801 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
803 with pytest.raises(OverflowError):
804 e.qualifier = qualifier
806 def test_negative_qualifier(self):
807 """Tests negative qualifier handling"""
808 # Note: this presumes that uid_t/gid_t in C are unsigned...
811 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
813 for qualifier in [-10, -5, -1]:
814 with pytest.raises(OverflowError):
815 e.qualifier = qualifier
817 def test_invalid_qualifier(self):
818 """Tests invalid qualifier handling"""
821 with pytest.raises(TypeError):
822 e.qualifier = object()
823 with pytest.raises((TypeError, AttributeError)):
826 def test_qualifier_on_wrong_tag(self):
827 """Tests qualifier setting on wrong tag"""
830 e.tag_type = posix1e.ACL_OTHER
831 with pytest.raises(TypeError):
833 with pytest.raises(TypeError):
836 @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
837 def test_tag_types(self, tag):
838 """Tests tag type correct set/get"""
842 assert e.tag_type == tag
843 # check we can show all tag types without breaking
846 @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
847 @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
848 def test_tag_overwrite(self, src_tag, dst_tag):
849 """Tests tag type correct set/get"""
853 assert e.tag_type == src_tag
856 assert e.tag_type == dst_tag
859 def test_invalid_tags(self):
860 """Tests tag type incorrect set/get"""
863 with pytest.raises(TypeError):
864 e.tag_type = object()
865 e.tag_type = posix1e.ACL_USER_OBJ
866 # For some reason, PyPy raises AttributeError. Strange...
867 with pytest.raises((TypeError, AttributeError)):
870 def test_tag_wrong_overwrite(self):
873 e.tag_type = posix1e.ACL_USER_OBJ
874 tag = max(ALL_TAG_VALUES) + 1
875 with pytest.raises(EnvironmentError):
877 # Check tag is still valid.
878 assert e.tag_type == posix1e.ACL_USER_OBJ
880 if __name__ == "__main__":