]> git.k1024.org Git - pylibacl.git/blob - tests/test_acls.py
Update .gitignore file
[pylibacl.git] / tests / test_acls.py
1 #
2 #
3
4 """Unittests for the posix1e module"""
5
6 #  Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop <iustin@k1024.org>
7 #
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.
12 #
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.
17 #
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
21 #  02110-1301  USA
22
23
24 import unittest
25 import os
26 import tempfile
27 import sys
28 import platform
29 import re
30 import errno
31 import operator
32 import pytest  # type: ignore
33 import contextlib
34 import pathlib
35 import io
36
37 import posix1e
38 from posix1e import *
39
40 TEST_DIR = os.environ.get("TEST_DIR", ".")
41
42 BASIC_ACL_TEXT = "u::rw,g::r,o::-"
43 TEXT_0755 = "u::rwx,g::rx,o::rx"
44
45 # Permset permission information
46 PERMSETS = [
47     (ACL_READ, "read", Permset.read),
48     (ACL_WRITE, "write", Permset.write),
49     (ACL_EXECUTE, "execute", Permset.execute),
50 ]
51
52 PERMSETS_IDS = [p[1] for p in PERMSETS]
53
54 ALL_TAGS = [
55   (posix1e.ACL_USER, "user"),
56   (posix1e.ACL_GROUP, "group"),
57   (posix1e.ACL_USER_OBJ, "user object"),
58   (posix1e.ACL_GROUP_OBJ, "group object"),
59   (posix1e.ACL_MASK, "mask"),
60   (posix1e.ACL_OTHER, "other"),
61 ]
62
63 ALL_TAG_VALUES = [i[0] for i in ALL_TAGS]
64 ALL_TAG_DESCS = [i[1] for i in ALL_TAGS]
65
66 # Fixtures and helpers
67
68 def ignore_ioerror(errnum, fn, *args, **kwargs):
69     """Call a function while ignoring some IOErrors.
70
71     This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL)
72     when doing certain operations on an invalid ACL.
73
74     """
75     try:
76         fn(*args, **kwargs)
77     except IOError as err:
78         if err.errno == errnum:
79             return
80         raise
81
82 def assert_acl_eq(a, b):
83     if HAS_ACL_CHECK:
84         assert a == b
85     assert str(a) == str(b)
86
87 @pytest.fixture
88 def testdir():
89     """per-test temp dir based in TEST_DIR"""
90     with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname:
91         yield dname
92
93 def get_file(path):
94     fh, fname = tempfile.mkstemp(".test", "xattr-", path)
95     return fh, fname
96
97 @contextlib.contextmanager
98 def get_file_name(path):
99     fh, fname = get_file(path)
100     os.close(fh)
101     yield fname
102
103 @contextlib.contextmanager
104 def get_file_fd(path):
105     fd = get_file(path)[0]
106     yield fd
107     os.close(fd)
108
109 @contextlib.contextmanager
110 def get_file_object(path):
111     fd = get_file(path)[0]
112     with os.fdopen(fd) as f:
113         yield f
114
115 @contextlib.contextmanager
116 def get_dir(path):
117     yield tempfile.mkdtemp(".test", "xattr-", path)
118
119 def get_symlink(path, dangling=True):
120     """create a symlink"""
121     fh, fname = get_file(path)
122     os.close(fh)
123     if dangling:
124         os.unlink(fname)
125     sname = fname + ".symlink"
126     os.symlink(fname, sname)
127     return fname, sname
128
129 @contextlib.contextmanager
130 def get_valid_symlink(path):
131     yield get_symlink(path, dangling=False)[1]
132
133 @contextlib.contextmanager
134 def get_dangling_symlink(path):
135     yield get_symlink(path, dangling=True)[1]
136
137 @contextlib.contextmanager
138 def get_file_and_symlink(path):
139     yield get_symlink(path, dangling=False)
140
141 @contextlib.contextmanager
142 def get_file_and_fobject(path):
143     fh, fname = get_file(path)
144     with os.fdopen(fh) as fo:
145         yield fname, fo
146
147 # Wrappers that build upon existing values
148
149 def as_wrapper(call, fn, closer=None):
150     @contextlib.contextmanager
151     def f(path):
152         with call(path) as r:
153             val = fn(r)
154             yield val
155             if closer is not None:
156                 closer(val)
157     return f
158
159 def as_bytes(call):
160     return as_wrapper(call, lambda r: r.encode())
161
162 def as_fspath(call):
163     return as_wrapper(call, pathlib.PurePath)
164
165 def as_iostream(call):
166     opener = lambda f: io.open(f, "r")
167     closer = lambda r: r.close()
168     return as_wrapper(call, opener, closer)
169
170 NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)",
171                                   strict=True)
172 NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'",
173                                   strict=False)
174
175 require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE")
176 require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK")
177 require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY")
178 require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK")
179 require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE")
180 require_copy_ext = pytest.mark.skipif("not HAS_COPY_EXT")
181
182 # Note: ACLs are valid only for files/directories, not symbolic links
183 # themselves, so we only create valid symlinks.
184 FILE_P = [
185     get_file_name,
186     as_bytes(get_file_name),
187     pytest.param(as_fspath(get_file_name),
188                  marks=[NOT_BEFORE_36, NOT_PYPY]),
189     get_dir,
190     as_bytes(get_dir),
191     pytest.param(as_fspath(get_dir),
192                  marks=[NOT_BEFORE_36, NOT_PYPY]),
193     get_valid_symlink,
194     as_bytes(get_valid_symlink),
195     pytest.param(as_fspath(get_valid_symlink),
196                  marks=[NOT_BEFORE_36, NOT_PYPY]),
197 ]
198
199 FILE_D = [
200     "file name",
201     "file name (bytes)",
202     "file name (path)",
203     "directory",
204     "directory (bytes)",
205     "directory (path)",
206     "file via symlink",
207     "file via symlink (bytes)",
208     "file via symlink (path)",
209 ]
210
211 FD_P = [
212     get_file_fd,
213     get_file_object,
214     as_iostream(get_file_name),
215 ]
216
217 FD_D = [
218     "file FD",
219     "file object",
220     "file io stream",
221 ]
222
223 DIR_D = [
224     "directory",
225     "directory (bytes)",
226     "directory (path object)",
227 ]
228
229 DIR_P = [
230     get_dir,
231     as_bytes(get_dir),
232     pytest.param(as_fspath(get_dir),
233                  marks=[NOT_BEFORE_36, NOT_PYPY]),
234 ]
235
236 ALL_P = FILE_P + FD_P
237 ALL_D = FILE_D + FD_D
238
239 @pytest.fixture(params=FILE_P, ids=FILE_D)
240 def file_subject(testdir, request):
241     with request.param(testdir) as value:
242         yield value
243
244 @pytest.fixture(params=FD_P, ids=FD_D)
245 def fd_subject(testdir, request):
246     with request.param(testdir) as value:
247         yield value
248
249 @pytest.fixture(params=DIR_P, ids=DIR_D)
250 def dir_subject(testdir, request):
251     with request.param(testdir) as value:
252         yield value
253
254 @pytest.fixture(params=ALL_P, ids=ALL_D)
255 def subject(testdir, request):
256     with request.param(testdir) as value:
257         yield value
258
259
260 class TestLoad:
261     """Load/create tests"""
262     def test_from_file(self, file_subject):
263         """Test loading ACLs from a file/directory"""
264         acl = posix1e.ACL(file=file_subject)
265         assert acl.valid()
266
267     def test_from_dir(self, dir_subject):
268         """Test loading ACLs from a directory"""
269         acl2 = posix1e.ACL(filedef=dir_subject)
270         # default ACLs might or might not be valid; missing ones are
271         # not valid, so we don't test acl2 for validity
272
273     def test_from_fd(self, fd_subject):
274         """Test loading ACLs from a file descriptor"""
275         acl = posix1e.ACL(fd=fd_subject)
276         assert acl.valid()
277
278     def test_from_nonexisting(self, testdir):
279         _, fname = get_file(testdir)
280         with pytest.raises(IOError):
281             posix1e.ACL(file="fname"+".no-such-file")
282         with pytest.raises(IOError):
283             posix1e.ACL(filedef="fname"+".no-such-file")
284
285     def test_from_invalid_fd(self, testdir):
286         fd, _ = get_file(testdir)
287         os.close(fd)
288         with pytest.raises(IOError):
289             posix1e.ACL(fd=fd)
290
291     def test_from_empty_invalid(self):
292         """Test creating an empty ACL"""
293         acl1 = posix1e.ACL()
294         assert not acl1.valid()
295
296     def test_from_text(self):
297         """Test creating an ACL from text"""
298         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
299         assert acl1.valid()
300
301     # This is acl_check, but should actually be have_linux...
302     @require_acl_check
303     def test_from_acl(self):
304         """Test creating an ACL from an existing ACL"""
305         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
306         acl2 = posix1e.ACL(acl=acl1)
307         assert acl1 == acl2
308
309     def test_from_acl_via_str(self):
310         # This is needed for not HAVE_LINUX cases.
311         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
312         acl2 = posix1e.ACL(acl=acl1)
313         assert str(acl1) == str(acl2)
314
315     def test_invalid_creation_params(self, testdir):
316         """Test that creating an ACL from multiple objects fails"""
317         fd, _ = get_file(testdir)
318         with pytest.raises(ValueError):
319           posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd)
320
321     def test_invalid_value_creation(self):
322         """Test that creating an ACL from wrong specification fails"""
323         with pytest.raises(EnvironmentError):
324           posix1e.ACL(text="foobar")
325         with pytest.raises(TypeError):
326           posix1e.ACL(foo="bar")
327
328     def test_uninit(self):
329         """Checks that uninit is actually empty init"""
330         acl = posix1e.ACL.__new__(posix1e.ACL)
331         assert not acl.valid()
332         e = acl.append()
333         e.permset
334         acl.delete_entry(e)
335
336     def test_double_init(self):
337         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
338         assert acl1.valid()
339         acl1.__init__(text=BASIC_ACL_TEXT) # type: ignore
340         assert acl1.valid()
341         acl2 = ACL(text=TEXT_0755)
342         assert acl1 != acl2
343         acl1.__init__(acl=acl2)  # type: ignore
344         assert_acl_eq(acl1, acl2)
345
346     def test_reinit_failure_noop(self):
347         a = posix1e.ACL(text=TEXT_0755)
348         b = posix1e.ACL(acl=a)
349         assert_acl_eq(a, b)
350         with pytest.raises(IOError):
351             a.__init__(text='foobar')
352         assert_acl_eq(a, b)
353
354     @pytest.mark.xfail(reason="Unreliable test, re-init doesn't always invalidate children")
355     def test_double_init_breaks_children(self):
356         acl = posix1e.ACL()
357         e = acl.append()
358         e.permset.write = True
359         acl.__init__() # type: ignore
360         with pytest.raises(EnvironmentError):
361             e.permset.write = False
362
363
364 class TestAclExtensions:
365     """ACL extensions checks"""
366
367     @require_acl_from_mode
368     def test_from_mode(self):
369         """Test loading ACLs from an octal mode"""
370         acl1 = posix1e.ACL(mode=0o644)
371         assert acl1.valid()
372
373     @require_acl_check
374     def test_acl_check(self):
375         """Test the acl_check method"""
376         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
377         assert not acl1.check()
378         acl2 = posix1e.ACL()
379         c = acl2.check()
380         assert c == (ACL_MISS_ERROR, 0)
381         assert isinstance(c, tuple)
382         assert c[0] == ACL_MISS_ERROR
383         e = acl2.append()
384         c = acl2.check()
385         assert c == (ACL_ENTRY_ERROR, 0)
386
387     def test_applyto(self, subject):
388         """Test the apply_to function"""
389         # TODO: add read/compare with before, once ACL can be init'ed
390         # from any source.
391         basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
392         basic_acl.applyto(subject)
393         enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
394         assert enhanced_acl.valid()
395         enhanced_acl.applyto(subject)
396
397     def test_apply_to_with_wrong_object(self):
398         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
399         assert acl1.valid()
400         with pytest.raises(TypeError):
401           acl1.applyto(object())
402         with pytest.raises(TypeError):
403           acl1.applyto(object(), object()) # type: ignore
404
405     def test_apply_to_fail(self, testdir):
406         acl1 = posix1e.ACL(text=BASIC_ACL_TEXT)
407         assert acl1.valid()
408         fd, fname = get_file(testdir)
409         os.close(fd)
410         with pytest.raises(IOError):
411           acl1.applyto(fd)
412         with pytest.raises(IOError, match="no-such-file"):
413           acl1.applyto(fname+".no-such-file")
414
415     @require_extended_check
416     def test_applyto_extended(self, subject):
417         """Test the acl_extended function"""
418         basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
419         basic_acl.applyto(subject)
420         assert not has_extended(subject)
421         enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
422         assert enhanced_acl.valid()
423         enhanced_acl.applyto(subject)
424         assert has_extended(subject)
425
426     @require_extended_check
427     @pytest.mark.parametrize(
428         "gen", [ get_file_and_symlink, get_file_and_fobject ])
429     def test_applyto_extended_mixed(self, testdir, gen):
430         """Test the acl_extended function"""
431         with gen(testdir) as (a, b):
432             basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT)
433             basic_acl.applyto(a)
434             for item in a, b:
435                 assert not has_extended(item)
436             enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r")
437             assert enhanced_acl.valid()
438             enhanced_acl.applyto(b)
439             for item in a, b:
440                 assert has_extended(item)
441
442     @require_extended_check
443     def test_extended_fail(self, testdir):
444         fd, fname = get_file(testdir)
445         os.close(fd)
446         with pytest.raises(IOError):
447           has_extended(fd)
448         with pytest.raises(IOError, match="no-such-file"):
449           has_extended(fname+".no-such-file")
450
451     @require_extended_check
452     def test_extended_arg_handling(self):
453       with pytest.raises(TypeError):
454         has_extended() # type: ignore
455       with pytest.raises(TypeError):
456         has_extended(object()) # type: ignore
457
458     @require_equiv_mode
459     def test_equiv_mode(self):
460         """Test the equiv_mode function"""
461         if HAS_ACL_FROM_MODE:
462             for mode in 0o644, 0o755:
463                 acl = posix1e.ACL(mode=mode)
464                 assert acl.equiv_mode() == mode
465         acl = posix1e.ACL(text="u::rw,g::r,o::r")
466         assert acl.equiv_mode() == 0o644
467         acl = posix1e.ACL(text="u::rx,g::-,o::-")
468         assert acl.equiv_mode() == 0o500
469
470     @require_equiv_mode
471     @pytest.mark.xfail(reason="It seems equiv mode always passes, even for empty ACLs")
472     def test_equiv_mode_invalid(self):
473         """Test equiv_mode on invalid ACLs"""
474         a = posix1e.ACL()
475         with pytest.raises(EnvironmentError):
476             a.equiv_mode()
477
478     @require_acl_check
479     def test_to_any_text(self):
480         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
481         assert b"u::" in \
482           acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)
483         assert b"user::" in acl.to_any_text()
484
485     @require_acl_check
486     def test_to_any_text_wrong_args(self):
487         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
488         with pytest.raises(TypeError):
489           acl.to_any_text(foo="bar") # type: ignore
490
491
492     @require_acl_check
493     def test_rich_compare(self):
494         acl1 = posix1e.ACL(text="u::rw,g::r,o::r")
495         acl2 = posix1e.ACL(acl=acl1)
496         acl3 = posix1e.ACL(text="u::rw,g::rw,o::r")
497         assert acl1 == acl2
498         assert acl1 != acl3
499         with pytest.raises(TypeError):
500           acl1 < acl2 # type: ignore
501         with pytest.raises(TypeError):
502           acl1 >= acl3 # type: ignore
503         assert acl1 != True # type: ignore
504         assert not (acl1 == 1) # type: ignore
505         with pytest.raises(TypeError):
506           acl1 > True # type: ignore
507
508     @require_acl_entry
509     def test_acl_iterator(self):
510         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
511         for entry in acl:
512             assert entry.parent is acl
513
514     @require_copy_ext
515     def test_acl_copy_ext(self):
516         a = posix1e.ACL(text=BASIC_ACL_TEXT)
517         b = posix1e.ACL()
518         c = posix1e.ACL(acl=b)
519         assert a != b
520         assert b == c
521         state = a.__getstate__()
522         b.__setstate__(state)
523         assert a == b
524         assert b != c
525
526     @staticmethod
527     def get_nulled_state(src=None):
528         """Generate a mostly-valid external serialization
529
530         Passing arbitrary state into acl_copy_int() is dangerous. That
531         C function gets a void * buffer, and then casts that to an ACL
532         structure, irrespective of buffer length; this can lead to
533         segfaults (via unallocated memory indexing). Depending on the
534         exact buffer, the same code might segfault on all
535         architectures, some architectures, all C compiler versions, or
536         some C compilers, or any combination of the above :(
537
538         To mitigate this, pass a much larger buffer size as returned
539         from the state, just nulled out - in the Linux version of the
540         library, the first byte is the structure size and is tested
541         for correct size, and a null byte will cause failure.
542
543         """
544         if src is None:
545             src = posix1e.ACL()
546         state = src.__getstate__()
547         nulled = b'\x00' * (10 * len(state))
548         return nulled
549
550     @require_copy_ext
551     def test_acl_copy_int_failure(self):
552         a = posix1e.ACL()
553         nulled = self.get_nulled_state(a)
554         with pytest.raises(IOError):
555             a.__setstate__(nulled)
556
557     @require_copy_ext
558     def test_acl_copy_int_failure_is_noop(self):
559         a = posix1e.ACL(text=BASIC_ACL_TEXT)
560         b = posix1e.ACL()
561         c = posix1e.ACL(acl=a)
562         assert a == c
563         assert a != b
564         nulled = self.get_nulled_state(b)
565         with pytest.raises(IOError):
566             a.__setstate__(nulled)
567         # Assert that 'a' didn't change in the attempt to restore
568         # invalid state.
569         assert a == c
570
571     @require_copy_ext
572     def test_acl_copy_int_args(self):
573         a = posix1e.ACL()
574         with pytest.raises(TypeError):
575             a.__setstate__(None)
576
577     @require_copy_ext
578     def test_acl_init_copy_int(self):
579         a = posix1e.ACL(text=BASIC_ACL_TEXT)
580         b = posix1e.ACL()
581         c = posix1e.ACL(data=a.__getstate__())
582         assert c != b
583         assert c == a
584
585     @require_copy_ext
586     def test_acl_init_copy_int_invalid(self):
587         with pytest.raises(IOError):
588             posix1e.ACL(data=self.get_nulled_state())
589
590
591 class TestWrite:
592     """Write tests"""
593
594     def test_delete_default(self, testdir):
595         """Test removing the default ACL"""
596         with get_dir(testdir) as dname:
597           posix1e.delete_default(dname)
598
599     def test_delete_default_fail(self, testdir):
600         """Test removing the default ACL"""
601         with get_file_name(testdir) as fname:
602             with pytest.raises(IOError, match="no-such-file"):
603                 posix1e.delete_default(fname+".no-such-file")
604
605     @NOT_PYPY
606     def test_delete_default_wrong_arg(self):
607         with pytest.raises(TypeError):
608           posix1e.delete_default(object()) # type: ignore
609
610     def test_reapply(self, testdir):
611         """Test re-applying an ACL"""
612         fd, fname = get_file(testdir)
613         acl1 = posix1e.ACL(fd=fd)
614         acl1.applyto(fd)
615         acl1.applyto(fname)
616         with get_dir(testdir) as dname:
617           acl2 = posix1e.ACL(file=fname)
618           acl2.applyto(dname)
619
620
621
622 @require_acl_entry
623 class TestModification:
624     """ACL modification tests"""
625
626     def checkRef(self, obj):
627         """Checks if a given obj has a 'sane' refcount"""
628         if platform.python_implementation() == "PyPy":
629             return
630         ref_cnt = sys.getrefcount(obj)
631         # FIXME: hardcoded value for the max ref count... but I've
632         # seen it overflow on bad reference counting, so it's better
633         # to be safe
634         if ref_cnt < 2 or ref_cnt > 1024:
635             pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
636                         ref_cnt)
637
638     def test_str(self):
639         """Test str() of an ACL."""
640         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
641         str_acl = str(acl)
642         self.checkRef(str_acl)
643
644     def test_append(self):
645         """Test append a new Entry to the ACL"""
646         acl = posix1e.ACL()
647         e = acl.append()
648         e.tag_type = posix1e.ACL_OTHER
649         ignore_ioerror(errno.EINVAL, acl.calc_mask)
650         str_format = str(e)
651         self.checkRef(str_format)
652         e2 = acl.append(e)
653         ignore_ioerror(errno.EINVAL, acl.calc_mask)
654         assert not acl.valid()
655
656     def test_wrong_append(self):
657         """Test append a new Entry to the ACL based on wrong object type"""
658         acl = posix1e.ACL()
659         with pytest.raises(TypeError):
660           acl.append(object()) # type: ignore
661
662     @pytest.mark.xfail(reason="Behaviour not conform to specification")
663     def test_append_invalid_source(self):
664         a = posix1e.ACL()
665         b = posix1e.ACL()
666         f = b.append()
667         b.delete_entry(f)
668         with pytest.raises(EnvironmentError):
669             f.permset.write = True
670         with pytest.raises(EnvironmentError):
671             e = a.append(f)
672
673     def test_entry_creation(self):
674         acl = posix1e.ACL()
675         e = posix1e.Entry(acl)
676         ignore_ioerror(errno.EINVAL, acl.calc_mask)
677         str_format = str(e)
678         self.checkRef(str_format)
679
680     def test_entry_failed_creation(self):
681         # Checks for partial initialisation and deletion on error
682         # path.
683         with pytest.raises(TypeError):
684           posix1e.Entry(object()) # type: ignore
685
686     def test_entry_reinitialisations(self):
687         a = posix1e.ACL()
688         b = posix1e.ACL()
689         e = posix1e.Entry(a)
690         e.__init__(a) # type: ignore
691         with pytest.raises(ValueError, match="different parent"):
692             e.__init__(b) # type: ignore
693
694     @NOT_PYPY
695     def test_entry_reinit_leaks_refcount(self):
696         acl = posix1e.ACL()
697         e = acl.append()
698         ref = sys.getrefcount(acl)
699         e.__init__(acl) # type: ignore
700         assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..."
701
702     def test_delete(self):
703         """Test delete Entry from the ACL"""
704         acl = posix1e.ACL()
705         e = acl.append()
706         e.tag_type = posix1e.ACL_OTHER
707         ignore_ioerror(errno.EINVAL, acl.calc_mask)
708         acl.delete_entry(e)
709         ignore_ioerror(errno.EINVAL, acl.calc_mask)
710
711     def test_double_delete(self):
712         """Test delete Entry from the ACL"""
713         # This is not entirely valid/correct, since the entry object
714         # itself is invalid after the first deletion, so we're
715         # actually testing deleting an invalid object, not a
716         # non-existing entry...
717         acl = posix1e.ACL()
718         e = acl.append()
719         e.tag_type = posix1e.ACL_OTHER
720         ignore_ioerror(errno.EINVAL, acl.calc_mask)
721         acl.delete_entry(e)
722         ignore_ioerror(errno.EINVAL, acl.calc_mask)
723         with pytest.raises(EnvironmentError):
724           acl.delete_entry(e)
725
726     def test_delete_unowned(self):
727         """Test delete Entry from the ACL"""
728         a = posix1e.ACL()
729         b = posix1e.ACL()
730         e = a.append()
731         e.tag_type = posix1e.ACL_OTHER
732         with pytest.raises(ValueError, match="un-owned entry"):
733             b.delete_entry(e)
734
735     # This currently fails as this deletion seems to be accepted :/
736     @pytest.mark.xfail(reason="Entry deletion is unreliable")
737     def testDeleteInvalidEntry(self):
738         """Test delete foreign Entry from the ACL"""
739         acl1 = posix1e.ACL()
740         acl2 = posix1e.ACL()
741         e = acl1.append()
742         e.tag_type = posix1e.ACL_OTHER
743         ignore_ioerror(errno.EINVAL, acl1.calc_mask)
744         with pytest.raises(EnvironmentError):
745           acl2.delete_entry(e)
746
747     def test_delete_invalid_object(self):
748         """Test delete a non-Entry from the ACL"""
749         acl = posix1e.ACL()
750         with pytest.raises(TypeError):
751           acl.delete_entry(object()) # type: ignore
752
753     def test_double_entries(self):
754         """Test double entries"""
755         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
756         assert acl.valid()
757         for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
758                          posix1e.ACL_OTHER):
759             e = acl.append()
760             e.tag_type = tag_type
761             e.permset.clear()
762             assert not acl.valid(), ("ACL containing duplicate entries"
763                                      " should not be valid")
764             acl.delete_entry(e)
765
766     def test_multiple_good_entries(self):
767         """Test multiple valid entries"""
768         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
769         assert acl.valid()
770         for tag_type in (posix1e.ACL_USER,
771                          posix1e.ACL_GROUP):
772             for obj_id in range(5):
773                 e = acl.append()
774                 e.tag_type = tag_type
775                 e.qualifier = obj_id
776                 e.permset.clear()
777                 acl.calc_mask()
778                 assert acl.valid(), ("ACL should be able to hold multiple"
779                                      " user/group entries")
780
781     def test_multiple_bad_entries(self):
782         """Test multiple invalid entries"""
783         for tag_type in (posix1e.ACL_USER,
784                          posix1e.ACL_GROUP):
785             acl = posix1e.ACL(text=BASIC_ACL_TEXT)
786             assert acl.valid()
787             e1 = acl.append()
788             e1.tag_type = tag_type
789             e1.qualifier = 0
790             e1.permset.clear()
791             acl.calc_mask()
792             assert acl.valid(), ("ACL should be able to add a"
793                                  " user/group entry")
794             e2 = acl.append()
795             e2.tag_type = tag_type
796             e2.qualifier = 0
797             e2.permset.clear()
798             ignore_ioerror(errno.EINVAL, acl.calc_mask)
799             assert not acl.valid(), ("ACL should not validate when"
800                                      " containing two duplicate entries")
801             acl.delete_entry(e1)
802             # FreeBSD trips over itself here and can't delete the
803             # entry, even though it still exists.
804             ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
805
806     def test_copy(self):
807         acl = ACL()
808         e1 = acl.append()
809         e1.tag_type = ACL_USER
810         p1 = e1.permset
811         p1.clear()
812         p1.read = True
813         p1.write = True
814         e2 = acl.append()
815         e2.tag_type = ACL_GROUP
816         p2 = e2.permset
817         p2.clear()
818         p2.read = True
819         assert not p2.write
820         e2.copy(e1)
821         assert p2.write
822         assert e1.tag_type == e2.tag_type
823
824     def test_copy_wrong_arg(self):
825         acl = ACL()
826         e = acl.append()
827         with pytest.raises(TypeError):
828           e.copy(object()) # type: ignore
829
830     def test_set_permset(self):
831         acl = ACL()
832         e1 = acl.append()
833         e1.tag_type = ACL_USER
834         p1 = e1.permset
835         p1.clear()
836         p1.read = True
837         p1.write = True
838         e2 = acl.append()
839         e2.tag_type = ACL_GROUP
840         p2 = e2.permset
841         p2.clear()
842         p2.read = True
843         assert not p2.write
844         e2.permset = p1
845         assert e2.permset.write
846         assert e2.tag_type == ACL_GROUP
847
848     def test_set_permset_wrong_arg(self):
849         acl = ACL()
850         e = acl.append()
851         with pytest.raises(TypeError):
852           e.permset = object() # type: ignore
853
854     def test_permset_creation(self):
855         acl = ACL()
856         e = acl.append()
857         p1 = e.permset
858         p2 = Permset(e)
859         #assert p1 == p2
860
861     def test_permset_creation_wrong_arg(self):
862         with pytest.raises(TypeError):
863           Permset(object()) # type: ignore
864
865     def test_permset_reinitialisations(self):
866         a = posix1e.ACL()
867         e = posix1e.Entry(a)
868         f = posix1e.Entry(a)
869         p = e.permset
870         p.__init__(e) # type: ignore
871         with pytest.raises(ValueError, match="different parent"):
872             p.__init__(f) # type: ignore
873
874     @NOT_PYPY
875     def test_permset_reinit_leaks_refcount(self):
876         acl = posix1e.ACL()
877         e = acl.append()
878         p = e.permset
879         ref = sys.getrefcount(e)
880         p.__init__(e) # type: ignore
881         assert ref == sys.getrefcount(e), "Uh-oh, ref leaks..."
882
883     @pytest.mark.parametrize("perm, txt, accessor",
884                              PERMSETS, ids=PERMSETS_IDS)
885     def test_permset(self, perm, txt, accessor):
886         """Test permissions"""
887         del accessor
888         acl = posix1e.ACL()
889         e = acl.append()
890         ps = e.permset
891         ps.clear()
892         str_ps = str(ps)
893         self.checkRef(str_ps)
894         assert not ps.test(perm), ("Empty permission set should not"
895                                    " have permission '%s'" % txt)
896         ps.add(perm)
897         assert ps.test(perm), ("Permission '%s' should exist"
898                                " after addition" % txt)
899         str_ps = str(ps)
900         self.checkRef(str_ps)
901         ps.delete(perm)
902         assert not ps.test(perm), ("Permission '%s' should not exist"
903                                    " after deletion" % txt)
904         ps.add(perm)
905         assert ps.test(perm), ("Permission '%s' should exist"
906                                " after addition" % txt)
907         ps.clear()
908         assert not ps.test(perm), ("Permission '%s' should not exist"
909                                    " after clearing" % txt)
910
911
912
913     @pytest.mark.parametrize("perm, txt, accessor",
914                              PERMSETS, ids=PERMSETS_IDS)
915     def test_permset_via_accessors(self, perm, txt, accessor):
916         """Test permissions"""
917         acl = posix1e.ACL()
918         e = acl.append()
919         ps = e.permset
920         ps.clear()
921         def getter():
922             return accessor.__get__(ps) # type: ignore
923         def setter(value):
924             return accessor.__set__(ps, value) # type: ignore
925         str_ps = str(ps)
926         self.checkRef(str_ps)
927         assert not getter(), ("Empty permission set should not"
928                               " have permission '%s'" % txt)
929         setter(True)
930         assert ps.test(perm), ("Permission '%s' should exist"
931                                " after addition" % txt)
932         assert getter(), ("Permission '%s' should exist"
933                           " after addition" % txt)
934         str_ps = str(ps)
935         self.checkRef(str_ps)
936         setter(False)
937         assert not ps.test(perm), ("Permission '%s' should not exist"
938                                    " after deletion" % txt)
939         assert not getter(), ("Permission '%s' should not exist"
940                                   " after deletion" % txt)
941         setter(True)
942         assert getter()
943         ps.clear()
944         assert not getter()
945
946     def test_permset_invalid_type(self):
947         acl = posix1e.ACL()
948         e = acl.append()
949         ps = e.permset
950         ps.clear()
951         with pytest.raises(TypeError):
952           ps.add("foobar") # type: ignore
953         with pytest.raises(TypeError):
954           ps.delete("foobar") # type: ignore
955         with pytest.raises(TypeError):
956           ps.test("foobar") # type: ignore
957         with pytest.raises(ValueError):
958           ps.write = object() # type: ignore
959
960     @pytest.mark.parametrize("tag", [ACL_USER, ACL_GROUP],
961                              ids=["ACL_USER", "ACL_GROUP"])
962     def test_qualifier_values(self, tag):
963         """Tests qualifier correct store/retrieval"""
964         acl = posix1e.ACL()
965         e = acl.append()
966         qualifier = 1
967         e.tag_type = tag
968         while True:
969             regex = re.compile("(user|group) with (u|g)id %d" % qualifier)
970             try:
971                 e.qualifier = qualifier
972             except OverflowError:
973                 # reached overflow condition, break
974                 break
975             assert e.qualifier == qualifier
976             assert regex.search(str(e)) is not None
977             qualifier *= 2
978
979     def test_qualifier_overflow(self):
980         """Tests qualifier overflow handling"""
981         acl = posix1e.ACL()
982         e = acl.append()
983         # the uid_t/gid_t are unsigned, so they can hold slightly more
984         # than sys.maxsize*2 (on Linux).
985         qualifier = (sys.maxsize + 1) * 2
986         for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
987             e.tag_type = tag
988             with pytest.raises(OverflowError):
989                 e.qualifier = qualifier
990
991     def test_qualifier_underflow(self):
992         """Tests negative qualifier handling"""
993         # Note: this presumes that uid_t/gid_t in C are unsigned...
994         acl = posix1e.ACL()
995         e = acl.append()
996         for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
997             e.tag_type = tag
998             for qualifier in [-10, -5, -1]:
999                 with pytest.raises(OverflowError):
1000                     e.qualifier = qualifier
1001
1002     def test_invalid_qualifier(self):
1003         """Tests invalid qualifier handling"""
1004         acl = posix1e.ACL()
1005         e = acl.append()
1006         with pytest.raises(TypeError):
1007           e.qualifier = object() # type: ignore
1008         with pytest.raises((TypeError, AttributeError)):
1009           del e.qualifier
1010
1011     def test_qualifier_on_wrong_tag(self):
1012         """Tests qualifier setting on wrong tag"""
1013         acl = posix1e.ACL()
1014         e = acl.append()
1015         e.tag_type = posix1e.ACL_OTHER
1016         with pytest.raises(TypeError):
1017           e.qualifier = 1
1018         with pytest.raises(TypeError):
1019           e.qualifier
1020
1021     @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
1022     def test_tag_types(self, tag):
1023         """Tests tag type correct set/get"""
1024         acl = posix1e.ACL()
1025         e = acl.append()
1026         e.tag_type = tag
1027         assert e.tag_type == tag
1028         # check we can show all tag types without breaking
1029         assert str(e)
1030
1031     @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
1032     @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
1033     def test_tag_overwrite(self, src_tag, dst_tag):
1034         """Tests tag type correct set/get"""
1035         acl = posix1e.ACL()
1036         e = acl.append()
1037         e.tag_type = src_tag
1038         assert e.tag_type == src_tag
1039         assert str(e)
1040         e.tag_type = dst_tag
1041         assert e.tag_type == dst_tag
1042         assert str(e)
1043
1044     def test_invalid_tags(self):
1045         """Tests tag type incorrect set/get"""
1046         acl = posix1e.ACL()
1047         e = acl.append()
1048         with pytest.raises(TypeError):
1049           e.tag_type = object() # type: ignore
1050         e.tag_type = posix1e.ACL_USER_OBJ
1051         # For some reason, PyPy raises AttributeError. Strange...
1052         with pytest.raises((TypeError, AttributeError)):
1053           del e.tag_type
1054
1055     def test_tag_wrong_overwrite(self):
1056         acl = posix1e.ACL()
1057         e = acl.append()
1058         e.tag_type = posix1e.ACL_USER_OBJ
1059         tag = max(ALL_TAG_VALUES) + 1
1060         with pytest.raises(EnvironmentError):
1061           e.tag_type = tag
1062         # Check tag is still valid.
1063         assert e.tag_type == posix1e.ACL_USER_OBJ
1064
1065 if __name__ == "__main__":
1066     unittest.main()