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