]> git.k1024.org Git - pylibacl.git/blob - tests/test_acls.py
Implement creating an ACL directly from serialised form
[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     @require_copy_ext
496     def test_acl_init_copy_ext(self):
497         a = posix1e.ACL(text=BASIC_ACL_TEXT)
498         b = posix1e.ACL()
499         c = posix1e.ACL(data=a.__getstate__())
500         assert c != b
501         assert c == a
502
503     @require_copy_ext
504     def test_acl_init_copy_ext_invalid(self):
505         with pytest.raises(IOError):
506             posix1e.ACL(data=b"foobar")
507
508
509 class TestWrite:
510     """Write tests"""
511
512     def test_delete_default(self, testdir):
513         """Test removing the default ACL"""
514         with get_dir(testdir) as dname:
515           posix1e.delete_default(dname)
516
517     def test_delete_default_fail(self, testdir):
518         """Test removing the default ACL"""
519         with get_file_name(testdir) as fname:
520             with pytest.raises(IOError, match="no-such-file"):
521                 posix1e.delete_default(fname+".no-such-file")
522
523     @NOT_PYPY
524     def test_delete_default_wrong_arg(self):
525         with pytest.raises(TypeError):
526           posix1e.delete_default(object()) # type: ignore
527
528     def test_reapply(self, testdir):
529         """Test re-applying an ACL"""
530         fd, fname = get_file(testdir)
531         acl1 = posix1e.ACL(fd=fd)
532         acl1.applyto(fd)
533         acl1.applyto(fname)
534         with get_dir(testdir) as dname:
535           acl2 = posix1e.ACL(file=fname)
536           acl2.applyto(dname)
537
538
539
540 @require_acl_entry
541 class TestModification:
542     """ACL modification tests"""
543
544     def checkRef(self, obj):
545         """Checks if a given obj has a 'sane' refcount"""
546         if platform.python_implementation() == "PyPy":
547             return
548         ref_cnt = sys.getrefcount(obj)
549         # FIXME: hardcoded value for the max ref count... but I've
550         # seen it overflow on bad reference counting, so it's better
551         # to be safe
552         if ref_cnt < 2 or ref_cnt > 1024:
553             pytest.fail("Wrong reference count, expected 2-1024 and got %d" %
554                         ref_cnt)
555
556     def test_str(self):
557         """Test str() of an ACL."""
558         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
559         str_acl = str(acl)
560         self.checkRef(str_acl)
561
562     def test_append(self):
563         """Test append a new Entry to the ACL"""
564         acl = posix1e.ACL()
565         e = acl.append()
566         e.tag_type = posix1e.ACL_OTHER
567         ignore_ioerror(errno.EINVAL, acl.calc_mask)
568         str_format = str(e)
569         self.checkRef(str_format)
570         e2 = acl.append(e)
571         ignore_ioerror(errno.EINVAL, acl.calc_mask)
572         assert not acl.valid()
573
574     def test_wrong_append(self):
575         """Test append a new Entry to the ACL based on wrong object type"""
576         acl = posix1e.ACL()
577         with pytest.raises(TypeError):
578           acl.append(object()) # type: ignore
579
580     @pytest.mark.xfail(reason="Behaviour not conform to specification")
581     def test_append_invalid_source(self):
582         a = posix1e.ACL()
583         b = posix1e.ACL()
584         f = b.append()
585         b.delete_entry(f)
586         with pytest.raises(EnvironmentError):
587             f.permset.write = True
588         with pytest.raises(EnvironmentError):
589             e = a.append(f)
590
591     def test_entry_creation(self):
592         acl = posix1e.ACL()
593         e = posix1e.Entry(acl)
594         ignore_ioerror(errno.EINVAL, acl.calc_mask)
595         str_format = str(e)
596         self.checkRef(str_format)
597
598     def test_entry_failed_creation(self):
599         # Checks for partial initialisation and deletion on error
600         # path.
601         with pytest.raises(TypeError):
602           posix1e.Entry(object()) # type: ignore
603
604     def test_entry_reinitialisations(self):
605         a = posix1e.ACL()
606         b = posix1e.ACL()
607         e = posix1e.Entry(a)
608         e.__init__(a) # type: ignore
609         with pytest.raises(ValueError, match="different parent"):
610             e.__init__(b) # type: ignore
611
612     @NOT_PYPY
613     def test_entry_reinit_leaks_refcount(self):
614         acl = posix1e.ACL()
615         e = acl.append()
616         ref = sys.getrefcount(acl)
617         e.__init__(acl) # type: ignore
618         assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..."
619
620     def test_delete(self):
621         """Test delete Entry from the ACL"""
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
629     def test_double_delete(self):
630         """Test delete Entry from the ACL"""
631         # This is not entirely valid/correct, since the entry object
632         # itself is invalid after the first deletion, so we're
633         # actually testing deleting an invalid object, not a
634         # non-existing entry...
635         acl = posix1e.ACL()
636         e = acl.append()
637         e.tag_type = posix1e.ACL_OTHER
638         ignore_ioerror(errno.EINVAL, acl.calc_mask)
639         acl.delete_entry(e)
640         ignore_ioerror(errno.EINVAL, acl.calc_mask)
641         with pytest.raises(EnvironmentError):
642           acl.delete_entry(e)
643
644     def test_delete_unowned(self):
645         """Test delete Entry from the ACL"""
646         a = posix1e.ACL()
647         b = posix1e.ACL()
648         e = a.append()
649         e.tag_type = posix1e.ACL_OTHER
650         with pytest.raises(ValueError, match="un-owned entry"):
651             b.delete_entry(e)
652
653     # This currently fails as this deletion seems to be accepted :/
654     @pytest.mark.xfail(reason="Entry deletion is unreliable")
655     def testDeleteInvalidEntry(self):
656         """Test delete foreign Entry from the ACL"""
657         acl1 = posix1e.ACL()
658         acl2 = posix1e.ACL()
659         e = acl1.append()
660         e.tag_type = posix1e.ACL_OTHER
661         ignore_ioerror(errno.EINVAL, acl1.calc_mask)
662         with pytest.raises(EnvironmentError):
663           acl2.delete_entry(e)
664
665     def test_delete_invalid_object(self):
666         """Test delete a non-Entry from the ACL"""
667         acl = posix1e.ACL()
668         with pytest.raises(TypeError):
669           acl.delete_entry(object()) # type: ignore
670
671     def test_double_entries(self):
672         """Test double entries"""
673         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
674         assert acl.valid()
675         for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ,
676                          posix1e.ACL_OTHER):
677             e = acl.append()
678             e.tag_type = tag_type
679             e.permset.clear()
680             assert not acl.valid(), ("ACL containing duplicate entries"
681                                      " should not be valid")
682             acl.delete_entry(e)
683
684     def test_multiple_good_entries(self):
685         """Test multiple valid entries"""
686         acl = posix1e.ACL(text=BASIC_ACL_TEXT)
687         assert acl.valid()
688         for tag_type in (posix1e.ACL_USER,
689                          posix1e.ACL_GROUP):
690             for obj_id in range(5):
691                 e = acl.append()
692                 e.tag_type = tag_type
693                 e.qualifier = obj_id
694                 e.permset.clear()
695                 acl.calc_mask()
696                 assert acl.valid(), ("ACL should be able to hold multiple"
697                                      " user/group entries")
698
699     def test_multiple_bad_entries(self):
700         """Test multiple invalid entries"""
701         for tag_type in (posix1e.ACL_USER,
702                          posix1e.ACL_GROUP):
703             acl = posix1e.ACL(text=BASIC_ACL_TEXT)
704             assert acl.valid()
705             e1 = acl.append()
706             e1.tag_type = tag_type
707             e1.qualifier = 0
708             e1.permset.clear()
709             acl.calc_mask()
710             assert acl.valid(), ("ACL should be able to add a"
711                                  " user/group entry")
712             e2 = acl.append()
713             e2.tag_type = tag_type
714             e2.qualifier = 0
715             e2.permset.clear()
716             ignore_ioerror(errno.EINVAL, acl.calc_mask)
717             assert not acl.valid(), ("ACL should not validate when"
718                                      " containing two duplicate entries")
719             acl.delete_entry(e1)
720             # FreeBSD trips over itself here and can't delete the
721             # entry, even though it still exists.
722             ignore_ioerror(errno.EINVAL, acl.delete_entry, e2)
723
724     def test_copy(self):
725         acl = ACL()
726         e1 = acl.append()
727         e1.tag_type = ACL_USER
728         p1 = e1.permset
729         p1.clear()
730         p1.read = True
731         p1.write = True
732         e2 = acl.append()
733         e2.tag_type = ACL_GROUP
734         p2 = e2.permset
735         p2.clear()
736         p2.read = True
737         assert not p2.write
738         e2.copy(e1)
739         assert p2.write
740         assert e1.tag_type == e2.tag_type
741
742     def test_copy_wrong_arg(self):
743         acl = ACL()
744         e = acl.append()
745         with pytest.raises(TypeError):
746           e.copy(object()) # type: ignore
747
748     def test_set_permset(self):
749         acl = ACL()
750         e1 = acl.append()
751         e1.tag_type = ACL_USER
752         p1 = e1.permset
753         p1.clear()
754         p1.read = True
755         p1.write = True
756         e2 = acl.append()
757         e2.tag_type = ACL_GROUP
758         p2 = e2.permset
759         p2.clear()
760         p2.read = True
761         assert not p2.write
762         e2.permset = p1
763         assert e2.permset.write
764         assert e2.tag_type == ACL_GROUP
765
766     def test_set_permset_wrong_arg(self):
767         acl = ACL()
768         e = acl.append()
769         with pytest.raises(TypeError):
770           e.permset = object() # type: ignore
771
772     def test_permset_creation(self):
773         acl = ACL()
774         e = acl.append()
775         p1 = e.permset
776         p2 = Permset(e)
777         #assert p1 == p2
778
779     def test_permset_creation_wrong_arg(self):
780         with pytest.raises(TypeError):
781           Permset(object()) # type: ignore
782
783     def test_permset_reinitialisations(self):
784         a = posix1e.ACL()
785         e = posix1e.Entry(a)
786         f = posix1e.Entry(a)
787         p = e.permset
788         p.__init__(e) # type: ignore
789         with pytest.raises(ValueError, match="different parent"):
790             p.__init__(f) # type: ignore
791
792     @NOT_PYPY
793     def test_permset_reinit_leaks_refcount(self):
794         acl = posix1e.ACL()
795         e = acl.append()
796         p = e.permset
797         ref = sys.getrefcount(e)
798         p.__init__(e) # type: ignore
799         assert ref == sys.getrefcount(e), "Uh-oh, ref leaks..."
800
801     @pytest.mark.parametrize("perm, txt, accessor",
802                              PERMSETS, ids=PERMSETS_IDS)
803     def test_permset(self, perm, txt, accessor):
804         """Test permissions"""
805         del accessor
806         acl = posix1e.ACL()
807         e = acl.append()
808         ps = e.permset
809         ps.clear()
810         str_ps = str(ps)
811         self.checkRef(str_ps)
812         assert not ps.test(perm), ("Empty permission set should not"
813                                    " have permission '%s'" % txt)
814         ps.add(perm)
815         assert ps.test(perm), ("Permission '%s' should exist"
816                                " after addition" % txt)
817         str_ps = str(ps)
818         self.checkRef(str_ps)
819         ps.delete(perm)
820         assert not ps.test(perm), ("Permission '%s' should not exist"
821                                    " after deletion" % txt)
822         ps.add(perm)
823         assert ps.test(perm), ("Permission '%s' should exist"
824                                " after addition" % txt)
825         ps.clear()
826         assert not ps.test(perm), ("Permission '%s' should not exist"
827                                    " after clearing" % txt)
828
829
830
831     @pytest.mark.parametrize("perm, txt, accessor",
832                              PERMSETS, ids=PERMSETS_IDS)
833     def test_permset_via_accessors(self, perm, txt, accessor):
834         """Test permissions"""
835         acl = posix1e.ACL()
836         e = acl.append()
837         ps = e.permset
838         ps.clear()
839         def getter():
840             return accessor.__get__(ps) # type: ignore
841         def setter(value):
842             return accessor.__set__(ps, value) # type: ignore
843         str_ps = str(ps)
844         self.checkRef(str_ps)
845         assert not getter(), ("Empty permission set should not"
846                               " have permission '%s'" % txt)
847         setter(True)
848         assert ps.test(perm), ("Permission '%s' should exist"
849                                " after addition" % txt)
850         assert getter(), ("Permission '%s' should exist"
851                           " after addition" % txt)
852         str_ps = str(ps)
853         self.checkRef(str_ps)
854         setter(False)
855         assert not ps.test(perm), ("Permission '%s' should not exist"
856                                    " after deletion" % txt)
857         assert not getter(), ("Permission '%s' should not exist"
858                                   " after deletion" % txt)
859         setter(True)
860         assert getter()
861         ps.clear()
862         assert not getter()
863
864     def test_permset_invalid_type(self):
865         acl = posix1e.ACL()
866         e = acl.append()
867         ps = e.permset
868         ps.clear()
869         with pytest.raises(TypeError):
870           ps.add("foobar") # type: ignore
871         with pytest.raises(TypeError):
872           ps.delete("foobar") # type: ignore
873         with pytest.raises(TypeError):
874           ps.test("foobar") # type: ignore
875         with pytest.raises(ValueError):
876           ps.write = object() # type: ignore
877
878     @pytest.mark.parametrize("tag", [ACL_USER, ACL_GROUP],
879                              ids=["ACL_USER", "ACL_GROUP"])
880     def test_qualifier_values(self, tag):
881         """Tests qualifier correct store/retrieval"""
882         acl = posix1e.ACL()
883         e = acl.append()
884         qualifier = 1
885         e.tag_type = tag
886         while True:
887             regex = re.compile("(user|group) with (u|g)id %d" % qualifier)
888             try:
889                 e.qualifier = qualifier
890             except OverflowError:
891                 # reached overflow condition, break
892                 break
893             assert e.qualifier == qualifier
894             assert regex.search(str(e)) is not None
895             qualifier *= 2
896
897     def test_qualifier_overflow(self):
898         """Tests qualifier overflow handling"""
899         acl = posix1e.ACL()
900         e = acl.append()
901         # the uid_t/gid_t are unsigned, so they can hold slightly more
902         # than sys.maxsize*2 (on Linux).
903         qualifier = (sys.maxsize + 1) * 2
904         for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
905             e.tag_type = tag
906             with pytest.raises(OverflowError):
907                 e.qualifier = qualifier
908
909     def test_qualifier_underflow(self):
910         """Tests negative qualifier handling"""
911         # Note: this presumes that uid_t/gid_t in C are unsigned...
912         acl = posix1e.ACL()
913         e = acl.append()
914         for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]:
915             e.tag_type = tag
916             for qualifier in [-10, -5, -1]:
917                 with pytest.raises(OverflowError):
918                     e.qualifier = qualifier
919
920     def test_invalid_qualifier(self):
921         """Tests invalid qualifier handling"""
922         acl = posix1e.ACL()
923         e = acl.append()
924         with pytest.raises(TypeError):
925           e.qualifier = object() # type: ignore
926         with pytest.raises((TypeError, AttributeError)):
927           del e.qualifier
928
929     def test_qualifier_on_wrong_tag(self):
930         """Tests qualifier setting on wrong tag"""
931         acl = posix1e.ACL()
932         e = acl.append()
933         e.tag_type = posix1e.ACL_OTHER
934         with pytest.raises(TypeError):
935           e.qualifier = 1
936         with pytest.raises(TypeError):
937           e.qualifier
938
939     @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
940     def test_tag_types(self, tag):
941         """Tests tag type correct set/get"""
942         acl = posix1e.ACL()
943         e = acl.append()
944         e.tag_type = tag
945         assert e.tag_type == tag
946         # check we can show all tag types without breaking
947         assert str(e)
948
949     @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
950     @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS)
951     def test_tag_overwrite(self, src_tag, dst_tag):
952         """Tests tag type correct set/get"""
953         acl = posix1e.ACL()
954         e = acl.append()
955         e.tag_type = src_tag
956         assert e.tag_type == src_tag
957         assert str(e)
958         e.tag_type = dst_tag
959         assert e.tag_type == dst_tag
960         assert str(e)
961
962     def test_invalid_tags(self):
963         """Tests tag type incorrect set/get"""
964         acl = posix1e.ACL()
965         e = acl.append()
966         with pytest.raises(TypeError):
967           e.tag_type = object() # type: ignore
968         e.tag_type = posix1e.ACL_USER_OBJ
969         # For some reason, PyPy raises AttributeError. Strange...
970         with pytest.raises((TypeError, AttributeError)):
971           del e.tag_type
972
973     def test_tag_wrong_overwrite(self):
974         acl = posix1e.ACL()
975         e = acl.append()
976         e.tag_type = posix1e.ACL_USER_OBJ
977         tag = max(ALL_TAG_VALUES) + 1
978         with pytest.raises(EnvironmentError):
979           e.tag_type = tag
980         # Check tag is still valid.
981         assert e.tag_type == posix1e.ACL_USER_OBJ
982
983 if __name__ == "__main__":
984     unittest.main()