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.
75 err = sys.exc_info()[1]
76 if err.errno == errnum:
82 """per-test temp dir based in TEST_DIR"""
83 with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname:
87 fh, fname = tempfile.mkstemp(".test", "xattr-", path)
90 @contextlib.contextmanager
91 def get_file_name(path):
92 fh, fname = get_file(path)
96 @contextlib.contextmanager
97 def get_file_fd(path):
98 fd = get_file(path)[0]
102 @contextlib.contextmanager
103 def get_file_object(path):
104 fd = get_file(path)[0]
105 with os.fdopen(fd) as f:
108 @contextlib.contextmanager
110 yield tempfile.mkdtemp(".test", "xattr-", path)
112 def get_symlink(path, dangling=True):
113 """create a symlink"""
114 fh, fname = get_file(path)
118 sname = fname + ".symlink"
119 os.symlink(fname, sname)
122 @contextlib.contextmanager
123 def get_valid_symlink(path):
124 yield get_symlink(path, dangling=False)[1]
126 @contextlib.contextmanager
127 def get_dangling_symlink(path):
128 yield get_symlink(path, dangling=True)[1]
130 @contextlib.contextmanager
131 def get_file_and_symlink(path):
132 yield get_symlink(path, dangling=False)
134 @contextlib.contextmanager
135 def get_file_and_fobject(path):
136 fh, fname = get_file(path)
137 with os.fdopen(fh) as fo:
140 # Wrappers that build upon existing values
142 def as_wrapper(call, fn, closer=None):
143 @contextlib.contextmanager
145 with call(path) as r:
148 if closer is not None:
153 return as_wrapper(call, lambda r: r.encode())
156 return as_wrapper(call, pathlib.PurePath)
158 def as_iostream(call):
159 opener = lambda f: io.open(f, "r")
160 closer = lambda r: r.close()
161 return as_wrapper(call, opener, closer)
163 NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)",
165 NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'",
168 require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE")
169 require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK")
170 require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY")
171 require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK")
172 require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE")
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
443 def test_delete_default(self, testdir):
444 """Test removing the default ACL"""
445 with get_dir(testdir) as dname:
446 posix1e.delete_default(dname)
448 def test_delete_default_fail(self, testdir):
449 """Test removing the default ACL"""
450 with get_file_name(testdir) as fname:
451 with pytest.raises(IOError, match="no-such-file"):
452 posix1e.delete_default(fname+".no-such-file")
455 def test_delete_default_wrong_arg(self):
456 with pytest.raises(TypeError):
457 posix1e.delete_default(object())
459 def test_reapply(self, testdir):
460 """Test re-applying an ACL"""
461 fd, fname = get_file(testdir)
462 acl1 = posix1e.ACL(fd=fd)
465 with get_dir(testdir) as dname:
466 acl2 = posix1e.ACL(file=fname)
472 class TestModification:
473 """ACL modification tests"""
475 def checkRef(self, obj):
476 """Checks if a given obj has a 'sane' refcount"""
477 if platform.python_implementation() == "PyPy":
479 ref_cnt = sys.getrefcount(obj)
480 # FIXME: hardcoded value for the max ref count... but I've
481 # seen it overflow on bad reference counting, so it's better
483 if ref_cnt < 2 or ref_cnt > 1024:
484 pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
488 """Test str() of an ACL."""
489 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
491 self.checkRef(str_acl)
493 def test_append(self):
494 """Test append a new Entry to the ACL"""
497 e.tag_type = posix1e.ACL_OTHER
498 ignore_ioerror(errno.EINVAL, acl.calc_mask)
500 self.checkRef(str_format)
502 ignore_ioerror(errno.EINVAL, acl.calc_mask)
503 assert not acl.valid()
505 def test_wrong_append(self):
506 """Test append a new Entry to the ACL based on wrong object type"""
508 with pytest.raises(TypeError):
511 def test_entry_creation(self):
513 e = posix1e.Entry(acl)
514 ignore_ioerror(errno.EINVAL, acl.calc_mask)
516 self.checkRef(str_format)
518 def test_entry_failed_creation(self):
519 # Checks for partial initialisation and deletion on error
521 with pytest.raises(TypeError):
522 posix1e.Entry(object())
524 def test_delete(self):
525 """Test delete Entry from the ACL"""
528 e.tag_type = posix1e.ACL_OTHER
529 ignore_ioerror(errno.EINVAL, acl.calc_mask)
531 ignore_ioerror(errno.EINVAL, acl.calc_mask)
533 def test_double_delete(self):
534 """Test delete Entry from the ACL"""
535 # This is not entirely valid/correct, since the entry object
536 # itself is invalid after the first deletion, so we're
537 # actually testing deleting an invalid object, not a
538 # non-existing entry...
541 e.tag_type = posix1e.ACL_OTHER
542 ignore_ioerror(errno.EINVAL, acl.calc_mask)
544 ignore_ioerror(errno.EINVAL, acl.calc_mask)
545 with pytest.raises(EnvironmentError):
548 # This currently fails as this deletion seems to be accepted :/
549 @pytest.mark.xfail(reason="Entry deletion is unreliable")
550 def testDeleteInvalidEntry(self):
551 """Test delete foreign Entry from the ACL"""
555 e.tag_type = posix1e.ACL_OTHER
556 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
557 with pytest.raises(EnvironmentError):
560 def test_delete_invalid_object(self):
561 """Test delete a non-Entry from the ACL"""
563 with pytest.raises(TypeError):
564 acl.delete_entry(object())
566 def test_double_entries(self):
567 """Test double entries"""
568 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
570 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
573 e.tag_type = tag_type
575 assert not acl.valid(), ("ACL containing duplicate entries"
576 " should not be valid")
579 def test_multiple_good_entries(self):
580 """Test multiple valid entries"""
581 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
583 for tag_type in (posix1e.ACL_USER,
585 for obj_id in range(5):
587 e.tag_type = tag_type
591 assert acl.valid(), ("ACL should be able to hold multiple"
592 " user/group entries")
594 def test_multiple_bad_entries(self):
595 """Test multiple invalid entries"""
596 for tag_type in (posix1e.ACL_USER,
598 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
601 e1.tag_type = tag_type
605 assert acl.valid(), ("ACL should be able to add a"
608 e2.tag_type = tag_type
611 ignore_ioerror(errno.EINVAL, acl.calc_mask)
612 assert not acl.valid(), ("ACL should not validate when"
613 " containing two duplicate entries")
615 # FreeBSD trips over itself here and can't delete the
616 # entry, even though it still exists.
617 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
622 e1.tag_type = ACL_USER
628 e2.tag_type = ACL_GROUP
635 assert e1.tag_type == e2.tag_type
637 def test_copy_wrong_arg(self):
640 with pytest.raises(TypeError):
643 def test_set_permset(self):
646 e1.tag_type = ACL_USER
652 e2.tag_type = ACL_GROUP
658 assert e2.permset.write
659 assert e2.tag_type == ACL_GROUP
661 def test_set_permset_wrong_arg(self):
664 with pytest.raises(TypeError):
667 def test_permset_creation(self):
672 #self.assertEqual(p1, p2)
674 def test_permset_creation_wrong_arg(self):
675 with pytest.raises(TypeError):
678 def test_permset(self):
679 """Test permissions"""
685 self.checkRef(str_ps)
686 for perm in PERMSETS:
688 txt = PERMSETS[perm][0]
689 self.checkRef(str_ps)
690 assert not ps.test(perm), ("Empty permission set should not"
691 " have permission '%s'" % txt)
693 assert ps.test(perm), ("Permission '%s' should exist"
694 " after addition" % txt)
696 self.checkRef(str_ps)
698 assert not ps.test(perm), ("Permission '%s' should not exist"
699 " after deletion" % txt)
701 def test_permset_via_accessors(self):
702 """Test permissions"""
708 self.checkRef(str_ps)
710 return PERMSETS[perm][1].__get__(ps)
711 def setter(parm, value):
712 return PERMSETS[perm][1].__set__(ps, value)
713 for perm in PERMSETS:
715 self.checkRef(str_ps)
716 txt = PERMSETS[perm][0]
717 assert not getter(perm), ("Empty permission set should not"
718 " have permission '%s'" % txt)
720 assert ps.test(perm), ("Permission '%s' should exist"
721 " after addition" % txt)
722 assert getter(perm), ("Permission '%s' should exist"
723 " after addition" % txt)
725 self.checkRef(str_ps)
727 assert not ps.test(perm), ("Permission '%s' should not exist"
728 " after deletion" % txt)
729 assert not getter(perm), ("Permission '%s' should not exist"
730 " after deletion" % txt)
732 def test_permset_invalid_type(self):
737 with pytest.raises(TypeError):
739 with pytest.raises(TypeError):
741 with pytest.raises(TypeError):
743 with pytest.raises(ValueError):
746 def test_qualifier_values(self):
747 """Tests qualifier correct store/retrieval"""
750 # work around deprecation warnings
751 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
755 if tag == posix1e.ACL_USER:
756 regex = re.compile("user with uid %d" % qualifier)
758 regex = re.compile("group with gid %d" % qualifier)
760 e.qualifier = qualifier
761 except OverflowError:
762 # reached overflow condition, break
764 assert e.qualifier == qualifier
765 assert regex.search(str(e)) is not None
768 def test_qualifier_overflow(self):
769 """Tests qualifier overflow handling"""
772 qualifier = sys.maxsize * 2
773 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
775 with pytest.raises(OverflowError):
776 e.qualifier = qualifier
778 def test_negative_qualifier(self):
779 """Tests negative qualifier handling"""
780 # Note: this presumes that uid_t/gid_t in C are unsigned...
783 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
785 for qualifier in [-10, -5, -1]:
786 with pytest.raises(OverflowError):
787 e.qualifier = qualifier
789 def test_invalid_qualifier(self):
790 """Tests invalid qualifier handling"""
793 with pytest.raises(TypeError):
794 e.qualifier = object()
795 with pytest.raises((TypeError, AttributeError)):
798 def test_qualifier_on_wrong_tag(self):
799 """Tests qualifier setting on wrong tag"""
802 e.tag_type = posix1e.ACL_OTHER
803 with pytest.raises(TypeError):
805 with pytest.raises(TypeError):
808 @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
809 def test_tag_types(self, tag):
810 """Tests tag type correct set/get"""
814 assert e.tag_type == tag
815 # check we can show all tag types without breaking
818 @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
819 @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
820 def test_tag_overwrite(self, src_tag, dst_tag):
821 """Tests tag type correct set/get"""
825 assert e.tag_type == src_tag
828 assert e.tag_type == dst_tag
831 def test_invalid_tags(self):
832 """Tests tag type incorrect set/get"""
835 with pytest.raises(TypeError):
836 e.tag_type = object()
837 e.tag_type = posix1e.ACL_USER_OBJ
838 # For some reason, PyPy raises AttributeError. Strange...
839 with pytest.raises((TypeError, AttributeError)):
842 def test_tag_wrong_overwrite(self):
845 e.tag_type = posix1e.ACL_USER_OBJ
846 tag = max(ALL_TAG_VALUES) + 1
847 with pytest.raises(EnvironmentError):
849 # Check tag is still valid.
850 assert e.tag_type == posix1e.ACL_USER_OBJ
852 if __name__ == "__main__":