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")
173 # Note: ACLs are valid only for files/directories, not symbolic links
174 # themselves, so we only create valid symlinks.
177 as_bytes(get_file_name),
178 pytest.param(as_fspath(get_file_name),
179 marks=[NOT_BEFORE_36, NOT_PYPY]),
182 pytest.param(as_fspath(get_dir),
183 marks=[NOT_BEFORE_36, NOT_PYPY]),
185 as_bytes(get_valid_symlink),
186 pytest.param(as_fspath(get_valid_symlink),
187 marks=[NOT_BEFORE_36, NOT_PYPY]),
198 "file via symlink (bytes)",
199 "file via symlink (path)",
205 as_iostream(get_file_name),
214 ALL_P = FILE_P + FD_P
215 ALL_D = FILE_D + FD_D
217 @pytest.fixture(params=FILE_P, ids=FILE_D)
218 def file_subject(testdir, request):
219 with request.param(testdir) as value:
222 @pytest.fixture(params=FD_P, ids=FD_D)
223 def fd_subject(testdir, request):
224 with request.param(testdir) as value:
227 @pytest.fixture(params=ALL_P, ids=ALL_D)
228 def subject(testdir, request):
229 with request.param(testdir) as value:
234 """Load/create tests"""
235 def test_from_file(self, testdir):
236 """Test loading ACLs from a file"""
237 _, fname = get_file(testdir)
238 acl1 = posix1e.ACL(file=fname)
241 def test_from_dir(self, testdir):
242 """Test loading ACLs from a directory"""
243 with get_dir(testdir) as dname:
244 acl1 = posix1e.ACL(file=dname)
245 acl2 = posix1e.ACL(filedef=dname)
247 # default ACLs might or might not be valid; missing ones are
248 # not valid, so we don't test acl2 for validity
250 def test_from_fd(self, testdir):
251 """Test loading ACLs from a file descriptor"""
252 fd, _ = get_file(testdir)
253 acl1 = posix1e.ACL(fd=fd)
256 def test_from_nonexisting(self, testdir):
257 _, fname = get_file(testdir)
258 with pytest.raises(IOError):
259 posix1e.ACL(file="fname"+".no-such-file")
261 def test_from_invalid_fd(self, testdir):
262 fd, _ = get_file(testdir)
264 with pytest.raises(IOError):
267 def test_from_empty_invalid(self):
268 """Test creating an empty ACL"""
270 assert not acl1.valid()
272 def test_from_text(self):
273 """Test creating an ACL from text"""
274 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
277 def test_from_acl(self):
278 """Test creating an ACL from an existing ACL"""
280 acl2 = posix1e.ACL(acl=acl1)
283 def test_invalid_creation_params(self, testdir):
284 """Test that creating an ACL from multiple objects fails"""
285 fd, _ = get_file(testdir)
286 with pytest.raises(ValueError):
287 posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd)
289 def test_invalid_value_creation(self):
290 """Test that creating an ACL from wrong specification fails"""
291 with pytest.raises(EnvironmentError):
292 posix1e.ACL(text="foobar")
293 with pytest.raises(TypeError):
294 posix1e.ACL(foo="bar")
296 def test_double_init(self):
297 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
299 acl1.__init__(text=BASIC_ACL_TEXT)
302 class TestAclExtensions:
303 """ACL extensions checks"""
305 @require_acl_from_mode
306 def test_from_mode(self):
307 """Test loading ACLs from an octal mode"""
308 acl1 = posix1e.ACL(mode=0o644)
312 def test_acl_check(self):
313 """Test the acl_check method"""
314 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
315 assert not acl1.check()
319 def test_applyto(self, subject):
320 """Test the apply_to function"""
321 # TODO: add read/compare with before, once ACL can be init'ed
323 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
324 basic_acl.applyto(subject)
325 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
326 assert enhanced_acl.valid()
327 enhanced_acl.applyto(subject)
329 def test_apply_to_with_wrong_object(self):
330 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
332 with pytest.raises(TypeError):
333 acl1.applyto(object())
334 with pytest.raises(TypeError):
335 acl1.applyto(object(), object())
337 def test_apply_to_fail(self, testdir):
338 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
340 fd, fname = get_file(testdir)
342 with pytest.raises(IOError):
344 with pytest.raises(IOError, match="no-such-file"):
345 acl1.applyto(fname+".no-such-file")
347 @require_extended_check
348 def test_applyto_extended(self, subject):
349 """Test the acl_extended function"""
350 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
351 basic_acl.applyto(subject)
352 assert not has_extended(subject)
353 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
354 assert enhanced_acl.valid()
355 enhanced_acl.applyto(subject)
356 assert has_extended(subject)
358 @require_extended_check
359 @pytest.mark.parametrize(
360 "gen", [ get_file_and_symlink, get_file_and_fobject ])
361 def test_applyto_extended_mixed(self, testdir, gen):
362 """Test the acl_extended function"""
363 with gen(testdir) as (a, b):
364 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
367 assert not has_extended(item)
368 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
369 assert enhanced_acl.valid()
370 enhanced_acl.applyto(b)
372 assert has_extended(item)
374 @require_extended_check
375 def test_extended_fail(self, testdir):
376 fd, fname = get_file(testdir)
378 with pytest.raises(IOError):
380 with pytest.raises(IOError, match="no-such-file"):
381 has_extended(fname+".no-such-file")
383 @require_extended_check
384 def test_extended_arg_handling(self):
385 with pytest.raises(TypeError):
387 with pytest.raises(TypeError):
388 has_extended(object())
391 def test_equiv_mode(self):
392 """Test the equiv_mode function"""
393 if HAS_ACL_FROM_MODE:
394 for mode in 0o644, 0o755:
395 acl = posix1e.ACL(mode=mode)
396 assert acl.equiv_mode() == mode
397 acl = posix1e.ACL(text="u::rw,g::r,o::r")
398 assert acl.equiv_mode() == 0o644
399 acl = posix1e.ACL(text="u::rx,g::-,o::-")
400 assert acl.equiv_mode() == 0o500
403 def test_to_any_text(self):
404 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
406 acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
407 assert b"user::" in acl.to_any_text()
410 def test_to_any_text_wrong_args(self):
411 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
412 with pytest.raises(TypeError):
413 acl.to_any_text(foo="bar")
417 def test_rich_compare(self):
418 acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
419 acl2 = posix1e.ACL(acl=acl1)
420 acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
423 with pytest.raises(TypeError):
425 with pytest.raises(TypeError):
428 assert not (acl1 == 1)
429 with pytest.raises(TypeError):
433 def test_acl_iterator(self):
434 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
436 assert entry.parent is acl
442 def test_delete_default(self, testdir):
443 """Test removing the default ACL"""
444 with get_dir(testdir) as dname:
445 posix1e.delete_default(dname)
447 def test_delete_default_fail(self, testdir):
448 """Test removing the default ACL"""
449 with get_file_name(testdir) as fname:
450 with pytest.raises(IOError, match="no-such-file"):
451 posix1e.delete_default(fname+".no-such-file")
454 def test_delete_default_wrong_arg(self):
455 with pytest.raises(TypeError):
456 posix1e.delete_default(object())
458 def test_reapply(self, testdir):
459 """Test re-applying an ACL"""
460 fd, fname = get_file(testdir)
461 acl1 = posix1e.ACL(fd=fd)
464 with get_dir(testdir) as dname:
465 acl2 = posix1e.ACL(file=fname)
471 class TestModification:
472 """ACL modification tests"""
474 def checkRef(self, obj):
475 """Checks if a given obj has a 'sane' refcount"""
476 if platform.python_implementation() == "PyPy":
478 ref_cnt = sys.getrefcount(obj)
479 # FIXME: hardcoded value for the max ref count... but I've
480 # seen it overflow on bad reference counting, so it's better
482 if ref_cnt < 2 or ref_cnt > 1024:
483 pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
487 """Test str() of an ACL."""
488 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
490 self.checkRef(str_acl)
492 def test_append(self):
493 """Test append a new Entry to the ACL"""
496 e.tag_type = posix1e.ACL_OTHER
497 ignore_ioerror(errno.EINVAL, acl.calc_mask)
499 self.checkRef(str_format)
501 ignore_ioerror(errno.EINVAL, acl.calc_mask)
502 assert not acl.valid()
504 def test_wrong_append(self):
505 """Test append a new Entry to the ACL based on wrong object type"""
507 with pytest.raises(TypeError):
510 def test_entry_creation(self):
512 e = posix1e.Entry(acl)
513 ignore_ioerror(errno.EINVAL, acl.calc_mask)
515 self.checkRef(str_format)
517 def test_entry_failed_creation(self):
518 # Checks for partial initialisation and deletion on error
520 with pytest.raises(TypeError):
521 posix1e.Entry(object())
523 def test_entry_reinitialisations(self):
528 with pytest.raises(ValueError, match="different parent"):
532 def test_entry_reinit_leaks_refcount(self):
535 ref = sys.getrefcount(acl)
537 assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..."
539 def test_delete(self):
540 """Test delete Entry from the ACL"""
543 e.tag_type = posix1e.ACL_OTHER
544 ignore_ioerror(errno.EINVAL, acl.calc_mask)
546 ignore_ioerror(errno.EINVAL, acl.calc_mask)
548 def test_double_delete(self):
549 """Test delete Entry from the ACL"""
550 # This is not entirely valid/correct, since the entry object
551 # itself is invalid after the first deletion, so we're
552 # actually testing deleting an invalid object, not a
553 # non-existing entry...
556 e.tag_type = posix1e.ACL_OTHER
557 ignore_ioerror(errno.EINVAL, acl.calc_mask)
559 ignore_ioerror(errno.EINVAL, acl.calc_mask)
560 with pytest.raises(EnvironmentError):
563 # This currently fails as this deletion seems to be accepted :/
564 @pytest.mark.xfail(reason="Entry deletion is unreliable")
565 def testDeleteInvalidEntry(self):
566 """Test delete foreign Entry from the ACL"""
570 e.tag_type = posix1e.ACL_OTHER
571 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
572 with pytest.raises(EnvironmentError):
575 def test_delete_invalid_object(self):
576 """Test delete a non-Entry from the ACL"""
578 with pytest.raises(TypeError):
579 acl.delete_entry(object())
581 def test_double_entries(self):
582 """Test double entries"""
583 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
585 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
588 e.tag_type = tag_type
590 assert not acl.valid(), ("ACL containing duplicate entries"
591 " should not be valid")
594 def test_multiple_good_entries(self):
595 """Test multiple valid entries"""
596 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
598 for tag_type in (posix1e.ACL_USER,
600 for obj_id in range(5):
602 e.tag_type = tag_type
606 assert acl.valid(), ("ACL should be able to hold multiple"
607 " user/group entries")
609 def test_multiple_bad_entries(self):
610 """Test multiple invalid entries"""
611 for tag_type in (posix1e.ACL_USER,
613 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
616 e1.tag_type = tag_type
620 assert acl.valid(), ("ACL should be able to add a"
623 e2.tag_type = tag_type
626 ignore_ioerror(errno.EINVAL, acl.calc_mask)
627 assert not acl.valid(), ("ACL should not validate when"
628 " containing two duplicate entries")
630 # FreeBSD trips over itself here and can't delete the
631 # entry, even though it still exists.
632 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
637 e1.tag_type = ACL_USER
643 e2.tag_type = ACL_GROUP
650 assert e1.tag_type == e2.tag_type
652 def test_copy_wrong_arg(self):
655 with pytest.raises(TypeError):
658 def test_set_permset(self):
661 e1.tag_type = ACL_USER
667 e2.tag_type = ACL_GROUP
673 assert e2.permset.write
674 assert e2.tag_type == ACL_GROUP
676 def test_set_permset_wrong_arg(self):
679 with pytest.raises(TypeError):
682 def test_permset_creation(self):
687 #self.assertEqual(p1, p2)
689 def test_permset_creation_wrong_arg(self):
690 with pytest.raises(TypeError):
693 def test_permset(self):
694 """Test permissions"""
700 self.checkRef(str_ps)
701 for perm in PERMSETS:
703 txt = PERMSETS[perm][0]
704 self.checkRef(str_ps)
705 assert not ps.test(perm), ("Empty permission set should not"
706 " have permission '%s'" % txt)
708 assert ps.test(perm), ("Permission '%s' should exist"
709 " after addition" % txt)
711 self.checkRef(str_ps)
713 assert not ps.test(perm), ("Permission '%s' should not exist"
714 " after deletion" % txt)
716 def test_permset_via_accessors(self):
717 """Test permissions"""
723 self.checkRef(str_ps)
725 return PERMSETS[perm][1].__get__(ps)
726 def setter(parm, value):
727 return PERMSETS[perm][1].__set__(ps, value)
728 for perm in PERMSETS:
730 self.checkRef(str_ps)
731 txt = PERMSETS[perm][0]
732 assert not getter(perm), ("Empty permission set should not"
733 " have permission '%s'" % txt)
735 assert ps.test(perm), ("Permission '%s' should exist"
736 " after addition" % txt)
737 assert getter(perm), ("Permission '%s' should exist"
738 " after addition" % txt)
740 self.checkRef(str_ps)
742 assert not ps.test(perm), ("Permission '%s' should not exist"
743 " after deletion" % txt)
744 assert not getter(perm), ("Permission '%s' should not exist"
745 " after deletion" % txt)
747 def test_permset_invalid_type(self):
752 with pytest.raises(TypeError):
754 with pytest.raises(TypeError):
756 with pytest.raises(TypeError):
758 with pytest.raises(ValueError):
761 def test_qualifier_values(self):
762 """Tests qualifier correct store/retrieval"""
765 # work around deprecation warnings
766 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
770 if tag == posix1e.ACL_USER:
771 regex = re.compile("user with uid %d" % qualifier)
773 regex = re.compile("group with gid %d" % qualifier)
775 e.qualifier = qualifier
776 except OverflowError:
777 # reached overflow condition, break
779 assert e.qualifier == qualifier
780 assert regex.search(str(e)) is not None
783 def test_qualifier_overflow(self):
784 """Tests qualifier overflow handling"""
787 qualifier = sys.maxsize * 2
788 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
790 with pytest.raises(OverflowError):
791 e.qualifier = qualifier
793 def test_negative_qualifier(self):
794 """Tests negative qualifier handling"""
795 # Note: this presumes that uid_t/gid_t in C are unsigned...
798 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
800 for qualifier in [-10, -5, -1]:
801 with pytest.raises(OverflowError):
802 e.qualifier = qualifier
804 def test_invalid_qualifier(self):
805 """Tests invalid qualifier handling"""
808 with pytest.raises(TypeError):
809 e.qualifier = object()
810 with pytest.raises((TypeError, AttributeError)):
813 def test_qualifier_on_wrong_tag(self):
814 """Tests qualifier setting on wrong tag"""
817 e.tag_type = posix1e.ACL_OTHER
818 with pytest.raises(TypeError):
820 with pytest.raises(TypeError):
823 @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
824 def test_tag_types(self, tag):
825 """Tests tag type correct set/get"""
829 assert e.tag_type == tag
830 # check we can show all tag types without breaking
833 @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
834 @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
835 def test_tag_overwrite(self, src_tag, dst_tag):
836 """Tests tag type correct set/get"""
840 assert e.tag_type == src_tag
843 assert e.tag_type == dst_tag
846 def test_invalid_tags(self):
847 """Tests tag type incorrect set/get"""
850 with pytest.raises(TypeError):
851 e.tag_type = object()
852 e.tag_type = posix1e.ACL_USER_OBJ
853 # For some reason, PyPy raises AttributeError. Strange...
854 with pytest.raises((TypeError, AttributeError)):
857 def test_tag_wrong_overwrite(self):
860 e.tag_type = posix1e.ACL_USER_OBJ
861 tag = max(ALL_TAG_VALUES) + 1
862 with pytest.raises(EnvironmentError):
864 # Check tag is still valid.
865 assert e.tag_type == posix1e.ACL_USER_OBJ
867 if __name__ == "__main__":