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