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 # This is to workaround python 2/3 differences at syntactic level
43 # (which can't be worked around via if's)
44 M0500 = 320 # octal 0500
45 M0644 = 420 # octal 0644
46 M0755 = 493 # octal 755
48 # Permset permission information
50 posix1e.ACL_READ: ("read", posix1e.Permset.read),
51 posix1e.ACL_WRITE: ("write", posix1e.Permset.write),
52 posix1e.ACL_EXECUTE: ("execute", posix1e.Permset.execute),
59 posix1e.ACL_GROUP_OBJ,
64 # Check if running under Python 3
65 IS_PY_3K = sys.hexversion >= 0x03000000
67 # Fixtures and helpers
69 def ignore_ioerror(errnum, fn, *args, **kwargs):
70 """Call a function while ignoring some IOErrors.
72 This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL)
73 when doing certain operations on an invalid ACL.
79 err = sys.exc_info()[1]
80 if err.errno == errnum:
85 """Encode a string if needed (under Python 3)"""
93 """per-test temp dir based in TEST_DIR"""
94 with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname:
98 fh, fname = tempfile.mkstemp(".test", "xattr-", path)
101 @contextlib.contextmanager
102 def get_file_name(path):
103 fh, fname = get_file(path)
107 @contextlib.contextmanager
108 def get_file_fd(path):
109 fd = get_file(path)[0]
113 @contextlib.contextmanager
114 def get_file_object(path):
115 fd = get_file(path)[0]
116 with os.fdopen(fd) as f:
119 @contextlib.contextmanager
121 yield tempfile.mkdtemp(".test", "xattr-", path)
123 def get_symlink(path, dangling=True):
124 """create a symlink"""
125 fh, fname = get_file(path)
129 sname = fname + ".symlink"
130 os.symlink(fname, sname)
133 @contextlib.contextmanager
134 def get_valid_symlink(path):
135 yield get_symlink(path, dangling=False)[1]
137 @contextlib.contextmanager
138 def get_dangling_symlink(path):
139 yield get_symlink(path, dangling=True)[1]
141 @contextlib.contextmanager
142 def get_file_and_symlink(path):
143 yield get_symlink(path, dangling=False)
145 @contextlib.contextmanager
146 def get_file_and_fobject(path):
147 fh, fname = get_file(path)
148 with os.fdopen(fh) as fo:
151 # Wrappers that build upon existing values
153 def as_wrapper(call, fn, closer=None):
154 @contextlib.contextmanager
156 with call(path) as r:
159 if closer is not None:
164 return as_wrapper(call, lambda r: r.encode())
167 return as_wrapper(call, pathlib.PurePath)
169 def as_iostream(call):
170 opener = lambda f: io.open(f, "r")
171 closer = lambda r: r.close()
172 return as_wrapper(call, opener, closer)
174 NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)",
176 NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'",
179 require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE")
180 require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK")
181 require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY")
182 require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK")
183 require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE")
186 """Load/create tests"""
187 def test_from_file(self, testdir):
188 """Test loading ACLs from a file"""
189 _, fname = get_file(testdir)
190 acl1 = posix1e.ACL(file=fname)
193 def test_from_dir(self, testdir):
194 """Test loading ACLs from a directory"""
195 with get_dir(testdir) as dname:
196 acl1 = posix1e.ACL(file=dname)
197 acl2 = posix1e.ACL(filedef=dname)
199 # default ACLs might or might not be valid; missing ones are
200 # not valid, so we don't test acl2 for validity
202 def test_from_fd(self, testdir):
203 """Test loading ACLs from a file descriptor"""
204 fd, _ = get_file(testdir)
205 acl1 = posix1e.ACL(fd=fd)
208 def test_from_empty_invalid(self):
209 """Test creating an empty ACL"""
211 assert not acl1.valid()
213 def test_from_text(self):
214 """Test creating an ACL from text"""
215 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
218 def test_from_acl(self):
219 """Test creating an ACL from an existing ACL"""
221 acl2 = posix1e.ACL(acl=acl1)
224 def test_invalid_creation_params(self, testdir):
225 """Test that creating an ACL from multiple objects fails"""
226 fd, _ = get_file(testdir)
227 with pytest.raises(ValueError):
228 posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd)
230 def test_invalid_value_creation(self):
231 """Test that creating an ACL from wrong specification fails"""
232 with pytest.raises(EnvironmentError):
233 posix1e.ACL(text="foobar")
234 with pytest.raises(TypeError):
235 posix1e.ACL(foo="bar")
237 def test_double_init(self):
238 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
240 acl1.__init__(text=BASIC_ACL_TEXT)
243 class TestAclExtensions:
244 """ACL extensions checks"""
246 @require_acl_from_mode
247 def test_from_mode(self):
248 """Test loading ACLs from an octal mode"""
249 acl1 = posix1e.ACL(mode=M0644)
253 def test_acl_check(self):
254 """Test the acl_check method"""
255 acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
256 assert not acl1.check()
260 @require_extended_check
261 def test_extended(self, testdir):
262 """Test the acl_extended function"""
263 fd, fname = get_file(testdir)
264 basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
265 basic_acl.applyto(fd)
266 for item in fd, fname:
267 assert not has_extended(item)
268 enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
269 assert enhanced_acl.valid()
270 enhanced_acl.applyto(fd)
271 for item in fd, fname:
272 assert has_extended(item)
274 @require_extended_check
275 def test_extended_arg_handling(self):
276 with pytest.raises(TypeError):
278 with pytest.raises(TypeError):
279 has_extended(object())
282 def test_equiv_mode(self):
283 """Test the equiv_mode function"""
284 if HAS_ACL_FROM_MODE:
285 for mode in M0644, M0755:
286 acl = posix1e.ACL(mode=mode)
287 assert acl.equiv_mode() == mode
288 acl = posix1e.ACL(text="u::rw,g::r,o::r")
289 assert acl.equiv_mode() == 0o644
290 acl = posix1e.ACL(text="u::rx,g::-,o::-")
291 assert acl.equiv_mode() == 0o500
294 def test_to_any_text(self):
295 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
296 assert encode("u::") in \
297 acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
298 assert encode("user::") in acl.to_any_text()
301 def test_to_any_text_wrong_args(self):
302 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
303 with pytest.raises(TypeError):
304 acl.to_any_text(foo="bar")
308 def test_rich_compare(self):
309 acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
310 acl2 = posix1e.ACL(acl=acl1)
311 acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
314 with pytest.raises(TypeError):
316 with pytest.raises(TypeError):
319 assert not (acl1 == 1)
320 with pytest.raises(TypeError):
323 @pytest.mark.skipif(not hasattr(posix1e.ACL, "__cmp__"), reason="__cmp__ is missing")
327 with pytest.raises(TypeError):
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())
339 def test_acl_iterator(self):
340 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
342 assert entry.parent is acl
348 def test_delete_default(self, testdir):
349 """Test removing the default ACL"""
350 with get_dir(testdir) as dname:
351 posix1e.delete_default(dname)
354 def test_delete_default_wrong_arg(self):
355 with pytest.raises(TypeError):
356 posix1e.delete_default(object())
358 def test_reapply(self, testdir):
359 """Test re-applying an ACL"""
360 fd, fname = get_file(testdir)
361 acl1 = posix1e.ACL(fd=fd)
364 with get_dir(testdir) as dname:
365 acl2 = posix1e.ACL(file=fname)
371 class TestModification:
372 """ACL modification tests"""
374 def checkRef(self, obj):
375 """Checks if a given obj has a 'sane' refcount"""
376 if platform.python_implementation() == "PyPy":
378 ref_cnt = sys.getrefcount(obj)
379 # FIXME: hardcoded value for the max ref count... but I've
380 # seen it overflow on bad reference counting, so it's better
382 if ref_cnt < 2 or ref_cnt > 1024:
383 pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
387 """Test str() of an ACL."""
388 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
390 self.checkRef(str_acl)
392 def test_append(self):
393 """Test append a new Entry to the ACL"""
396 e.tag_type = posix1e.ACL_OTHER
397 ignore_ioerror(errno.EINVAL, acl.calc_mask)
399 self.checkRef(str_format)
401 ignore_ioerror(errno.EINVAL, acl.calc_mask)
402 assert not acl.valid()
404 def test_wrong_append(self):
405 """Test append a new Entry to the ACL based on wrong object type"""
407 with pytest.raises(TypeError):
410 def test_entry_creation(self):
412 e = posix1e.Entry(acl)
413 ignore_ioerror(errno.EINVAL, acl.calc_mask)
415 self.checkRef(str_format)
417 def test_entry_failed_creation(self):
418 # Checks for partial initialisation and deletion on error
420 with pytest.raises(TypeError):
421 posix1e.Entry(object())
423 def test_delete(self):
424 """Test delete Entry from the ACL"""
427 e.tag_type = posix1e.ACL_OTHER
428 ignore_ioerror(errno.EINVAL, acl.calc_mask)
430 ignore_ioerror(errno.EINVAL, acl.calc_mask)
432 def test_double_delete(self):
433 """Test delete Entry from the ACL"""
434 # This is not entirely valid/correct, since the entry object
435 # itself is invalid after the first deletion, so we're
436 # actually testing deleting an invalid object, not a
437 # non-existing entry...
440 e.tag_type = posix1e.ACL_OTHER
441 ignore_ioerror(errno.EINVAL, acl.calc_mask)
443 ignore_ioerror(errno.EINVAL, acl.calc_mask)
444 with pytest.raises(EnvironmentError):
447 # This currently fails as this deletion seems to be accepted :/
448 @pytest.mark.xfail(reason="Entry deletion is unreliable")
449 def testDeleteInvalidEntry(self):
450 """Test delete foreign Entry from the ACL"""
454 e.tag_type = posix1e.ACL_OTHER
455 ignore_ioerror(errno.EINVAL, acl1.calc_mask)
456 with pytest.raises(EnvironmentError):
459 def test_delete_invalid_object(self):
460 """Test delete a non-Entry from the ACL"""
462 with pytest.raises(TypeError):
463 acl.delete_entry(object())
465 def test_double_entries(self):
466 """Test double entries"""
467 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
469 for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
472 e.tag_type = tag_type
474 assert not acl.valid(), ("ACL containing duplicate entries"
475 " should not be valid")
478 def test_multiple_good_entries(self):
479 """Test multiple valid entries"""
480 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
482 for tag_type in (posix1e.ACL_USER,
484 for obj_id in range(5):
486 e.tag_type = tag_type
490 assert acl.valid(), ("ACL should be able to hold multiple"
491 " user/group entries")
493 def test_multiple_bad_entries(self):
494 """Test multiple invalid entries"""
495 for tag_type in (posix1e.ACL_USER,
497 acl = posix1e.ACL(text=BASIC_ACL_TEXT)
500 e1.tag_type = tag_type
504 assert acl.valid(), ("ACL should be able to add a"
507 e2.tag_type = tag_type
510 ignore_ioerror(errno.EINVAL, acl.calc_mask)
511 assert not acl.valid(), ("ACL should not validate when"
512 " containing two duplicate entries")
514 # FreeBSD trips over itself here and can't delete the
515 # entry, even though it still exists.
516 ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
521 e1.tag_type = ACL_USER
527 e2.tag_type = ACL_GROUP
534 assert e1.tag_type == e2.tag_type
536 def test_copy_wrong_arg(self):
539 with pytest.raises(TypeError):
542 def test_set_permset(self):
545 e1.tag_type = ACL_USER
551 e2.tag_type = ACL_GROUP
557 assert e2.permset.write
558 assert e2.tag_type == ACL_GROUP
560 def test_set_permset_wrong_arg(self):
563 with pytest.raises(TypeError):
566 def test_permset_creation(self):
571 #self.assertEqual(p1, p2)
573 def test_permset_creation_wrong_arg(self):
574 with pytest.raises(TypeError):
577 def test_permset(self):
578 """Test permissions"""
584 self.checkRef(str_ps)
585 for perm in PERMSETS:
587 txt = PERMSETS[perm][0]
588 self.checkRef(str_ps)
589 assert not ps.test(perm), ("Empty permission set should not"
590 " have permission '%s'" % txt)
592 assert ps.test(perm), ("Permission '%s' should exist"
593 " after addition" % txt)
595 self.checkRef(str_ps)
597 assert not ps.test(perm), ("Permission '%s' should not exist"
598 " after deletion" % txt)
600 def test_permset_via_accessors(self):
601 """Test permissions"""
607 self.checkRef(str_ps)
609 return PERMSETS[perm][1].__get__(ps)
610 def setter(parm, value):
611 return PERMSETS[perm][1].__set__(ps, value)
612 for perm in PERMSETS:
614 self.checkRef(str_ps)
615 txt = PERMSETS[perm][0]
616 assert not getter(perm), ("Empty permission set should not"
617 " have permission '%s'" % txt)
619 assert ps.test(perm), ("Permission '%s' should exist"
620 " after addition" % txt)
621 assert getter(perm), ("Permission '%s' should exist"
622 " after addition" % txt)
624 self.checkRef(str_ps)
626 assert not ps.test(perm), ("Permission '%s' should not exist"
627 " after deletion" % txt)
628 assert not getter(perm), ("Permission '%s' should not exist"
629 " after deletion" % txt)
631 def test_permset_invalid_type(self):
636 with pytest.raises(TypeError):
638 with pytest.raises(TypeError):
640 with pytest.raises(TypeError):
642 with pytest.raises(ValueError):
645 def test_qualifier_values(self):
646 """Tests qualifier correct store/retrieval"""
649 # work around deprecation warnings
650 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
654 if tag == posix1e.ACL_USER:
655 regex = re.compile("user with uid %d" % qualifier)
657 regex = re.compile("group with gid %d" % qualifier)
659 e.qualifier = qualifier
660 except OverflowError:
661 # reached overflow condition, break
663 assert e.qualifier == qualifier
664 assert regex.search(str(e)) is not None
667 def test_qualifier_overflow(self):
668 """Tests qualifier overflow handling"""
671 qualifier = sys.maxsize * 2
672 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
674 with pytest.raises(OverflowError):
675 e.qualifier = qualifier
677 def test_negative_qualifier(self):
678 """Tests negative qualifier handling"""
679 # Note: this presumes that uid_t/gid_t in C are unsigned...
682 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
684 for qualifier in [-10, -5, -1]:
685 with pytest.raises(OverflowError):
686 e.qualifier = qualifier
688 def test_invalid_qualifier(self):
689 """Tests invalid qualifier handling"""
692 with pytest.raises(TypeError):
693 e.qualifier = object()
694 with pytest.raises((TypeError, AttributeError)):
697 def test_qualifier_on_wrong_tag(self):
698 """Tests qualifier setting on wrong tag"""
701 e.tag_type = posix1e.ACL_OTHER
702 with pytest.raises(TypeError):
704 with pytest.raises(TypeError):
707 @pytest.mark.parametrize("tag", ALL_TAG_TYPES)
708 def test_tag_types(self, tag):
709 """Tests tag type correct set/get"""
713 assert e.tag_type == tag
714 # check we can show all tag types without breaking
717 def test_invalid_tags(self):
718 """Tests tag type incorrect set/get"""
721 with pytest.raises(TypeError):
722 e.tag_type = object()
723 e.tag_type = posix1e.ACL_USER_OBJ
724 # For some reason, PyPy raises AttributeError. Strange...
725 with pytest.raises((TypeError, AttributeError)):
728 e.tag_type = posix1e.ACL_USER_OBJ
729 tag = max(ALL_TAG_TYPES) + 1
730 with pytest.raises(EnvironmentError):
732 # Check tag is still valid.
733 assert e.tag_type == posix1e.ACL_USER_OBJ
735 if __name__ == "__main__":