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