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