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