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