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