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 # Permset permission information
44 posix1e.ACL_READ: ("read", posix1e.Permset.read),
45 posix1e.ACL_WRITE: ("write", posix1e.Permset.write),
46 posix1e.ACL_EXECUTE: ("execute", posix1e.Permset.execute),
50 (posix1e.ACL_USER, "user"),
51 (posix1e.ACL_GROUP, "group"),
52 (posix1e.ACL_USER_OBJ, "user object"),
53 (posix1e.ACL_GROUP_OBJ, "group object"),
54 (posix1e.ACL_MASK, "mask"),
55 (posix1e.ACL_OTHER, "other"),
58 ALL_TAG_VALUES = [i[0] for i in ALL_TAGS]
59 ALL_TAG_DESCS = [i[1] for i in ALL_TAGS]
61 # Fixtures and helpers
63 def ignore_ioerror(errnum, fn, *args, **kwargs):
64 """Call a function while ignoring some IOErrors.
66 This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL)
67 when doing certain operations on an invalid ACL.
73 err = sys.exc_info()[1]
74 if err.errno == errnum:
80 """per-test temp dir based in TEST_DIR"""
81 with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname:
85 fh, fname = tempfile.mkstemp(".test", "xattr-", path)
88 @contextlib.contextmanager
89 def get_file_name(path):
90 fh, fname = get_file(path)
94 @contextlib.contextmanager
95 def get_file_fd(path):
96 fd = get_file(path)[0]
100 @contextlib.contextmanager
101 def get_file_object(path):
102 fd = get_file(path)[0]
103 with os.fdopen(fd) as f:
106 @contextlib.contextmanager
108 yield tempfile.mkdtemp(".test", "xattr-", path)
110 def get_symlink(path, dangling=True):
111 """create a symlink"""
112 fh, fname = get_file(path)
116 sname = fname + ".symlink"
117 os.symlink(fname, sname)
120 @contextlib.contextmanager
121 def get_valid_symlink(path):
122 yield get_symlink(path, dangling=False)[1]
124 @contextlib.contextmanager
125 def get_dangling_symlink(path):
126 yield get_symlink(path, dangling=True)[1]
128 @contextlib.contextmanager
129 def get_file_and_symlink(path):
130 yield get_symlink(path, dangling=False)
132 @contextlib.contextmanager
133 def get_file_and_fobject(path):
134 fh, fname = get_file(path)
135 with os.fdopen(fh) as fo:
138 # Wrappers that build upon existing values
140 def as_wrapper(call, fn, closer=None):
141 @contextlib.contextmanager
143 with call(path) as r:
146 if closer is not None:
151 return as_wrapper(call, lambda r: r.encode())
154 return as_wrapper(call, pathlib.PurePath)
156 def as_iostream(call):
157 opener = lambda f: io.open(f, "r")
158 closer = lambda r: r.close()
159 return as_wrapper(call, opener, closer)
161 NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)",
163 NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'",
166 require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE")
167 require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK")
168 require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY")
169 require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK")
170 require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE")
173 """Load/create tests"""
174 def test_from_file(self, testdir):
175 """Test loading ACLs from a file"""
176 _, fname = get_file(testdir)
177 acl1 = posix1e.ACL(file=fname)
180 def test_from_dir(self, testdir):
181 """Test loading ACLs from a directory"""
182 with get_dir(testdir) as dname:
183 acl1 = posix1e.ACL(file=dname)
184 acl2 = posix1e.ACL(filedef=dname)
186 # default ACLs might or might not be valid; missing ones are
187 # not valid, so we don't test acl2 for validity
189 def test_from_fd(self, testdir):
190 """Test loading ACLs from a file descriptor"""
191 fd, _ = get_file(testdir)
192 acl1 = posix1e.ACL(fd=fd)
195 def test_from_empty_invalid(self):
196 """Test creating an empty ACL"""
198 assert not acl1.valid()
200 def test_from_text(self):
201 """Test creating an ACL from text"""
202 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
205 def test_from_acl(self):
206 """Test creating an ACL from an existing ACL"""
208 acl2 = posix1e.ACL(acl=acl1)
211 def test_invalid_creation_params(self, testdir):
212 """Test that creating an ACL from multiple objects fails"""
213 fd, _ = get_file(testdir)
214 with pytest.raises(ValueError):
215 posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd)
217 def test_invalid_value_creation(self):
218 """Test that creating an ACL from wrong specification fails"""
219 with pytest.raises(EnvironmentError):
220 posix1e.ACL(text="foobar")
221 with pytest.raises(TypeError):
222 posix1e.ACL(foo="bar")
224 def test_double_init(self):
225 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
227 acl1.__init__(text=BASIC_ACL_TEXT)
230 class TestAclExtensions:
231 """ACL extensions checks"""
233 @require_acl_from_mode
234 def test_from_mode(self):
235 """Test loading ACLs from an octal mode"""
236 acl1 = posix1e.ACL(mode=0o644)
240 def test_acl_check(self):
241 """Test the acl_check method"""
242 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
243 assert not acl1.check()
247 @require_extended_check
248 def test_extended(self, testdir):
249 """Test the acl_extended function"""
250 fd, fname = get_file(testdir)
251 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
252 basic_acl.applyto(fd)
253 for item in fd, fname:
254 assert not has_extended(item)
255 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
256 assert enhanced_acl.valid()
257 enhanced_acl.applyto(fd)
258 for item in fd, fname:
259 assert has_extended(item)
261 @require_extended_check
262 def test_extended_arg_handling(self):
263 with pytest.raises(TypeError):
265 with pytest.raises(TypeError):
266 has_extended(object())
269 def test_equiv_mode(self):
270 """Test the equiv_mode function"""
271 if HAS_ACL_FROM_MODE:
272 for mode in 0o644, 0o755:
273 acl = posix1e.ACL(mode=mode)
274 assert acl.equiv_mode() == mode
275 acl = posix1e.ACL(text="u::rw,g::r,o::r")
276 assert acl.equiv_mode() == 0o644
277 acl = posix1e.ACL(text="u::rx,g::-,o::-")
278 assert acl.equiv_mode() == 0o500
281 def test_to_any_text(self):
282 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
284 acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
285 assert b"user::" in acl.to_any_text()
288 def test_to_any_text_wrong_args(self):
289 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
290 with pytest.raises(TypeError):
291 acl.to_any_text(foo="bar")
295 def test_rich_compare(self):
296 acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
297 acl2 = posix1e.ACL(acl=acl1)
298 acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
301 with pytest.raises(TypeError):
303 with pytest.raises(TypeError):
306 assert not (acl1 == 1)
307 with pytest.raises(TypeError):
310 def test_apply_to_with_wrong_object(self):
311 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
313 with pytest.raises(TypeError):
314 acl1.applyto(object())
315 with pytest.raises(TypeError):
316 acl1.applyto(object(), object())
319 def test_acl_iterator(self):
320 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
322 assert entry.parent is acl
328 def test_delete_default(self, testdir):
329 """Test removing the default ACL"""
330 with get_dir(testdir) as dname:
331 posix1e.delete_default(dname)
334 def test_delete_default_wrong_arg(self):
335 with pytest.raises(TypeError):
336 posix1e.delete_default(object())
338 def test_reapply(self, testdir):
339 """Test re-applying an ACL"""
340 fd, fname = get_file(testdir)
341 acl1 = posix1e.ACL(fd=fd)
344 with get_dir(testdir) as dname:
345 acl2 = posix1e.ACL(file=fname)
351 class TestModification:
352 """ACL modification tests"""
354 def checkRef(self, obj):
355 """Checks if a given obj has a 'sane' refcount"""
356 if platform.python_implementation() == "PyPy":
358 ref_cnt = sys.getrefcount(obj)
359 # FIXME: hardcoded value for the max ref count... but I've
360 # seen it overflow on bad reference counting, so it's better
362 if ref_cnt < 2 or ref_cnt > 1024:
363 pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
367 """Test str() of an ACL."""
368 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
370 self.checkRef(str_acl)
372 def test_append(self):
373 """Test append a new Entry to the ACL"""
376 e.tag_type = posix1e.ACL_OTHER
377 ignore_ioerror(errno.EINVAL, acl.calc_mask)
379 self.checkRef(str_format)
381 ignore_ioerror(errno.EINVAL, acl.calc_mask)
382 assert not acl.valid()
384 def test_wrong_append(self):
385 """Test append a new Entry to the ACL based on wrong object type"""
387 with pytest.raises(TypeError):
390 def test_entry_creation(self):
392 e = posix1e.Entry(acl)
393 ignore_ioerror(errno.EINVAL, acl.calc_mask)
395 self.checkRef(str_format)
397 def test_entry_failed_creation(self):
398 # Checks for partial initialisation and deletion on error
400 with pytest.raises(TypeError):
401 posix1e.Entry(object())
403 def test_delete(self):
404 """Test delete Entry from the ACL"""
407 e.tag_type = posix1e.ACL_OTHER
408 ignore_ioerror(errno.EINVAL, acl.calc_mask)
410 ignore_ioerror(errno.EINVAL, acl.calc_mask)
412 def test_double_delete(self):
413 """Test delete Entry from the ACL"""
414 # This is not entirely valid/correct, since the entry object
415 # itself is invalid after the first deletion, so we're
416 # actually testing deleting an invalid object, not a
417 # non-existing entry...
420 e.tag_type = posix1e.ACL_OTHER
421 ignore_ioerror(errno.EINVAL, acl.calc_mask)
423 ignore_ioerror(errno.EINVAL, acl.calc_mask)
424 with pytest.raises(EnvironmentError):
427 # This currently fails as this deletion seems to be accepted :/
428 @pytest.mark.xfail(reason="Entry deletion is unreliable")
429 def testDeleteInvalidEntry(self):
430 """Test delete foreign Entry from the ACL"""
434 e.tag_type = posix1e.ACL_OTHER
435 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
436 with pytest.raises(EnvironmentError):
439 def test_delete_invalid_object(self):
440 """Test delete a non-Entry from the ACL"""
442 with pytest.raises(TypeError):
443 acl.delete_entry(object())
445 def test_double_entries(self):
446 """Test double entries"""
447 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
449 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
452 e.tag_type = tag_type
454 assert not acl.valid(), ("ACL containing duplicate entries"
455 " should not be valid")
458 def test_multiple_good_entries(self):
459 """Test multiple valid entries"""
460 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
462 for tag_type in (posix1e.ACL_USER,
464 for obj_id in range(5):
466 e.tag_type = tag_type
470 assert acl.valid(), ("ACL should be able to hold multiple"
471 " user/group entries")
473 def test_multiple_bad_entries(self):
474 """Test multiple invalid entries"""
475 for tag_type in (posix1e.ACL_USER,
477 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
480 e1.tag_type = tag_type
484 assert acl.valid(), ("ACL should be able to add a"
487 e2.tag_type = tag_type
490 ignore_ioerror(errno.EINVAL, acl.calc_mask)
491 assert not acl.valid(), ("ACL should not validate when"
492 " containing two duplicate entries")
494 # FreeBSD trips over itself here and can't delete the
495 # entry, even though it still exists.
496 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
501 e1.tag_type = ACL_USER
507 e2.tag_type = ACL_GROUP
514 assert e1.tag_type == e2.tag_type
516 def test_copy_wrong_arg(self):
519 with pytest.raises(TypeError):
522 def test_set_permset(self):
525 e1.tag_type = ACL_USER
531 e2.tag_type = ACL_GROUP
537 assert e2.permset.write
538 assert e2.tag_type == ACL_GROUP
540 def test_set_permset_wrong_arg(self):
543 with pytest.raises(TypeError):
546 def test_permset_creation(self):
551 #self.assertEqual(p1, p2)
553 def test_permset_creation_wrong_arg(self):
554 with pytest.raises(TypeError):
557 def test_permset(self):
558 """Test permissions"""
564 self.checkRef(str_ps)
565 for perm in PERMSETS:
567 txt = PERMSETS[perm][0]
568 self.checkRef(str_ps)
569 assert not ps.test(perm), ("Empty permission set should not"
570 " have permission '%s'" % txt)
572 assert ps.test(perm), ("Permission '%s' should exist"
573 " after addition" % txt)
575 self.checkRef(str_ps)
577 assert not ps.test(perm), ("Permission '%s' should not exist"
578 " after deletion" % txt)
580 def test_permset_via_accessors(self):
581 """Test permissions"""
587 self.checkRef(str_ps)
589 return PERMSETS[perm][1].__get__(ps)
590 def setter(parm, value):
591 return PERMSETS[perm][1].__set__(ps, value)
592 for perm in PERMSETS:
594 self.checkRef(str_ps)
595 txt = PERMSETS[perm][0]
596 assert not getter(perm), ("Empty permission set should not"
597 " have permission '%s'" % txt)
599 assert ps.test(perm), ("Permission '%s' should exist"
600 " after addition" % txt)
601 assert getter(perm), ("Permission '%s' should exist"
602 " after addition" % txt)
604 self.checkRef(str_ps)
606 assert not ps.test(perm), ("Permission '%s' should not exist"
607 " after deletion" % txt)
608 assert not getter(perm), ("Permission '%s' should not exist"
609 " after deletion" % txt)
611 def test_permset_invalid_type(self):
616 with pytest.raises(TypeError):
618 with pytest.raises(TypeError):
620 with pytest.raises(TypeError):
622 with pytest.raises(ValueError):
625 def test_qualifier_values(self):
626 """Tests qualifier correct store/retrieval"""
629 # work around deprecation warnings
630 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
634 if tag == posix1e.ACL_USER:
635 regex = re.compile("user with uid %d" % qualifier)
637 regex = re.compile("group with gid %d" % qualifier)
639 e.qualifier = qualifier
640 except OverflowError:
641 # reached overflow condition, break
643 assert e.qualifier == qualifier
644 assert regex.search(str(e)) is not None
647 def test_qualifier_overflow(self):
648 """Tests qualifier overflow handling"""
651 qualifier = sys.maxsize * 2
652 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
654 with pytest.raises(OverflowError):
655 e.qualifier = qualifier
657 def test_negative_qualifier(self):
658 """Tests negative qualifier handling"""
659 # Note: this presumes that uid_t/gid_t in C are unsigned...
662 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
664 for qualifier in [-10, -5, -1]:
665 with pytest.raises(OverflowError):
666 e.qualifier = qualifier
668 def test_invalid_qualifier(self):
669 """Tests invalid qualifier handling"""
672 with pytest.raises(TypeError):
673 e.qualifier = object()
674 with pytest.raises((TypeError, AttributeError)):
677 def test_qualifier_on_wrong_tag(self):
678 """Tests qualifier setting on wrong tag"""
681 e.tag_type = posix1e.ACL_OTHER
682 with pytest.raises(TypeError):
684 with pytest.raises(TypeError):
687 @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
688 def test_tag_types(self, tag):
689 """Tests tag type correct set/get"""
693 assert e.tag_type == tag
694 # check we can show all tag types without breaking
697 @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
698 @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
699 def test_tag_overwrite(self, src_tag, dst_tag):
700 """Tests tag type correct set/get"""
704 assert e.tag_type == src_tag
707 assert e.tag_type == dst_tag
710 def test_invalid_tags(self):
711 """Tests tag type incorrect set/get"""
714 with pytest.raises(TypeError):
715 e.tag_type = object()
716 e.tag_type = posix1e.ACL_USER_OBJ
717 # For some reason, PyPy raises AttributeError. Strange...
718 with pytest.raises((TypeError, AttributeError)):
721 def test_tag_wrong_overwrite(self):
724 e.tag_type = posix1e.ACL_USER_OBJ
725 tag = max(ALL_TAG_VALUES) + 1
726 with pytest.raises(EnvironmentError):
728 # Check tag is still valid.
729 assert e.tag_type == posix1e.ACL_USER_OBJ
731 if __name__ == "__main__":