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