././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740347991.964322 pylibacl-0.7.2/0000775000000000000000000000000014756715130012070 5ustar00rootroot././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682280673.0 pylibacl-0.7.2/CONTRIBUTING.md0000644000000000000000000000422014421310341014275 0ustar00rootroot# Contributing to pylibacl Hi, and thanks for any and all contributions! ## Bugs and patches This is a small project, so let's keep things simple: - Please file all bug reports on github (), as this allows archival and discovery by other people; - Send patches as pull requests; for larger changes, would be good to first open a bug to discuss the plans; Due to simplicity, there are no old branches being kept alive, but if it ever happens that a bug is found in older versions and there is needed to support older Python versions, it is possible to do so. ## Code standards There are no formal standards, but: - Code should be tested - this is why there's a [Codecov integration](https://app.codecov.io/gh/iustin/pylibacl/tree/main). - New functions should have good docstrings (in the C code). - New functions/constants should be listed in the documentation, see `doc/module.rst` for how to include them. - All non-trivial changes should be listed in `NEWS.md` for further inclusion in new releases documentation. Add an "unreleased" section (if one doesn't exist yet) to list the changes. ## Release process Right now, due to GPG signing, I'm doing releases and signing them manually (offline, I mean). Basically, once GitHub workflows are fine: - Bump the version in all places - use `git grep -F $OLD_VER` and update as needed. - Ensure that `setup.py` has the right Python versions listed (bit me more than once). - Update the `NEWS.md` file is up to date (contents), and use the right date. - Check that the generated documentation (`make doc`) looks right. Then run these steps: ``` $ make clean $ make distcheck # this leaves things in dist/ $ git tag -m 'Release pylibacl-0.0.1' --sign v0.0.1 $ gpg --sign -b -a dist/pylibacl-0.0.1.tar.gz $ python3 -m twine upload dist/* ``` Separately: * Upload the `dist/` contents to GitHub and tag a new release. * Upload the `dist/` contents to the old-style download area, . Hopefully one day all this can be more automated. ## Signing key The releases are currently signed by my key, see . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1336822167.0 pylibacl-0.7.2/COPYING0000644000000000000000000006350411753444627013135 0ustar00rootroot GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682282516.0 pylibacl-0.7.2/MANIFEST.in0000644000000000000000000000037614421314024013614 0ustar00rootrootinclude COPYING include Makefile include NEWS.md include README.md include CONTRIBUTING.md include SECURITY.md include acl.c include setup.cfg include doc/conf.py include doc/*.rst include doc/*.md include tests/*.py include py.typed include posix1e.pyi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347900.0 pylibacl-0.7.2/Makefile0000644000000000000000000000366714756714774013557 0ustar00rootrootPYTHON = python3 SPHINXOPTS = -W SPHINXBUILD = $(PYTHON) -m sphinx DOCDIR = doc DOCHTML = $(DOCDIR)/html DOCTREES = $(DOCDIR)/doctrees ALLSPHINXOPTS = -d $(DOCTREES) $(SPHINXOPTS) $(DOCDIR) VERSION = 0.7.2 FULLVER = pylibacl-$(VERSION) DISTFILE = $(FULLVER).tar.gz MODNAME = posix1e.so DOCFILES = doc/index.rst doc/module.rst doc/news.md doc/readme.md doc/conf.py all: doc test $(MODNAME): acl.c $(PYTHON) ./setup.py build_ext --inplace $(DOCHTML)/index.html: $(MODNAME) $(DOCFILES) acl.c $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DOCHTML) touch $@ doc: $(DOCHTML)/index.html dist: fakeroot $(PYTHON) ./setup.py sdist distcheck: dist set -e; \ TDIR=$$(mktemp -d) && \ trap "rm -rf $$TDIR" EXIT; \ tar xzf dist/$(DISTFILE) -C $$TDIR && \ (cd $$TDIR/$(FULLVER) && make doc && make test && make dist) && \ echo "All good, you can upload $(DISTFILE)!" test: @set -e; \ for ver in 3.7 3.8 3.9 3.10 3.11 3.12; do \ for flavour in "" "-dbg"; do \ if type python$$ver$$flavour >/dev/null; then \ echo Testing with python$$ver$$flavour; \ python$$ver$$flavour ./setup.py build_ext -i; \ python$$ver$$flavour -m pytest tests ;\ fi; \ done; \ done; \ for pp in pypy3; do \ if type $$pp >/dev/null; then \ echo Testing with $$pp; \ $$pp ./setup.py build_ext -i; \ $$pp -m pytest tests; \ fi; \ done fast-test: python3 setup.py build_ext -i python3 -m pytest tests ci: while inotifywait -e CLOSE_WRITE tests/test_*.py; do \ python3 -m pytest tests; \ done coverage: $(MAKE) clean $(MAKE) test CFLAGS="-coverage" lcov --capture --no-external --directory . --output-file coverage.info genhtml coverage.info --output-directory out clean: rm -rf $(DOCHTML) $(DOCTREES) rm -f $(MODNAME) rm -f *.so rm -rf build types: MYPYPATH=. mypy --check-untyped-defs --warn-incomplete-stub tests/test_acls.py .PHONY: doc test clean dist coverage ci types ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347851.0 pylibacl-0.7.2/NEWS.md0000644000000000000000000001557514756714713013207 0ustar00rootroot# News ## Version 0.7.2 *released Sun, 23 Feb 2025* Single-bugfix release: fixed the typing stub module. Nothing exercised it, and having been generated with pre-3.6 stubgen, it failed to work on modern versions. No tests failed (should add some), but the doc build by Sphinx failed accidentally since the failure to import (which was ignored) led to a missing title for the module, which Sphinx complained about. Quite funny :) ## Version 0.7.1 *released Fri, 14 Feb 2025* Minor version, with a few test improvements, and updated documentation building dependencies. No user-visible changes otherwise. Tested with CPython versions 3.7-3.13, and PyPy 3.7-3.10. ## Version 0.7.0 *released Sun, 23 Apr 2023* Important: Python 3.7 is the minimum supported version, due to difficulty of testing old releases, and the fact that everything older has been deprecated a long time ago (e.g. 3.6 at the end of 2021). Otherwise, a minor release: - Improve error handling in some corner cases (not expected to have any real-life impact, but who knows). - Improved testing coverage and test infrastructure. - Modernise parts of the C code based on recent Python version guidelines. - Add a simple security policy and contribution guidelines. ## Version 0.6.0 *released Sun, 29 Nov 2020* Major release removing Python 2 support. This allow both code cleanup and new features, such as: - Support for pathlib objects in `apply_to` and `has_extended` functions when running with Python 3.6 and newer. - Use of built-in C API functions for bytes/unicode/pathlib conversion when dealing with file names, removing custom code (with the associated benefits). Important API changes/bug fixes: - Initialisation protocol has been changed, to disallow uninitialised objects; this means that `__new__` will always create valid objects, to prevent the need for checking initialisation status in all code paths; this also (implicitly) fixes memory leaks on re-initialisation (calling `__init__(…)` on an existing object) and segfaults (!) on non-initialised object attribute access. Note ACL re-initialisation is tricky and (still) leads to undefined behaviour of existing Entry objects pointing to it. - Fix another bug in ACL re-initialisation where failures would result in invalid objects; now failed re-initialisation does not touch the original object. - Restore `__setstate__`/`__getstate__` support on Linux; this was inadvertently removed due a typo(!) when adding support for it in FreeBSD. Pickle should work again for ACL instances, although not sure how stable this serialisation format actually is. - Additionally, slightly change `__setstate__()` input to not allow Unicode, since the serialisation format is an opaque binary format. - Fix (and change) entry qualifier (which is a user/group ID) behaviour: assume/require that uid_t/gid_t are unsigned types (they are with glibc, MacOS and FreeBSD at least; the standard doesn't document the signedness), and convert parsing and returning the qualifier to behave accordingly. The breakage was most apparent on 32-bit architectures, in which context the problem was originally reported (see issue #13). Minor improvements: - Added a `data` keyword argument to `ACL()`, which allows restoring an ACL directly from a serialised form (as given by `__getstate__()`), which should simplify some uses cases (`a = ACL(); a.__set state__(…)`). - When available, add the file path to I/O error messages, which should lead to easier debugging. - The test suite has changed to `pytest`, which allows increased coverage via parameterisation. ## Version 0.5.4 *released Thu, 14 Nov 2019* Maintenance release: - Switch build system to Python 3 by default (can be overridden if needed). - Internal improvements for better cpychecker support. - Fix compatibility with PyPy. - Test improvements (both local and on Travis), testing more variations (debug, PyPy). - Improve test coverage, and allow gathering test coverage results. - Drop support (well, drop testing) for Python lower than 2.7. - Minor documentation improvements (closes #9, #12). ## Version 0.5.3 *released Thu, 30 Apr 2015* FreeBSD fixes: - Enable all FreeBSD versions after 7.x at level 2 (thanks to Garrett Cooper). - Make test suite pass under FreeBSD, which has a stricter behaviour with regards to invalid ACLs (which we do exercise in the test suite), thanks again to Garret for the bug reports. ## Version 0.5.2 *released Sat, 24 May 2014* No visible changes release: just fix tests when running under pypy. ## Version 0.5.1 *released Sun, 13 May 2012* A bug-fix only release. Critical bugs (memory leaks and possible segmentation faults) have been fixed thanks to Dave Malcolm and his ``cpychecker`` tool. Additionally, some compatibility issues with Python 3.x have been fixed (str() methods returning bytes). The documentation has been improved and changed from epydoc to sphinx; note however that the documentation is still auto-generated from the docstrings. Project reorganisation: the project home page has been moved from SourceForge to GitHub. ## Version 0.5 *released Sun, 27 Dec 2009* Added support for Python 3.x and improved support for Unicode filenames. ## Version 0.4 *released Sat, 28 Jun 2008* ### License Starting with this version, pylibacl is licensed under LGPL 2.1, Febryary 1999 or any later versions (see README.rst and COPYING). ### Linux support A few more Linux-specific functions: - add the ACL.equiv_mode() method, which will return the equivalent octal mode if this is a basic ACL and raise an IOError exception otherwise - add the acl_extended(...) function, which will check if an fd or path has an extended ACL ### FreeBSD support FreeBSD 7.x will have almost all the acl manipulation functions that Linux has, with the exception of __getstate__/__setstate__. As a workaround, use the str() and ACL(text=...) methods to pass around textual representations. ### Interface At module level there are now a few constants exported for easy-checking at runtime what features have been compiled in: - `HAS_ACL_FROM_MODE`, denoting whether the ACL constructor supports the `mode=0xxx` parameter - `HAS_ACL_CHECK`, denoting whether ACL instances support the `check()` method - `HAS_ACL_ENTRY`, denoting whether ACL manipulation is possible and the Entry and Permset classes are available - `HAS_EXTENEDED_CHECK`, denoting whether the `acl_extended()` function is supported - `HAS_EQUIV_MODE`, denoting whether ACL instances support the `equiv_mode()` method ### Internals Many functions have now unittests, which is a good thing. ## Version 0.3 *released Sun, 21 Oct 2007* ### Linux support Under Linux, implement more functions from libacl: - add `ACL(mode=...)`, implementing `acl_from_mode`. - add `ACL.to_any_text()`, implementing `acl_to_any_text`. - add ACL comparison, using `acl_cmp`. - add `ACL.check()`, which is a more descriptive function than validate. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740347991.964322 pylibacl-0.7.2/PKG-INFO0000664000000000000000000000205014756715130013162 0ustar00rootrootMetadata-Version: 2.1 Name: pylibacl Version: 0.7.2 Summary: POSIX.1e ACLs for python Home-page: https://pylibacl.k1024.org/ Author: Iustin Pop Author-email: iustin@k1024.org License: LGPL Project-URL: Bug Tracker, https://github.com/iustin/pylibacl/issues Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Operating System :: POSIX :: BSD :: FreeBSD Classifier: Operating System :: POSIX :: Linux Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Filesystems Requires-Python: >=3.7 License-File: COPYING This is a C extension module for Python which implements POSIX ACLs manipulation. It is a wrapper on top of the systems's acl C library - see acl(5). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347909.0 pylibacl-0.7.2/README.md0000644000000000000000000000644414756715005013356 0ustar00rootroot# pylibacl This is a Python 3.7+ extension module allows you to manipulate the POSIX.1e Access Control Lists present in some OS/file-systems combinations. Downloads: go to . Latest version is 0.7.2. The source repository is either at or at . For any issues, please file bugs at . See the `CONTRIBUTING.md` file for details on how to contribute, or support me on [ko-fi](https://ko-fi.com/iustin). [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/iustin/pylibacl/ci.yml?branch=main)](https://github.com/iustin/pylibacl/actions/workflows/ci.yml) [![Codecov](https://img.shields.io/codecov/c/github/iustin/pylibacl)](https://codecov.io/gh/iustin/pylibacl) [![Read the Docs](https://img.shields.io/readthedocs/pylibacl)](http://pylibacl.readthedocs.io/en/latest/?badge=latest) [![GitHub issues](https://img.shields.io/github/issues/iustin/pylibacl)](https://github.com/iustin/pylibacl/issues) ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/iustin/pylibacl) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/iustin/pylibacl)](https://github.com/iustin/pylibacl/releases) [![PyPI](https://img.shields.io/pypi/v/pylibacl)](https://pypi.org/project/pylibacl/) ![Debian package](https://img.shields.io/debian/v/python-pylibacl) ![Ubuntu package](https://img.shields.io/ubuntu/v/python-pylibacl) ![GitHub Release Date](https://img.shields.io/github/release-date/iustin/pylibacl) ![GitHub commits since latest release](https://img.shields.io/github/commits-since/iustin/pylibacl/latest) ![GitHub last commit](https://img.shields.io/github/last-commit/iustin/pylibacl) ## Requirements pylibacl has been written and tested on Linux, kernel v2.4 or newer, with XFS filesystems; ext2/ext3 should also work. Since release 0.4.0, FreeBSD 7 also has quite good support. If any other platform implements the POSIX.1e draft, pylibacl can be used. I heard that Solaris does, but I can't test it. - Python 3.7 or newer. Python 2.4+ was supported in the 0.5.x branch, Python 3.4+ in the 0.6 branch. - Operating system: - Linux, kernel v2.4 or newer, and the libacl library and development packages (all modern distributions should have this, under various names); also the file-systems you use must have ACLs turned on, either as a compile or mount option. - FreeBSD 7.0 or newer. - The sphinx python module, for your python version, if building the documentation. ## FreeBSD Note that on FreeBSD, ACLs are not enabled by default (at least on UFS file systems). To enable them, run `tunefs -a enabled` on the file system in question (after mounting it read-only). Then install: - `pkg install py36-setuptools py36-sphinx` or: - `pkg install py37-setuptools` ## Security For reporting security vulnerabilities, please see `SECURITY.md`. ## License pylibacl is Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop. pylibacl is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. See the COPYING file for the full license terms. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1681673002.0 pylibacl-0.7.2/SECURITY.md0000644000000000000000000000121014417045452013646 0ustar00rootroot# Security Policy To report a (potential or confirmed) security issue, please email with a description of the issue, steps to reproduce it, affected versions, and if known, mitigations for the issue. Since this is a small project, there's no list of supported versions. I will attempt to reply to reports within a working week, and to fix and disclose vulnerabilities within 90 days, but this is not a guarantee. Optionally, you can encrypt the email with my GPG key, see for details . Alternatively, you can use the GitHub "Private vulnerability reporting" functionality (but note this is beta). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707519104.0 pylibacl-0.7.2/acl.c0000664000000000000000000016022314561526200012767 0ustar00rootroot/* posix1e - a python module exposing the posix acl functions Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #define PY_SSIZE_T_CLEAN #include #include #include #ifdef HAVE_LINUX #include #define get_perm acl_get_perm #elif HAVE_FREEBSD #define get_perm acl_get_perm_np #endif /* Used for cpychecker: */ /* The checker automatically defines this preprocessor name when creating the custom attribute: */ #if defined(WITH_CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF_ATTRIBUTE) #define CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF(typename) \ __attribute__((cpychecker_type_object_for_typedef(typename))) #else /* This handles the case where we're compiling with a "vanilla" compiler that doesn't supply this attribute: */ #define CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF(typename) #endif /* The checker automatically defines this preprocessor name when creating the custom attribute: */ #if defined(WITH_CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION_ATTRIBUTE) #define CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION \ __attribute__((cpychecker_negative_result_sets_exception)) #else #define CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION #endif static PyTypeObject ACL_Type CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF("ACL_Object"); static PyObject* ACL_applyto(PyObject* obj, PyObject* args); static PyObject* ACL_valid(PyObject* obj, PyObject* args); #ifdef HAVE_ACL_COPY_EXT static PyObject* ACL_get_state(PyObject *obj, PyObject* args); static PyObject* ACL_set_state(PyObject *obj, PyObject* args); #endif #ifdef HAVE_LEVEL2 static PyTypeObject Entry_Type CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF("Entry_Object"); static PyTypeObject Permset_Type CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF("Permset_Object"); static PyObject* Permset_new(PyTypeObject* type, PyObject* args, PyObject *keywds); #endif static acl_perm_t holder_ACL_EXECUTE = ACL_EXECUTE; static acl_perm_t holder_ACL_READ = ACL_READ; static acl_perm_t holder_ACL_WRITE = ACL_WRITE; typedef struct { PyObject_HEAD acl_t acl; #ifdef HAVE_LEVEL2 int entry_id; #endif } ACL_Object; #ifdef HAVE_LEVEL2 typedef struct { PyObject_HEAD PyObject *parent_acl; /* The parent acl, so it won't run out on us */ acl_entry_t entry; } Entry_Object; typedef struct { PyObject_HEAD PyObject *parent_entry; /* The parent entry, so it won't run out on us */ acl_permset_t permset; } Permset_Object; #endif /* Creation of a new ACL instance */ static PyObject* ACL_new(PyTypeObject* type, PyObject* args, PyObject *keywds) { PyObject* newacl; ACL_Object *acl; newacl = type->tp_alloc(type, 0); if(newacl == NULL) { return NULL; } acl = (ACL_Object*) newacl; acl->acl = acl_init(0); if (acl->acl == NULL) { /* LCOV_EXCL_START */ PyErr_SetFromErrno(PyExc_IOError); Py_DECREF(newacl); return NULL; /* LCOV_EXCL_STOP */ } #ifdef HAVE_LEVEL2 acl->entry_id = ACL_FIRST_ENTRY; #endif return newacl; } /* Initialization of a new ACL instance */ static int ACL_init(PyObject* obj, PyObject* args, PyObject *keywds) { ACL_Object* self = (ACL_Object*) obj; static char *kwlist[] = { "file", "fd", "text", "acl", "filedef", #ifdef HAVE_LINUX "mode", #endif #ifdef HAVE_ACL_COPY_EXT "data", #endif NULL }; char *format = "|O&OsO!O&" #ifdef HAVE_LINUX "i" #endif #ifdef HAVE_ACL_COPY_EXT "y#" #endif ; acl_t new = NULL; #ifdef HAVE_LINUX int mode = -1; #endif PyObject *file = NULL; PyObject *filedef = NULL; char *text = NULL; PyObject *fd = NULL; ACL_Object* thesrc = NULL; #ifdef HAVE_ACL_COPY_EXT const void *buf = NULL; Py_ssize_t bufsize; #endif int set_err = 0; if(!PyTuple_Check(args) || PyTuple_Size(args) != 0 || (keywds != NULL && PyDict_Check(keywds) && PyDict_Size(keywds) > 1)) { PyErr_SetString(PyExc_ValueError, "a max of one keyword argument" " must be passed"); return -1; } if(!PyArg_ParseTupleAndKeywords(args, keywds, format, kwlist, PyUnicode_FSConverter, &file, &fd, &text, &ACL_Type, &thesrc, PyUnicode_FSConverter, &filedef #ifdef HAVE_LINUX , &mode #endif #ifdef HAVE_ACL_COPY_EXT , &buf, &bufsize #endif )) return -1; if(file != NULL) { char *path = PyBytes_AS_STRING(file); new = acl_get_file(path, ACL_TYPE_ACCESS); // Set custom exception on this failure path which includes // the filename. if (new == NULL) { PyErr_SetFromErrnoWithFilename(PyExc_IOError, path); set_err = 1; } Py_DECREF(file); } else if(text != NULL) new = acl_from_text(text); else if(fd != NULL) { int fdval; if ((fdval = PyObject_AsFileDescriptor(fd)) != -1) { new = acl_get_fd(fdval); } } else if(thesrc != NULL) new = acl_dup(thesrc->acl); else if(filedef != NULL) { char *path = PyBytes_AS_STRING(filedef); new = acl_get_file(path, ACL_TYPE_DEFAULT); // Set custom exception on this failure path which includes // the filename. if (new == NULL) { PyErr_SetFromErrnoWithFilename(PyExc_IOError, path); set_err = 1; } Py_DECREF(filedef); } #ifdef HAVE_LINUX else if(mode != -1) new = acl_from_mode(mode); #endif #ifdef HAVE_ACL_COPY_EXT else if(buf != NULL) { new = acl_copy_int(buf); } #endif else new = acl_init(0); if(new == NULL) { if (!set_err) { PyErr_SetFromErrno(PyExc_IOError); } return -1; } /* Free the old acl_t without checking for error, we don't * care right now */ if(self->acl != NULL) acl_free(self->acl); self->acl = new; return 0; } /* Standard type functions */ static void ACL_dealloc(PyObject* obj) { ACL_Object *self = (ACL_Object*) obj; PyObject *err_type, *err_value, *err_traceback; PyErr_Fetch(&err_type, &err_value, &err_traceback); if(self->acl != NULL && acl_free(self->acl) != 0) PyErr_WriteUnraisable(obj); /* LCOV_EXCL_LINE */ PyErr_Restore(err_type, err_value, err_traceback); Py_TYPE(obj)->tp_free(obj); } /* Converts the acl to a text format */ static PyObject* ACL_str(PyObject *obj) { char *text; ACL_Object *self = (ACL_Object*) obj; PyObject *ret; text = acl_to_text(self->acl, NULL); if(text == NULL) { /* LCOV_EXCL_START */ return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_STOP */ } ret = PyUnicode_FromString(text); if(acl_free(text) != 0) { /* LCOV_EXCL_START */ Py_XDECREF(ret); return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_STOP */ } return ret; } #ifdef HAVE_LINUX static char __to_any_text_doc__[] = "to_any_text([prefix='', separator='n', options=0])\n" "Convert the ACL to a custom text format.\n" "\n" "This method encapsulates the ``acl_to_any_text()`` function.\n" "It allows a customized text format to be generated for the ACL. See\n" ":manpage:`acl_to_any_text(3)` for more details.\n" "\n" ":param string prefix: if given, this string will be pre-pended to\n" " all lines\n" ":param string separator: a single character (defaults to '\\n'); this will" " be used to separate the entries in the ACL\n" ":param options: a bitwise combination of:\n\n" " - :py:data:`TEXT_ABBREVIATE`: use 'u' instead of 'user', 'g' \n" " instead of 'group', etc.\n" " - :py:data:`TEXT_NUMERIC_IDS`: User and group IDs are included as\n" " decimal numbers instead of names\n" " - :py:data:`TEXT_SOME_EFFECTIVE`: Include comments denoting the\n" " effective permissions when some are masked\n" " - :py:data:`TEXT_ALL_EFFECTIVE`: Include comments after all ACL\n" " entries affected by an ACL_MASK entry\n" " - :py:data:`TEXT_SMART_INDENT`: Used in combination with the\n" " _EFFECTIVE options, this will ensure that comments are aligned\n" " to the fourth tab position (assuming one tab equals eight spaces)\n" ":rtype: string\n" ; /* Converts the acl to a custom text format */ static PyObject* ACL_to_any_text(PyObject *obj, PyObject *args, PyObject *kwds) { char *text; ACL_Object *self = (ACL_Object*) obj; PyObject *ret; const char *arg_prefix = NULL; char arg_separator = '\n'; int arg_options = 0; static char *kwlist[] = {"prefix", "separator", "options", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|sci", kwlist, &arg_prefix, &arg_separator, &arg_options)) return NULL; text = acl_to_any_text(self->acl, arg_prefix, arg_separator, arg_options); if(text == NULL) { return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ } ret = PyBytes_FromString(text); if(acl_free(text) != 0) { /* LCOV_EXCL_START */ Py_XDECREF(ret); return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_STOP */ } return ret; } static char __check_doc__[] = "Check the ACL validity.\n" "\n" "This is a non-portable, Linux specific extension that allow more\n" "information to be retrieved in case an ACL is not valid than via the\n" ":py:func:`valid` method.\n" "\n" "This method will return either False (the ACL is valid), or a tuple\n" "with two elements. The first element is one of the following\n" "constants:\n\n" " - :py:data:`ACL_MULTI_ERROR`: The ACL contains multiple entries that\n" " have a tag type that may occur at most once\n" " - :py:data:`ACL_DUPLICATE_ERROR`: The ACL contains multiple \n" " :py:data:`ACL_USER` or :py:data:`ACL_GROUP` entries with the\n" " same ID\n" " - :py:data:`ACL_MISS_ERROR`: A required entry is missing\n" " - :py:data:`ACL_ENTRY_ERROR`: The ACL contains an invalid entry\n" " tag type\n" "\n" "The second element of the tuple is the index of the entry that is\n" "invalid (in the same order as by iterating over the ACL entry)\n" ; /* The acl_check method */ static PyObject* ACL_check(PyObject* obj, PyObject* args) { ACL_Object *self = (ACL_Object*) obj; int result; int eindex; if((result = acl_check(self->acl, &eindex)) == -1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ if(result == 0) { Py_RETURN_FALSE; } return Py_BuildValue("(ii)", result, eindex); } /* Implementation of the rich compare for ACLs */ static PyObject* ACL_richcompare(PyObject* o1, PyObject* o2, int op) { ACL_Object *acl1, *acl2; int n; PyObject *ret; if(!PyObject_IsInstance(o2, (PyObject*)&ACL_Type)) { if(op == Py_EQ) Py_RETURN_FALSE; if(op == Py_NE) Py_RETURN_TRUE; PyErr_SetString(PyExc_TypeError, "can only compare to an ACL"); return NULL; } acl1 = (ACL_Object*)o1; acl2 = (ACL_Object*)o2; if((n=acl_cmp(acl1->acl, acl2->acl))==-1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ switch(op) { case Py_EQ: ret = n == 0 ? Py_True : Py_False; break; case Py_NE: ret = n == 1 ? Py_True : Py_False; break; default: PyErr_SetString(PyExc_TypeError, "ACLs are not orderable"); return NULL; } Py_INCREF(ret); return ret; } static char __equiv_mode_doc__[] = "Return the octal mode the ACL is equivalent to.\n" "\n" "This is a non-portable, Linux specific extension that checks\n" "if the ACL is a basic ACL and returns the corresponding mode.\n" "\n" ":rtype: integer\n" ":raise IOError: An IOerror exception will be raised if the ACL is\n" " an extended ACL.\n" ; /* The acl_equiv_mode method */ static PyObject* ACL_equiv_mode(PyObject* obj, PyObject* args) { ACL_Object *self = (ACL_Object*) obj; mode_t mode; if(acl_equiv_mode(self->acl, &mode) == -1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ return PyLong_FromLong(mode); } #endif /* Custom methods */ static char __applyto_doc__[] = "applyto(item[, flag=ACL_TYPE_ACCESS])\n" "Apply the ACL to a file or filehandle.\n" "\n" ":param item: either a filename or a file-like object or an integer;\n" " this represents the filesystem object on which to act\n" ":param flag: optional flag representing the type of ACL to set, either\n" " :py:data:`ACL_TYPE_ACCESS` (default) or :py:data:`ACL_TYPE_DEFAULT`\n" ; /* Applies the ACL to a file */ static PyObject* ACL_applyto(PyObject* obj, PyObject* args) { ACL_Object *self = (ACL_Object*) obj; PyObject *target, *tmp; acl_type_t type = ACL_TYPE_ACCESS; int nret; int fd; if (!PyArg_ParseTuple(args, "O|I", &target, &type)) return NULL; if ((fd = PyObject_AsFileDescriptor(target)) != -1) { if((nret = acl_set_fd(fd, self->acl)) == -1) { PyErr_SetFromErrno(PyExc_IOError); } } else { // PyObject_AsFileDescriptor sets an error when failing, so clear // it such that further code works; some method lookups fail if an // error already occured when called, which breaks at least // PyOS_FSPath (called by FSConverter). PyErr_Clear(); if(PyUnicode_FSConverter(target, &tmp)) { char *filename = PyBytes_AS_STRING(tmp); if ((nret = acl_set_file(filename, type, self->acl)) == -1) { PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename); } Py_DECREF(tmp); } else { nret = -1; } } if (nret < 0) { return NULL; } else { Py_RETURN_NONE; } } static char __valid_doc__[] = "Test the ACL for validity.\n" "\n" "This method tests the ACL to see if it is a valid ACL\n" "in terms of the file-system. More precisely, it checks that:\n" "\n" "The ACL contains exactly one entry with each of the\n" ":py:data:`ACL_USER_OBJ`, :py:data:`ACL_GROUP_OBJ`, and \n" ":py:data:`ACL_OTHER` tag types. Entries\n" "with :py:data:`ACL_USER` and :py:data:`ACL_GROUP` tag types may\n" "appear zero or more\n" "times in an ACL. An ACL that contains entries of :py:data:`ACL_USER` or\n" ":py:data:`ACL_GROUP` tag types must contain exactly one entry of the \n" ":py:data:`ACL_MASK` tag type. If an ACL contains no entries of\n" ":py:data:`ACL_USER` or :py:data:`ACL_GROUP` tag types, the\n" ":py:data:`ACL_MASK` entry is optional.\n" "\n" "All user ID qualifiers must be unique among all entries of\n" "the :py:data:`ACL_USER` tag type, and all group IDs must be unique\n" "among all entries of :py:data:`ACL_GROUP` tag type.\n" "\n" "The method will return 1 for a valid ACL and 0 for an invalid one.\n" "This has been chosen because the specification for\n" ":manpage:`acl_valid(3)`\n" "in the POSIX.1e standard documents only one possible value for errno\n" "in case of an invalid ACL, so we can't differentiate between\n" "classes of errors. Other suggestions are welcome.\n" "\n" ":return: 0 or 1\n" ":rtype: integer\n" ; /* Checks the ACL for validity */ static PyObject* ACL_valid(PyObject* obj, PyObject* args) { ACL_Object *self = (ACL_Object*) obj; if(acl_valid(self->acl) == -1) { Py_RETURN_FALSE; } else { Py_RETURN_TRUE; } } #ifdef HAVE_ACL_COPY_EXT static PyObject* ACL_get_state(PyObject *obj, PyObject* args) { ACL_Object *self = (ACL_Object*) obj; PyObject *ret; ssize_t size, nsize; char *buf; size = acl_size(self->acl); if(size == -1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ if((ret = PyBytes_FromStringAndSize(NULL, size)) == NULL) return NULL; buf = PyBytes_AsString(ret); if((nsize = acl_copy_ext(buf, self->acl, size)) == -1) { /* LCOV_EXCL_START */ Py_DECREF(ret); return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_STOP */ } return ret; } static PyObject* ACL_set_state(PyObject *obj, PyObject* args) { ACL_Object *self = (ACL_Object*) obj; const void *buf; Py_ssize_t bufsize; acl_t ptr; /* Parse the argument */ if (!PyArg_ParseTuple(args, "y#", &buf, &bufsize)) return NULL; /* Try to import the external representation */ if((ptr = acl_copy_int(buf)) == NULL) return PyErr_SetFromErrno(PyExc_IOError); if(self->acl != NULL) { /* Ignore errors in freeing the previous acl. We already allocated the new acl, and the state of the previous one is suspect if freeing failed (in Linux's libacl, deallocating a valid ACL can't actually happen, so this path is unlikely. */ acl_free(self->acl); /* LCOV_EXCL_LINE */ } self->acl = ptr; Py_RETURN_NONE; } #endif #ifdef HAVE_LEVEL2 /* tp_iter for the ACL type; since it can be iterated only * destructively, the type is its iterator */ static PyObject* ACL_iter(PyObject *obj) { ACL_Object *self = (ACL_Object*)obj; self->entry_id = ACL_FIRST_ENTRY; Py_INCREF(obj); return obj; } /* the tp_iternext function for the ACL type */ static PyObject* ACL_iternext(PyObject *obj) { ACL_Object *self = (ACL_Object*)obj; acl_entry_t the_entry_t; Entry_Object *the_entry_obj; int nerr; nerr = acl_get_entry(self->acl, self->entry_id, &the_entry_t); self->entry_id = ACL_NEXT_ENTRY; if(nerr == -1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ else if(nerr == 0) { /* Docs says this is not needed */ /*PyErr_SetObject(PyExc_StopIteration, Py_None);*/ return NULL; } the_entry_obj = (Entry_Object*) PyType_GenericNew(&Entry_Type, NULL, NULL); if(the_entry_obj == NULL) return NULL; the_entry_obj->entry = the_entry_t; the_entry_obj->parent_acl = obj; Py_INCREF(obj); /* For the reference we have in entry->parent */ return (PyObject*)the_entry_obj; } static char __ACL_delete_entry_doc__[] = "delete_entry(entry)\n" "Deletes an entry from the ACL.\n" "\n" ".. note:: Only available with level 2.\n" "\n" ":param entry: the Entry object which should be deleted; note that after\n" " this function is called, that object is unusable any longer\n" " and should be deleted\n" ; /* Deletes an entry from the ACL */ static PyObject* ACL_delete_entry(PyObject *obj, PyObject *args) { ACL_Object *self = (ACL_Object*)obj; Entry_Object *e; if (!PyArg_ParseTuple(args, "O!", &Entry_Type, &e)) return NULL; if (e->parent_acl != obj) { PyErr_SetString(PyExc_ValueError, "Can't remove un-owned entry"); return NULL; } if(acl_delete_entry(self->acl, e->entry) == -1) return PyErr_SetFromErrno(PyExc_IOError); Py_RETURN_NONE; } static char __ACL_calc_mask_doc__[] = "Compute the file group class mask.\n" "\n" "The calc_mask() method calculates and sets the permissions \n" "associated with the :py:data:`ACL_MASK` Entry of the ACL.\n" "The value of the new permissions is the union of the permissions \n" "granted by all entries of tag type :py:data:`ACL_GROUP`, \n" ":py:data:`ACL_GROUP_OBJ`, or \n" ":py:data:`ACL_USER`. If the ACL already contains an :py:data:`ACL_MASK`\n" "entry, its \n" "permissions are overwritten; if it does not contain an \n" ":py:data:`ACL_MASK` Entry, one is added.\n" "\n" "The order of existing entries in the ACL is undefined after this \n" "function.\n" ; /* Updates the mask entry in the ACL */ static PyObject* ACL_calc_mask(PyObject *obj, PyObject *args) { ACL_Object *self = (ACL_Object*)obj; if(acl_calc_mask(&self->acl) == -1) return PyErr_SetFromErrno(PyExc_IOError); Py_RETURN_NONE; } static char __ACL_append_doc__[] = "append([entry])\n" "Append a new Entry to the ACL and return it.\n" "\n" "This is a convenience function to create a new Entry \n" "and append it to the ACL.\n" "If a parameter of type Entry instance is given, the \n" "entry will be a copy of that one (as if copied with \n" ":py:func:`Entry.copy`), otherwise, the new entry will be empty.\n" "\n" ":rtype: :py:class:`Entry`\n" ":returns: the newly created entry\n" ; /* Convenience method to create a new Entry */ static PyObject* ACL_append(PyObject *obj, PyObject *args) { Entry_Object* newentry; Entry_Object* oldentry = NULL; int nret; if (!PyArg_ParseTuple(args, "|O!", &Entry_Type, &oldentry)) { return NULL; } PyObject *new_arglist = Py_BuildValue("(O)", obj); if (new_arglist == NULL) { return NULL; } newentry = (Entry_Object*) PyObject_CallObject((PyObject*)&Entry_Type, new_arglist); Py_DECREF(new_arglist); if(newentry == NULL) { return NULL; } if(oldentry != NULL) { nret = acl_copy_entry(newentry->entry, oldentry->entry); if(nret == -1) { /* LCOV_EXCL_START */ Py_DECREF(newentry); return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_STOP */ } } return (PyObject*)newentry; } /***** Entry type *****/ typedef struct { acl_tag_t tag; union { uid_t uid; gid_t gid; }; } tag_qual; /* Pre-declaring the function is more friendly to cpychecker, sigh. */ static int get_tag_qualifier(acl_entry_t entry, tag_qual *tq) CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; /* Helper function to get the tag and qualifier of an Entry at the same time. This is "needed" because the acl_get_qualifier function returns a pointer to different types, based on the tag value, and thus it's not straightforward to get the right type. It sets a Python exception if an error occurs, and returns -1 in this case. If successful, the tag is set to the tag type, the qualifier (if any) to either the uid or the gid entry in the tag_qual structure, and the return value is 0. */ static int get_tag_qualifier(acl_entry_t entry, tag_qual *tq) { void *p; if(acl_get_tag_type(entry, &tq->tag) == -1) { /* LCOV_EXCL_START */ PyErr_SetFromErrno(PyExc_IOError); return -1; /* LCOV_EXCL_STOP */ } if (tq->tag == ACL_USER || tq->tag == ACL_GROUP) { if((p = acl_get_qualifier(entry)) == NULL) { /* LCOV_EXCL_START */ PyErr_SetFromErrno(PyExc_IOError); return -1; /* LCOV_EXCL_STOP */ } if (tq->tag == ACL_USER) { tq->uid = *(uid_t*)p; } else { tq->gid = *(gid_t*)p; } acl_free(p); } return 0; } #define ENTRY_SET_CHECK(self, attr, value) \ if (value == NULL) { \ PyErr_SetString(PyExc_TypeError, \ attr " deletion is not supported"); \ return -1; \ } /* Creation of a new Entry instance */ static PyObject* Entry_new(PyTypeObject* type, PyObject* args, PyObject *keywds) { PyObject* newentry; Entry_Object* entry; ACL_Object* parent = NULL; if (!PyArg_ParseTuple(args, "O!", &ACL_Type, &parent)) return NULL; newentry = PyType_GenericNew(type, args, keywds); if(newentry == NULL) { return NULL; } entry = (Entry_Object*)newentry; if(acl_create_entry(&parent->acl, &entry->entry) == -1) { /* LCOV_EXCL_START */ PyErr_SetFromErrno(PyExc_IOError); Py_DECREF(newentry); return NULL; /* LCOV_EXCL_STOP */ } Py_INCREF(parent); entry->parent_acl = (PyObject*)parent; return newentry; } /* Initialization of a new Entry instance */ static int Entry_init(PyObject* obj, PyObject* args, PyObject *keywds) { Entry_Object* self = (Entry_Object*) obj; ACL_Object* parent = NULL; if (!PyArg_ParseTuple(args, "O!", &ACL_Type, &parent)) return -1; if ((PyObject*)parent != self->parent_acl) { PyErr_SetString(PyExc_ValueError, "Can't reinitialize with a different parent"); return -1; } return 0; } /* Free the Entry instance */ static void Entry_dealloc(PyObject* obj) { Entry_Object *self = (Entry_Object*) obj; PyObject *err_type, *err_value, *err_traceback; PyErr_Fetch(&err_type, &err_value, &err_traceback); if(self->parent_acl != NULL) { Py_DECREF(self->parent_acl); self->parent_acl = NULL; } PyErr_Restore(err_type, err_value, err_traceback); Py_TYPE(obj)->tp_free(obj); } /* Converts the entry to a text format */ static PyObject* Entry_str(PyObject *obj) { PyObject *format, *kind; Entry_Object *self = (Entry_Object*) obj; tag_qual tq; if(get_tag_qualifier(self->entry, &tq) < 0) { return NULL; } format = PyUnicode_FromString("ACL entry for "); if(format == NULL) return NULL; switch(tq.tag) { case ACL_UNDEFINED_TAG: kind = PyUnicode_FromString("undefined type"); break; case ACL_USER_OBJ: kind = PyUnicode_FromString("the owner"); break; case ACL_GROUP_OBJ: kind = PyUnicode_FromString("the group"); break; case ACL_OTHER: kind = PyUnicode_FromString("the others"); break; case ACL_USER: /* FIXME: here and in the group case, we're formatting with unsigned, because there's no way to automatically determine the signed-ness of the types; on Linux(glibc) they're unsigned, so we'll go along with that */ kind = PyUnicode_FromFormat("user with uid %u", tq.uid); break; case ACL_GROUP: kind = PyUnicode_FromFormat("group with gid %u", tq.gid); break; case ACL_MASK: kind = PyUnicode_FromString("the mask"); break; default: /* LCOV_EXCL_START */ kind = PyUnicode_FromString("UNKNOWN_TAG_TYPE!"); break; /* LCOV_EXCL_STOP */ } if (kind == NULL) { /* LCOV_EXCL_START */ Py_DECREF(format); return NULL; /* LCOV_EXCL_STOP */ } PyObject *ret = PyUnicode_Concat(format, kind); Py_DECREF(format); Py_DECREF(kind); return ret; } /* Sets the tag type of the entry */ static int Entry_set_tag_type(PyObject* obj, PyObject* value, void* arg) { Entry_Object *self = (Entry_Object*) obj; ENTRY_SET_CHECK(self, "tag type", value); if(!PyLong_Check(value)) { PyErr_SetString(PyExc_TypeError, "tag type must be integer"); return -1; } if(acl_set_tag_type(self->entry, (acl_tag_t)PyLong_AsLong(value)) == -1) { PyErr_SetFromErrno(PyExc_IOError); return -1; } return 0; } /* Returns the tag type of the entry */ static PyObject* Entry_get_tag_type(PyObject *obj, void* arg) { Entry_Object *self = (Entry_Object*) obj; acl_tag_t value; if(acl_get_tag_type(self->entry, &value) == -1) { /* LCOV_EXCL_START */ PyErr_SetFromErrno(PyExc_IOError); return NULL; /* LCOV_EXCL_STOP */ } return PyLong_FromLong(value); } /* Sets the qualifier (either uid_t or gid_t) for the entry, * usable only if the tag type if ACL_USER or ACL_GROUP */ static int Entry_set_qualifier(PyObject* obj, PyObject* value, void* arg) { Entry_Object *self = (Entry_Object*) obj; unsigned long uidgid; uid_t uid; gid_t gid; void *p; acl_tag_t tag; ENTRY_SET_CHECK(self, "qualifier", value); if(!PyLong_Check(value)) { PyErr_SetString(PyExc_TypeError, "qualifier must be integer"); return -1; } /* This is the negative value check, and larger than long check. If uid_t/gid_t are long-sized, this is enough to check for both over and underflow. */ if((uidgid = PyLong_AsUnsignedLong(value)) == (unsigned long) -1) { if(PyErr_Occurred() != NULL) { return -1; } } /* Due to how acl_set_qualifier takes its argument, we have to do this ugly dance with two variables and a pointer that will point to one of them. */ if(acl_get_tag_type(self->entry, &tag) == -1) { /* LCOV_EXCL_START */ PyErr_SetFromErrno(PyExc_IOError); return -1; /* LCOV_EXCL_STOP */ } uid = uidgid; gid = uidgid; /* This is an extra overflow check, in case uid_t/gid_t are int-sized (and int size smaller than long size). */ switch(tag) { case ACL_USER: if((unsigned long)uid != uidgid) { PyErr_SetString(PyExc_OverflowError, "Can't assign given qualifier"); return -1; } else { p = &uid; } break; case ACL_GROUP: if((unsigned long)gid != uidgid) { PyErr_SetString(PyExc_OverflowError, "Can't assign given qualifier"); return -1; } else { p = &gid; } break; default: PyErr_SetString(PyExc_TypeError, "Can only set qualifiers on ACL_USER or ACL_GROUP entries"); return -1; } if(acl_set_qualifier(self->entry, p) == -1) { /* LCOV_EXCL_START */ PyErr_SetFromErrno(PyExc_IOError); return -1; /* LCOV_EXCL_STOP */ } return 0; } /* Returns the qualifier of the entry */ static PyObject* Entry_get_qualifier(PyObject *obj, void* arg) { Entry_Object *self = (Entry_Object*) obj; unsigned long value; tag_qual tq; if(get_tag_qualifier(self->entry, &tq) < 0) { return NULL; } if (tq.tag == ACL_USER) { value = tq.uid; } else if (tq.tag == ACL_GROUP) { value = tq.gid; } else { PyErr_SetString(PyExc_TypeError, "Given entry doesn't have an user or" " group tag"); return NULL; } return PyLong_FromUnsignedLong(value); } /* Returns the parent ACL of the entry */ static PyObject* Entry_get_parent(PyObject *obj, void* arg) { Entry_Object *self = (Entry_Object*) obj; Py_INCREF(self->parent_acl); return self->parent_acl; } /* Returns the a new Permset representing the permset of the entry * FIXME: Should return a new reference to the same object, which * should be created at init time! */ static PyObject* Entry_get_permset(PyObject *obj, void* arg) { PyObject *p; PyObject *perm_arglist = Py_BuildValue("(O)", obj); if (perm_arglist == NULL) { return NULL; } p = PyObject_CallObject((PyObject*)&Permset_Type, perm_arglist); Py_DECREF(perm_arglist); return p; } /* Sets the permset of the entry to the passed Permset */ static int Entry_set_permset(PyObject* obj, PyObject* value, void* arg) { Entry_Object *self = (Entry_Object*)obj; Permset_Object *p; ENTRY_SET_CHECK(self, "permset", value); if(!PyObject_IsInstance(value, (PyObject*)&Permset_Type)) { PyErr_SetString(PyExc_TypeError, "argument 1 must be posix1e.Permset"); return -1; } p = (Permset_Object*)value; if(acl_set_permset(self->entry, p->permset) == -1) { /* LCOV_EXCL_START */ PyErr_SetFromErrno(PyExc_IOError); return -1; /* LCOV_EXCL_STOP */ } return 0; } static char __Entry_copy_doc__[] = "copy(src)\n" "Copies an ACL entry.\n" "\n" "This method sets all the parameters to those of another\n" "entry (either of the same ACL or belonging to another ACL).\n" "\n" ":param Entry src: instance of type Entry\n" ; /* Sets all the entry parameters to another entry */ static PyObject* Entry_copy(PyObject *obj, PyObject *args) { Entry_Object *self = (Entry_Object*)obj; Entry_Object *other; if(!PyArg_ParseTuple(args, "O!", &Entry_Type, &other)) return NULL; if(acl_copy_entry(self->entry, other->entry) == -1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ Py_RETURN_NONE; } /**** Permset type *****/ /* Creation of a new Permset instance */ static PyObject* Permset_new(PyTypeObject* type, PyObject* args, PyObject *keywds) { PyObject* newpermset; Permset_Object* permset; Entry_Object* parent = NULL; if (!PyArg_ParseTuple(args, "O!", &Entry_Type, &parent)) { return NULL; } newpermset = PyType_GenericNew(type, args, keywds); if(newpermset == NULL) { return NULL; } permset = (Permset_Object*)newpermset; if(acl_get_permset(parent->entry, &permset->permset) == -1) { PyErr_SetFromErrno(PyExc_IOError); Py_DECREF(newpermset); return NULL; } permset->parent_entry = (PyObject*)parent; Py_INCREF(parent); return newpermset; } /* Initialization of a new Permset instance */ static int Permset_init(PyObject* obj, PyObject* args, PyObject *keywds) { Permset_Object* self = (Permset_Object*) obj; Entry_Object* parent = NULL; if (!PyArg_ParseTuple(args, "O!", &Entry_Type, &parent)) return -1; if ((PyObject*)parent != self->parent_entry) { PyErr_SetString(PyExc_ValueError, "Can't reinitialize with a different parent"); return -1; } return 0; } /* Free the Permset instance */ static void Permset_dealloc(PyObject* obj) { Permset_Object *self = (Permset_Object*) obj; PyObject *err_type, *err_value, *err_traceback; PyErr_Fetch(&err_type, &err_value, &err_traceback); if(self->parent_entry != NULL) { Py_DECREF(self->parent_entry); self->parent_entry = NULL; } PyErr_Restore(err_type, err_value, err_traceback); Py_TYPE(obj)->tp_free((PyObject *)obj); } /* Permset string representation */ static PyObject* Permset_str(PyObject *obj) { Permset_Object *self = (Permset_Object*) obj; char pstr[3]; pstr[0] = get_perm(self->permset, ACL_READ) ? 'r' : '-'; pstr[1] = get_perm(self->permset, ACL_WRITE) ? 'w' : '-'; pstr[2] = get_perm(self->permset, ACL_EXECUTE) ? 'x' : '-'; return PyUnicode_FromStringAndSize(pstr, 3); } static char __Permset_clear_doc__[] = "Clears all permissions from the permission set.\n" ; /* Clears all permissions from the permset */ static PyObject* Permset_clear(PyObject* obj, PyObject* args) { Permset_Object *self = (Permset_Object*) obj; if(acl_clear_perms(self->permset) == -1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ Py_RETURN_NONE; } static PyObject* Permset_get_right(PyObject *obj, void* arg) { Permset_Object *self = (Permset_Object*) obj; if(get_perm(self->permset, *(acl_perm_t*)arg)) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } } static int Permset_set_right(PyObject* obj, PyObject* value, void* arg) { Permset_Object *self = (Permset_Object*) obj; int on; int nerr; if(!PyLong_Check(value)) { PyErr_SetString(PyExc_ValueError, "invalid argument, an integer" " is expected"); return -1; } on = PyLong_AsLong(value); if(on) nerr = acl_add_perm(self->permset, *(acl_perm_t*)arg); else nerr = acl_delete_perm(self->permset, *(acl_perm_t*)arg); if(nerr == -1) { /* LCOV_EXCL_START */ PyErr_SetFromErrno(PyExc_IOError); return -1; /* LCOV_EXCL_STOP */ } return 0; } static char __Permset_add_doc__[] = "add(perm)\n" "Add a permission to the permission set.\n" "\n" "This function adds the permission contained in \n" "the argument perm to the permission set. An attempt \n" "to add a permission that is already contained in the \n" "permission set is not considered an error.\n" "\n" ":param perm: a permission (:py:data:`ACL_WRITE`, :py:data:`ACL_READ`,\n" " :py:data:`ACL_EXECUTE`, ...)\n" ":raises IOError: in case the argument is not a valid descriptor\n" ; static PyObject* Permset_add(PyObject* obj, PyObject* args) { Permset_Object *self = (Permset_Object*) obj; int right; if (!PyArg_ParseTuple(args, "i", &right)) return NULL; if(acl_add_perm(self->permset, (acl_perm_t) right) == -1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ Py_RETURN_NONE; } static char __Permset_delete_doc__[] = "delete(perm)\n" "Delete a permission from the permission set.\n" "\n" "This function deletes the permission contained in \n" "the argument perm from the permission set. An attempt \n" "to delete a permission that is not contained in the \n" "permission set is not considered an error.\n" "\n" ":param perm: a permission (:py:data:`ACL_WRITE`, :py:data:`ACL_READ`,\n" " :py:data:`ACL_EXECUTE`, ...)\n" ":raises IOError: in case the argument is not a valid descriptor\n" ; static PyObject* Permset_delete(PyObject* obj, PyObject* args) { Permset_Object *self = (Permset_Object*) obj; int right; if (!PyArg_ParseTuple(args, "i", &right)) return NULL; if(acl_delete_perm(self->permset, (acl_perm_t) right) == -1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ Py_RETURN_NONE; } static char __Permset_test_doc__[] = "test(perm)\n" "Test if a permission exists in the permission set.\n" "\n" "The test() function tests if the permission represented by\n" "the argument perm exists in the permission set.\n" "\n" ":param perm: a permission (:py:data:`ACL_WRITE`, :py:data:`ACL_READ`,\n" " :py:data:`ACL_EXECUTE`, ...)\n" ":rtype: Boolean\n" ":raises IOError: in case the argument is not a valid descriptor\n" ; static PyObject* Permset_test(PyObject* obj, PyObject* args) { Permset_Object *self = (Permset_Object*) obj; int right; int ret; if (!PyArg_ParseTuple(args, "i", &right)) return NULL; ret = get_perm(self->permset, (acl_perm_t) right); if(ret == -1) return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ if(ret) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } } #endif static char __ACL_Type_doc__[] = "Type which represents a POSIX ACL\n" "\n" ".. note:: only one keyword parameter should be provided\n" "\n" ":param string/bytes/path-like file: creates an ACL representing\n" " the access ACL of the specified file or directory.\n" ":param string/bytes/path-like filedef: creates an ACL representing\n" " the default ACL of the given directory.\n" ":param int/iostream fd: creates an ACL representing\n" " the access ACL of the given file descriptor.\n" ":param string text: creates an ACL from a \n" " textual description; note the ACL must be valid, which\n" " means including a mask for extended ACLs, similar to\n" " ``setfacl --no-mask``\n" ":param ACL acl: creates a copy of an existing ACL instance.\n" ":param int mode: creates an ACL from a numeric mode\n" " (e.g. ``mode=0644``); this is valid only when the C library\n" " provides the ``acl_from_mode call``, and\n" " note that no validation is done on the given value.\n" ":param bytes data: creates an ACL from a serialised form,\n" " as provided by calling ``__getstate__()`` on an existing ACL\n" "\n" "If no parameters are passed, an empty ACL will be created; this\n" "makes sense only when your OS supports ACL modification\n" "(i.e. it implements full POSIX.1e support), otherwise the ACL won't\n" "be useful.\n" ; /* ACL type methods */ static PyMethodDef ACL_methods[] = { {"applyto", ACL_applyto, METH_VARARGS, __applyto_doc__}, {"valid", ACL_valid, METH_NOARGS, __valid_doc__}, #ifdef HAVE_LINUX {"to_any_text", (PyCFunction)ACL_to_any_text, METH_VARARGS | METH_KEYWORDS, __to_any_text_doc__}, {"check", ACL_check, METH_NOARGS, __check_doc__}, {"equiv_mode", ACL_equiv_mode, METH_NOARGS, __equiv_mode_doc__}, #endif #ifdef HAVE_ACL_COPY_EXT {"__getstate__", ACL_get_state, METH_NOARGS, "Dumps the ACL to an external format."}, {"__setstate__", ACL_set_state, METH_VARARGS, "Loads the ACL from an external format."}, #endif #ifdef HAVE_LEVEL2 {"delete_entry", ACL_delete_entry, METH_VARARGS, __ACL_delete_entry_doc__}, {"calc_mask", ACL_calc_mask, METH_NOARGS, __ACL_calc_mask_doc__}, {"append", ACL_append, METH_VARARGS, __ACL_append_doc__}, #endif {NULL, NULL, 0, NULL} }; /* The definition of the ACL Type */ static PyTypeObject ACL_Type = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "posix1e.ACL", .tp_basicsize = sizeof(ACL_Object), .tp_itemsize = 0, .tp_dealloc = ACL_dealloc, .tp_str = ACL_str, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = __ACL_Type_doc__, #ifdef HAVE_LINUX .tp_richcompare = ACL_richcompare, #endif #ifdef HAVE_LEVEL2 .tp_iter = ACL_iter, .tp_iternext = ACL_iternext, #endif .tp_methods = ACL_methods, .tp_init = ACL_init, .tp_new = ACL_new, }; #ifdef HAVE_LEVEL2 /* Entry type methods */ static PyMethodDef Entry_methods[] = { {"copy", Entry_copy, METH_VARARGS, __Entry_copy_doc__}, {NULL, NULL, 0, NULL} }; static char __Entry_tagtype_doc__[] = "The tag type of the current entry\n" "\n" "This is one of:\n" " - :py:data:`ACL_UNDEFINED_TAG`\n" " - :py:data:`ACL_USER_OBJ`\n" " - :py:data:`ACL_USER`\n" " - :py:data:`ACL_GROUP_OBJ`\n" " - :py:data:`ACL_GROUP`\n" " - :py:data:`ACL_MASK`\n" " - :py:data:`ACL_OTHER`\n" ; static char __Entry_qualifier_doc__[] = "The qualifier of the current entry\n" "\n" "If the tag type is :py:data:`ACL_USER`, this should be a user id.\n" "If the tag type if :py:data:`ACL_GROUP`, this should be a group id.\n" "Else it doesn't matter.\n" ; static char __Entry_parent_doc__[] = "The parent ACL of this entry\n" ; static char __Entry_permset_doc__[] = "The permission set of this ACL entry\n" ; /* Entry getset */ static PyGetSetDef Entry_getsets[] = { {"tag_type", Entry_get_tag_type, Entry_set_tag_type, __Entry_tagtype_doc__}, {"qualifier", Entry_get_qualifier, Entry_set_qualifier, __Entry_qualifier_doc__}, {"parent", Entry_get_parent, NULL, __Entry_parent_doc__}, {"permset", Entry_get_permset, Entry_set_permset, __Entry_permset_doc__}, {NULL} }; static char __Entry_Type_doc__[] = "Type which represents an entry in an ACL.\n" "\n" "The type exists only if the OS has full support for POSIX.1e\n" "Can be created either by:\n" "\n" " >>> e = posix1e.Entry(myACL) # this creates a new entry in the ACL\n" " >>> e = myACL.append() # another way for doing the same thing\n" "\n" "or by:\n" "\n" " >>> for entry in myACL:\n" " ... print entry\n" "\n" "Note that the Entry keeps a reference to its ACL, so even if \n" "you delete the ACL, it won't be cleaned up and will continue to \n" "exist until its Entry(ies) will be deleted.\n" ; /* The definition of the Entry Type */ static PyTypeObject Entry_Type = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "posix1e.Entry", .tp_basicsize = sizeof(Entry_Object), .tp_itemsize = 0, .tp_dealloc = Entry_dealloc, .tp_str = Entry_str, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = __Entry_Type_doc__, .tp_methods = Entry_methods, .tp_getset = Entry_getsets, .tp_init = Entry_init, .tp_new = Entry_new }; /* Permset type methods */ static PyMethodDef Permset_methods[] = { {"clear", Permset_clear, METH_NOARGS, __Permset_clear_doc__, }, {"add", Permset_add, METH_VARARGS, __Permset_add_doc__, }, {"delete", Permset_delete, METH_VARARGS, __Permset_delete_doc__, }, {"test", Permset_test, METH_VARARGS, __Permset_test_doc__, }, {NULL, NULL, 0, NULL} }; static char __Permset_execute_doc__[] = "Execute permission property\n" "\n" "This is a convenience method of retrieving and setting the execute\n" "permission in the permission set; the \n" "same effect can be achieved using the functions\n" "add(), test(), delete(), and those can take any \n" "permission defined by your platform.\n" ; static char __Permset_read_doc__[] = "Read permission property\n" "\n" "This is a convenience method of retrieving and setting the read\n" "permission in the permission set; the \n" "same effect can be achieved using the functions\n" "add(), test(), delete(), and those can take any \n" "permission defined by your platform.\n" ; static char __Permset_write_doc__[] = "Write permission property\n" "\n" "This is a convenience method of retrieving and setting the write\n" "permission in the permission set; the \n" "same effect can be achieved using the functions\n" "add(), test(), delete(), and those can take any \n" "permission defined by your platform.\n" ; /* Permset getset */ static PyGetSetDef Permset_getsets[] = { {"execute", Permset_get_right, Permset_set_right, __Permset_execute_doc__, &holder_ACL_EXECUTE}, {"read", Permset_get_right, Permset_set_right, __Permset_read_doc__, &holder_ACL_READ}, {"write", Permset_get_right, Permset_set_right, __Permset_write_doc__, &holder_ACL_WRITE}, {NULL} }; static char __Permset_Type_doc__[] = "Type which represents the permission set in an ACL entry\n" "\n" "The type exists only if the OS has full support for POSIX.1e\n" "Can be retrieved either by:\n\n" ">>> perms = myEntry.permset\n" "\n" "or by:\n\n" ">>> perms = posix1e.Permset(myEntry)\n" "\n" "Note that the Permset keeps a reference to its Entry, so even if \n" "you delete the entry, it won't be cleaned up and will continue to \n" "exist until its Permset will be deleted.\n" ; /* The definition of the Permset Type */ static PyTypeObject Permset_Type = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "posix1e.Permset", .tp_basicsize = sizeof(Permset_Object), .tp_itemsize = 0, .tp_dealloc = Permset_dealloc, .tp_str = Permset_str, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = __Permset_Type_doc__, .tp_methods = Permset_methods, .tp_getset = Permset_getsets, .tp_init = Permset_init, .tp_new = Permset_new, }; #endif /* Module methods */ static char __deletedef_doc__[] = "delete_default(path)\n" "Delete the default ACL from a directory.\n" "\n" "This function deletes the default ACL associated with\n" "a directory (the ACL which will be ANDed with the mode\n" "parameter to the open, creat functions).\n" "\n" ":param string path: the directory whose default ACL should be deleted\n" ; /* Deletes the default ACL from a directory */ static PyObject* aclmodule_delete_default(PyObject* obj, PyObject* args) { char *filename; /* Parse the arguments */ if (!PyArg_ParseTuple(args, "et", NULL, &filename)) return NULL; if(acl_delete_def_file(filename) == -1) { return PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename); } Py_RETURN_NONE; } #ifdef HAVE_LINUX static char __has_extended_doc__[] = "has_extended(item)\n" "Check if a file or file handle has an extended ACL.\n" "\n" ":param item: either a file name or a file-like object or an integer;\n" " it represents the file-system object on which to act\n" ; /* Check for extended ACL a file or fd */ static PyObject* aclmodule_has_extended(PyObject* obj, PyObject* args) { PyObject *item, *tmp; int nret; int fd; if (!PyArg_ParseTuple(args, "O", &item)) return NULL; if((fd = PyObject_AsFileDescriptor(item)) != -1) { if((nret = acl_extended_fd(fd)) == -1) { PyErr_SetFromErrno(PyExc_IOError); } } else { // PyObject_AsFileDescriptor sets an error when failing, so clear // it such that further code works; some method lookups fail if an // error already occured when called, which breaks at least // PyOS_FSPath (called by FSConverter). PyErr_Clear(); if(PyUnicode_FSConverter(item, &tmp)) { char *filename = PyBytes_AS_STRING(tmp); if ((nret = acl_extended_file(filename)) == -1) { PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename); } Py_DECREF(tmp); } else { nret = -1; } } if (nret < 0) { return NULL; } else { return PyBool_FromLong(nret); } } #endif /* The module methods */ static PyMethodDef aclmodule_methods[] = { {"delete_default", aclmodule_delete_default, METH_VARARGS, __deletedef_doc__}, #ifdef HAVE_LINUX {"has_extended", aclmodule_has_extended, METH_VARARGS, __has_extended_doc__}, #endif {NULL, NULL, 0, NULL} }; static char __posix1e_doc__[] = "POSIX.1e ACLs manipulation\n" "==========================\n" "\n" "This module provides support for manipulating POSIX.1e ACLS\n" "\n" "Depending on the operating system support for POSIX.1e, \n" "the ACL type will have more or less capabilities:\n\n" " - level 1, only basic support, you can create\n" " ACLs from files and text descriptions;\n" " once created, the type is immutable\n" " - level 2, complete support, you can alter\n" " the ACL once it is created\n" "\n" "Also, in level 2, more types are available, corresponding\n" "to acl_entry_t (the Entry type), acl_permset_t (the Permset type).\n" "\n" "The existence of level 2 support and other extensions can be\n" "checked by the constants:\n\n" " - :py:data:`HAS_ACL_ENTRY` for level 2 and the Entry/Permset classes\n" " - :py:data:`HAS_ACL_FROM_MODE` for ``ACL(mode=...)`` usage\n" " - :py:data:`HAS_ACL_CHECK` for the :py:func:`ACL.check` function\n" " - :py:data:`HAS_EXTENDED_CHECK` for the module-level\n" " :py:func:`has_extended` function\n" " - :py:data:`HAS_EQUIV_MODE` for the :py:func:`ACL.equiv_mode` method\n" " - :py:data:`HAS_COPY_EXT` for the :py:func:`ACL.__getstate__` and\n" " :py:func:`ACL.__setstate__` functions (pickle protocol)\n" "\n" "Example:\n" "\n" ">>> import posix1e\n" ">>> acl1 = posix1e.ACL(file=\"file.txt\") \n" ">>> print acl1\n" "user::rw-\n" "group::rw-\n" "other::r--\n" ">>>\n" ">>> b = posix1e.ACL(text=\"u::rx,g::-,o::-\")\n" ">>> print b\n" "user::r-x\n" "group::---\n" "other::---\n" ">>>\n" ">>> b.applyto(\"file.txt\")\n" ">>> print posix1e.ACL(file=\"file.txt\")\n" "user::r-x\n" "group::---\n" "other::---\n" ">>>\n" "\n" ".. py:data:: ACL_USER\n\n" " Denotes a specific user entry in an ACL.\n" "\n" ".. py:data:: ACL_USER_OBJ\n\n" " Denotes the user owner entry in an ACL.\n" "\n" ".. py:data:: ACL_GROUP\n\n" " Denotes the a group entry in an ACL.\n" "\n" ".. py:data:: ACL_GROUP_OBJ\n\n" " Denotes the group owner entry in an ACL.\n" "\n" ".. py:data:: ACL_OTHER\n\n" " Denotes the 'others' entry in an ACL.\n" "\n" ".. py:data:: ACL_MASK\n\n" " Denotes the mask entry in an ACL, representing the maximum\n" " access granted other users, the owner group and other groups.\n" "\n" ".. py:data:: ACL_UNDEFINED_TAG\n\n" " An undefined tag in an ACL.\n" "\n" ".. py:data:: ACL_READ\n\n" " Read permission in a permission set.\n" "\n" ".. py:data:: ACL_WRITE\n\n" " Write permission in a permission set.\n" "\n" ".. py:data:: ACL_EXECUTE\n\n" " Execute permission in a permission set.\n" "\n" ".. py:data:: HAS_ACL_ENTRY\n\n" " denotes support for level 2 and the Entry/Permset classes\n" "\n" ".. py:data:: HAS_ACL_FROM_MODE\n\n" " denotes support for building an ACL from an octal mode\n" "\n" ".. py:data:: HAS_ACL_CHECK\n\n" " denotes support for extended checks of an ACL's validity\n" "\n" ".. py:data:: HAS_EXTENDED_CHECK\n\n" " denotes support for checking whether an ACL is basic or extended\n" "\n" ".. py:data:: HAS_EQUIV_MODE\n\n" " denotes support for the equiv_mode function\n" "\n" ".. py:data:: HAS_COPY_EXT\n\n" " denotes support for __getstate__()/__setstate__() on an ACL\n" "\n" ; static struct PyModuleDef posix1emodule = { PyModuleDef_HEAD_INIT, .m_name = "posix1e", .m_doc = __posix1e_doc__, .m_size = 0, .m_methods = aclmodule_methods, }; PyMODINIT_FUNC PyInit_posix1e(void) { PyObject *m, *d; if(PyType_Ready(&ACL_Type) < 0) return NULL; #ifdef HAVE_LEVEL2 if(PyType_Ready(&Entry_Type) < 0) return NULL; if(PyType_Ready(&Permset_Type) < 0) return NULL; #endif m = PyModule_Create(&posix1emodule); if (m==NULL) return NULL; d = PyModule_GetDict(m); if (d == NULL) return NULL; Py_INCREF(&ACL_Type); if (PyDict_SetItemString(d, "ACL", (PyObject *) &ACL_Type) < 0) return NULL; /* 23.3.6 acl_type_t values */ PyModule_AddIntConstant(m, "ACL_TYPE_ACCESS", ACL_TYPE_ACCESS); PyModule_AddIntConstant(m, "ACL_TYPE_DEFAULT", ACL_TYPE_DEFAULT); #ifdef HAVE_LEVEL2 Py_INCREF(&Entry_Type); if (PyDict_SetItemString(d, "Entry", (PyObject *) &Entry_Type) < 0) return NULL; Py_INCREF(&Permset_Type); if (PyDict_SetItemString(d, "Permset", (PyObject *) &Permset_Type) < 0) return NULL; /* 23.2.2 acl_perm_t values */ PyModule_AddIntConstant(m, "ACL_READ", ACL_READ); PyModule_AddIntConstant(m, "ACL_WRITE", ACL_WRITE); PyModule_AddIntConstant(m, "ACL_EXECUTE", ACL_EXECUTE); /* 23.2.5 acl_tag_t values */ PyModule_AddIntConstant(m, "ACL_UNDEFINED_TAG", ACL_UNDEFINED_TAG); PyModule_AddIntConstant(m, "ACL_USER_OBJ", ACL_USER_OBJ); PyModule_AddIntConstant(m, "ACL_USER", ACL_USER); PyModule_AddIntConstant(m, "ACL_GROUP_OBJ", ACL_GROUP_OBJ); PyModule_AddIntConstant(m, "ACL_GROUP", ACL_GROUP); PyModule_AddIntConstant(m, "ACL_MASK", ACL_MASK); PyModule_AddIntConstant(m, "ACL_OTHER", ACL_OTHER); /* Document extended functionality via easy-to-use constants */ PyModule_AddIntConstant(m, "HAS_ACL_ENTRY", 1); #else PyModule_AddIntConstant(m, "HAS_ACL_ENTRY", 0); #endif #ifdef HAVE_LINUX /* Linux libacl specific acl_to_any_text constants */ PyModule_AddIntConstant(m, "TEXT_ABBREVIATE", TEXT_ABBREVIATE); PyModule_AddIntConstant(m, "TEXT_NUMERIC_IDS", TEXT_NUMERIC_IDS); PyModule_AddIntConstant(m, "TEXT_SOME_EFFECTIVE", TEXT_SOME_EFFECTIVE); PyModule_AddIntConstant(m, "TEXT_ALL_EFFECTIVE", TEXT_ALL_EFFECTIVE); PyModule_AddIntConstant(m, "TEXT_SMART_INDENT", TEXT_SMART_INDENT); /* Linux libacl specific acl_check constants */ PyModule_AddIntConstant(m, "ACL_MULTI_ERROR", ACL_MULTI_ERROR); PyModule_AddIntConstant(m, "ACL_DUPLICATE_ERROR", ACL_DUPLICATE_ERROR); PyModule_AddIntConstant(m, "ACL_MISS_ERROR", ACL_MISS_ERROR); PyModule_AddIntConstant(m, "ACL_ENTRY_ERROR", ACL_ENTRY_ERROR); #define LINUX_EXT_VAL 1 #else #define LINUX_EXT_VAL 0 #endif /* declare the Linux extensions */ PyModule_AddIntConstant(m, "HAS_ACL_FROM_MODE", LINUX_EXT_VAL); PyModule_AddIntConstant(m, "HAS_ACL_CHECK", LINUX_EXT_VAL); PyModule_AddIntConstant(m, "HAS_EXTENDED_CHECK", LINUX_EXT_VAL); PyModule_AddIntConstant(m, "HAS_EQUIV_MODE", LINUX_EXT_VAL); PyModule_AddIntConstant(m, "HAS_COPY_EXT", #ifdef HAVE_ACL_COPY_EXT 1 #else 0 #endif ); return m; } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740347991.9603221 pylibacl-0.7.2/doc/0000775000000000000000000000000014756715130012635 5ustar00rootroot././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347919.0 pylibacl-0.7.2/doc/conf.py0000664000000000000000000001751514756715017014151 0ustar00rootroot# -*- coding: utf-8 -*- # # pylibacl documentation build configuration file, created by # sphinx-quickstart on Sun May 13 01:05:18 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('../')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'myst_parser'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = ['.rst', '.md'] # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'pylibacl' copyright = u'2002-2009, 2012, 2014, 2015, Iustin Pop' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.7.2' # The full version, including alpha/beta/rc tags. release = '0.7.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build', 'html'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None default_domain = 'python' # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] keep_warnings = True # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True html_domain_indices = False # If false, no index is generated. #html_use_index = True html_use_index = False # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'pylibacldoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'pylibacl.tex', u'pylibacl Documentation', u'Iustin Pop', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pylibacl', u'pylibacl Documentation', [u'Iustin Pop'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'pylibacl', u'pylibacl Documentation', u'Iustin Pop', 'pylibacl', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' autodoc_member_order = 'alphabetical' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682280673.0 pylibacl-0.7.2/doc/contributing.md0000644000000000000000000000422014421310341015642 0ustar00rootroot# Contributing to pylibacl Hi, and thanks for any and all contributions! ## Bugs and patches This is a small project, so let's keep things simple: - Please file all bug reports on github (), as this allows archival and discovery by other people; - Send patches as pull requests; for larger changes, would be good to first open a bug to discuss the plans; Due to simplicity, there are no old branches being kept alive, but if it ever happens that a bug is found in older versions and there is needed to support older Python versions, it is possible to do so. ## Code standards There are no formal standards, but: - Code should be tested - this is why there's a [Codecov integration](https://app.codecov.io/gh/iustin/pylibacl/tree/main). - New functions should have good docstrings (in the C code). - New functions/constants should be listed in the documentation, see `doc/module.rst` for how to include them. - All non-trivial changes should be listed in `NEWS.md` for further inclusion in new releases documentation. Add an "unreleased" section (if one doesn't exist yet) to list the changes. ## Release process Right now, due to GPG signing, I'm doing releases and signing them manually (offline, I mean). Basically, once GitHub workflows are fine: - Bump the version in all places - use `git grep -F $OLD_VER` and update as needed. - Ensure that `setup.py` has the right Python versions listed (bit me more than once). - Update the `NEWS.md` file is up to date (contents), and use the right date. - Check that the generated documentation (`make doc`) looks right. Then run these steps: ``` $ make clean $ make distcheck # this leaves things in dist/ $ git tag -m 'Release pylibacl-0.0.1' --sign v0.0.1 $ gpg --sign -b -a dist/pylibacl-0.0.1.tar.gz $ python3 -m twine upload dist/* ``` Separately: * Upload the `dist/` contents to GitHub and tag a new release. * Upload the `dist/` contents to the old-style download area, . Hopefully one day all this can be more automated. ## Signing key The releases are currently signed by my key, see . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1336873443.0 pylibacl-0.7.2/doc/implementation.rst0000644000000000000000000000762111753610743016416 0ustar00rootrootImplementation details ====================== Functionality level ------------------- The IEEE 1003.1e draft 17 ("POSIX.1e") describes a set of 28 functions. These are grouped into three groups, based on their portability: - first group, the most portable one. All systems which claim to support POSIX.1e should implement these: acl_delete_def_file(3), acl_dup(3), acl_free(3), acl_from_text(3), acl_get_fd(3), acl_get_file(3), acl_init(3), acl_set_fd(3), acl_set_file(3), acl_to_text(3), acl_valid(3) - second group, containing the rest of the POSIX ACL functions. Systems which claim to fully implement POSIX.1e should implement these: acl_add_perm(3), acl_calc_mask(3), acl_clear_perms(3), acl_copy_entry(3), acl_copy_ext(3), acl_copy_int(3), acl_create_entry(3), acl_delete_entry(3), acl_delete_perm(3), acl_get_entry(3), acl_get_permset(3), acl_get_qualifier(3), acl_get_tag_type(3), acl_set_permset(3), acl_set_qualifier(3), acl_set_tag_type(3), acl_size(3) - third group, containing extra functions implemented by each OS. These are non-portable version. Both Linux and FreeBSD implement some extra functions. Thus we have the level of compliance. Depending on whether the system library support the second group, you get some extra methods for the ACL object. The implementation of the second group of function can be tested by checking the module-level constant HAS_ACL_ENTRY. The extra functionality available on Linux can be tested by additional HAS_* constants. Internal structure ------------------ The POSIX draft has the following stuff (correct me if I'm wrong): - an ACL is denoted by acl_t - an ACL contains many acl_entry_t, these are the individual entries in the list; they always(!) belong to an acl_t - each entry_t has a qualifier (think uid_t or gid_t), whose type is denoted by the acl_tag_t type, and an acl_permset_t - the acl_permset_t can contain acl_perm_t value (ACL_READ, ACL_WRITE, ACL_EXECUTE, ACL_ADD, ACL_DELETE, ...) - functions to manipulate all these, and functions to manipulate files Currently supported platforms ----------------------------- For any other platforms, volunteers are welcome. Linux ~~~~~ It needs kernel 2.4 or higher and the libacl library installed (with development headers, if installing from rpm). This library is available on all modern distributions. The level of compliance is level 2 (see IMPLEMENTATION), plus some extra functions; and as my development is done on Linux, I try to implement these extensions when it makes sense. FreeBSD ~~~~~~~ The current tested version is 7.0. FreeBSD supports all the standards functions, but 7.0-RELEASE seems to have some issues regarding the acl_valid() function when the qualifier of an ACL_USER or ACL_GROUP entry is the same as the current uid. By my interpretation, this should be a valid ACL, but FreeBSD declares the ACL invalid. As such, some unittests fail on FreeBSD. Porting to other platforms -------------------------- First, determine if your OS supports the full 28 functions of the POSIX.1e draft (if so, define HAVE_LEVEL2) or only the first 11 functions (most common case, meaning only HAVE_LEVEL1). If your OS supports only LEVEL1, modify ``setup.py`` as appropriately; unfortunately, the functionality of the module is quite low. If your OS supports LEVEL2, there is a function which you must define: testing if an acl_permset_t contains a given permission. For example, under Linux, the acl library defines:: int acl_get_perm(acl_permset_t permset_d, acl_perm_t perm); under FreeBSD, the library defines ``acl_get_perm_np`` with a similar syntax. So just see how this is implemented in your platform and either define a simple macro or a full function with the syntax:: static int get_perm(acl_permset_t permset_d, acl_perm_t perm); which must return 1 if the permset contains perm and 0 otherwise. .. Local Variables: .. mode: rst .. fill-column: 72 .. End: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682280837.0 pylibacl-0.7.2/doc/index.rst0000644000000000000000000000060014421310605014453 0ustar00rootroot====================================== Welcome to pylibacl's documentation! ====================================== See the :doc:`README ` for start, or the detailed :doc:`module ` information. Contents -------- .. toctree:: :maxdepth: 2 readme.md contributing.md security.md module.rst implementation.rst news.md Also see the :ref:`search`. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1336874861.0 pylibacl-0.7.2/doc/module.rst0000644000000000000000000000007111753613555014652 0ustar00rootroot.. automodule:: posix1e :members: :undoc-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347851.0 pylibacl-0.7.2/doc/news.md0000644000000000000000000001557514756714713014154 0ustar00rootroot# News ## Version 0.7.2 *released Sun, 23 Feb 2025* Single-bugfix release: fixed the typing stub module. Nothing exercised it, and having been generated with pre-3.6 stubgen, it failed to work on modern versions. No tests failed (should add some), but the doc build by Sphinx failed accidentally since the failure to import (which was ignored) led to a missing title for the module, which Sphinx complained about. Quite funny :) ## Version 0.7.1 *released Fri, 14 Feb 2025* Minor version, with a few test improvements, and updated documentation building dependencies. No user-visible changes otherwise. Tested with CPython versions 3.7-3.13, and PyPy 3.7-3.10. ## Version 0.7.0 *released Sun, 23 Apr 2023* Important: Python 3.7 is the minimum supported version, due to difficulty of testing old releases, and the fact that everything older has been deprecated a long time ago (e.g. 3.6 at the end of 2021). Otherwise, a minor release: - Improve error handling in some corner cases (not expected to have any real-life impact, but who knows). - Improved testing coverage and test infrastructure. - Modernise parts of the C code based on recent Python version guidelines. - Add a simple security policy and contribution guidelines. ## Version 0.6.0 *released Sun, 29 Nov 2020* Major release removing Python 2 support. This allow both code cleanup and new features, such as: - Support for pathlib objects in `apply_to` and `has_extended` functions when running with Python 3.6 and newer. - Use of built-in C API functions for bytes/unicode/pathlib conversion when dealing with file names, removing custom code (with the associated benefits). Important API changes/bug fixes: - Initialisation protocol has been changed, to disallow uninitialised objects; this means that `__new__` will always create valid objects, to prevent the need for checking initialisation status in all code paths; this also (implicitly) fixes memory leaks on re-initialisation (calling `__init__(…)` on an existing object) and segfaults (!) on non-initialised object attribute access. Note ACL re-initialisation is tricky and (still) leads to undefined behaviour of existing Entry objects pointing to it. - Fix another bug in ACL re-initialisation where failures would result in invalid objects; now failed re-initialisation does not touch the original object. - Restore `__setstate__`/`__getstate__` support on Linux; this was inadvertently removed due a typo(!) when adding support for it in FreeBSD. Pickle should work again for ACL instances, although not sure how stable this serialisation format actually is. - Additionally, slightly change `__setstate__()` input to not allow Unicode, since the serialisation format is an opaque binary format. - Fix (and change) entry qualifier (which is a user/group ID) behaviour: assume/require that uid_t/gid_t are unsigned types (they are with glibc, MacOS and FreeBSD at least; the standard doesn't document the signedness), and convert parsing and returning the qualifier to behave accordingly. The breakage was most apparent on 32-bit architectures, in which context the problem was originally reported (see issue #13). Minor improvements: - Added a `data` keyword argument to `ACL()`, which allows restoring an ACL directly from a serialised form (as given by `__getstate__()`), which should simplify some uses cases (`a = ACL(); a.__set state__(…)`). - When available, add the file path to I/O error messages, which should lead to easier debugging. - The test suite has changed to `pytest`, which allows increased coverage via parameterisation. ## Version 0.5.4 *released Thu, 14 Nov 2019* Maintenance release: - Switch build system to Python 3 by default (can be overridden if needed). - Internal improvements for better cpychecker support. - Fix compatibility with PyPy. - Test improvements (both local and on Travis), testing more variations (debug, PyPy). - Improve test coverage, and allow gathering test coverage results. - Drop support (well, drop testing) for Python lower than 2.7. - Minor documentation improvements (closes #9, #12). ## Version 0.5.3 *released Thu, 30 Apr 2015* FreeBSD fixes: - Enable all FreeBSD versions after 7.x at level 2 (thanks to Garrett Cooper). - Make test suite pass under FreeBSD, which has a stricter behaviour with regards to invalid ACLs (which we do exercise in the test suite), thanks again to Garret for the bug reports. ## Version 0.5.2 *released Sat, 24 May 2014* No visible changes release: just fix tests when running under pypy. ## Version 0.5.1 *released Sun, 13 May 2012* A bug-fix only release. Critical bugs (memory leaks and possible segmentation faults) have been fixed thanks to Dave Malcolm and his ``cpychecker`` tool. Additionally, some compatibility issues with Python 3.x have been fixed (str() methods returning bytes). The documentation has been improved and changed from epydoc to sphinx; note however that the documentation is still auto-generated from the docstrings. Project reorganisation: the project home page has been moved from SourceForge to GitHub. ## Version 0.5 *released Sun, 27 Dec 2009* Added support for Python 3.x and improved support for Unicode filenames. ## Version 0.4 *released Sat, 28 Jun 2008* ### License Starting with this version, pylibacl is licensed under LGPL 2.1, Febryary 1999 or any later versions (see README.rst and COPYING). ### Linux support A few more Linux-specific functions: - add the ACL.equiv_mode() method, which will return the equivalent octal mode if this is a basic ACL and raise an IOError exception otherwise - add the acl_extended(...) function, which will check if an fd or path has an extended ACL ### FreeBSD support FreeBSD 7.x will have almost all the acl manipulation functions that Linux has, with the exception of __getstate__/__setstate__. As a workaround, use the str() and ACL(text=...) methods to pass around textual representations. ### Interface At module level there are now a few constants exported for easy-checking at runtime what features have been compiled in: - `HAS_ACL_FROM_MODE`, denoting whether the ACL constructor supports the `mode=0xxx` parameter - `HAS_ACL_CHECK`, denoting whether ACL instances support the `check()` method - `HAS_ACL_ENTRY`, denoting whether ACL manipulation is possible and the Entry and Permset classes are available - `HAS_EXTENEDED_CHECK`, denoting whether the `acl_extended()` function is supported - `HAS_EQUIV_MODE`, denoting whether ACL instances support the `equiv_mode()` method ### Internals Many functions have now unittests, which is a good thing. ## Version 0.3 *released Sun, 21 Oct 2007* ### Linux support Under Linux, implement more functions from libacl: - add `ACL(mode=...)`, implementing `acl_from_mode`. - add `ACL.to_any_text()`, implementing `acl_to_any_text`. - add ACL comparison, using `acl_cmp`. - add `ACL.check()`, which is a more descriptive function than validate. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347909.0 pylibacl-0.7.2/doc/readme.md0000644000000000000000000000644414756715005014423 0ustar00rootroot# pylibacl This is a Python 3.7+ extension module allows you to manipulate the POSIX.1e Access Control Lists present in some OS/file-systems combinations. Downloads: go to . Latest version is 0.7.2. The source repository is either at or at . For any issues, please file bugs at . See the `CONTRIBUTING.md` file for details on how to contribute, or support me on [ko-fi](https://ko-fi.com/iustin). [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/iustin/pylibacl/ci.yml?branch=main)](https://github.com/iustin/pylibacl/actions/workflows/ci.yml) [![Codecov](https://img.shields.io/codecov/c/github/iustin/pylibacl)](https://codecov.io/gh/iustin/pylibacl) [![Read the Docs](https://img.shields.io/readthedocs/pylibacl)](http://pylibacl.readthedocs.io/en/latest/?badge=latest) [![GitHub issues](https://img.shields.io/github/issues/iustin/pylibacl)](https://github.com/iustin/pylibacl/issues) ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/iustin/pylibacl) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/iustin/pylibacl)](https://github.com/iustin/pylibacl/releases) [![PyPI](https://img.shields.io/pypi/v/pylibacl)](https://pypi.org/project/pylibacl/) ![Debian package](https://img.shields.io/debian/v/python-pylibacl) ![Ubuntu package](https://img.shields.io/ubuntu/v/python-pylibacl) ![GitHub Release Date](https://img.shields.io/github/release-date/iustin/pylibacl) ![GitHub commits since latest release](https://img.shields.io/github/commits-since/iustin/pylibacl/latest) ![GitHub last commit](https://img.shields.io/github/last-commit/iustin/pylibacl) ## Requirements pylibacl has been written and tested on Linux, kernel v2.4 or newer, with XFS filesystems; ext2/ext3 should also work. Since release 0.4.0, FreeBSD 7 also has quite good support. If any other platform implements the POSIX.1e draft, pylibacl can be used. I heard that Solaris does, but I can't test it. - Python 3.7 or newer. Python 2.4+ was supported in the 0.5.x branch, Python 3.4+ in the 0.6 branch. - Operating system: - Linux, kernel v2.4 or newer, and the libacl library and development packages (all modern distributions should have this, under various names); also the file-systems you use must have ACLs turned on, either as a compile or mount option. - FreeBSD 7.0 or newer. - The sphinx python module, for your python version, if building the documentation. ## FreeBSD Note that on FreeBSD, ACLs are not enabled by default (at least on UFS file systems). To enable them, run `tunefs -a enabled` on the file system in question (after mounting it read-only). Then install: - `pkg install py36-setuptools py36-sphinx` or: - `pkg install py37-setuptools` ## Security For reporting security vulnerabilities, please see `SECURITY.md`. ## License pylibacl is Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop. pylibacl is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. See the COPYING file for the full license terms. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1681673002.0 pylibacl-0.7.2/doc/security.md0000644000000000000000000000121014417045452015013 0ustar00rootroot# Security Policy To report a (potential or confirmed) security issue, please email with a description of the issue, steps to reproduce it, affected versions, and if known, mitigations for the issue. Since this is a small project, there's no list of supported versions. I will attempt to reply to reports within a working week, and to fix and disclose vulnerabilities within 90 days, but this is not a guarantee. Optionally, you can encrypt the email with my GPG key, see for details . Alternatively, you can use the GitHub "Private vulnerability reporting" functionality (but note this is beta). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347047.0 pylibacl-0.7.2/posix1e.pyi0000664000000000000000000000412414756713247014213 0ustar00rootroot# Stubs for posix1e (Python 3) # # NOTE: This dynamically typed stub was automatically generated by stubgen. from typing import Optional, Union, Tuple, TypeVar from os import PathLike from typing import overload ACL_DUPLICATE_ERROR: int ACL_ENTRY_ERROR: int ACL_EXECUTE: int ACL_GROUP: int ACL_GROUP_OBJ: int ACL_MASK: int ACL_MISS_ERROR: int ACL_MULTI_ERROR: int ACL_OTHER: int ACL_READ: int ACL_TYPE_ACCESS: int ACL_TYPE_DEFAULT: int ACL_UNDEFINED_TAG: int ACL_USER: int ACL_USER_OBJ: int ACL_WRITE: int HAS_ACL_CHECK: int HAS_ACL_ENTRY: int HAS_ACL_FROM_MODE: int HAS_COPY_EXT: int HAS_EQUIV_MODE: int HAS_EXTENDED_CHECK: int TEXT_ABBREVIATE: int TEXT_ALL_EFFECTIVE: int TEXT_NUMERIC_IDS: int TEXT_SMART_INDENT: int TEXT_SOME_EFFECTIVE: int S = TypeVar('S', str, bytes, int, PathLike) def delete_default(path: str) -> None: ... def has_extended(item: S) -> bool: ... class ACL: def __init__(*args, **kwargs) -> None: ... @overload def append(self) -> 'Entry': ... @overload def append(self, __entry: 'Entry') -> 'Entry': ... def applyto(self, __item, flag: Optional[int]=0) -> None: ... def calc_mask(self) -> None: ... def check(self) -> Union[bool, Tuple[int, int]]: ... def delete_entry(self, __entry: 'Entry') -> None: ... def equiv_mode(self) -> int: ... def to_any_text(self, prefix: str=..., separator: str=..., options: int=...) -> bytes: ... def valid(self) -> bool: ... def __iter__(self) -> 'ACL': ... def __next__(self) -> 'Entry': ... def __getstate__(self) -> bytes: ... def __setstate__(self, state: bytes) -> None: ... class Entry: parent: ACL = ... permset: 'Permset' = ... qualifier: int = ... tag_type: int = ... def __init__(self, __acl: ACL) -> None: ... def copy(self, __src: 'Entry') -> None: ... class Permset: execute: bool = ... read: bool = ... write: bool = ... def __init__(self, __entry: Entry) -> None: ... @classmethod def add(self, perm: int) -> None: ... def clear(self) -> None: ... def delete(self, perm: int) -> None: ... def test(self, perm: int) -> bool: ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1575044495.0 pylibacl-0.7.2/py.typed0000644000000000000000000000000013570242617013551 0ustar00rootroot././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740347991.964322 pylibacl-0.7.2/pylibacl.egg-info/0000775000000000000000000000000014756715130015361 5ustar00rootroot././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347991.0 pylibacl-0.7.2/pylibacl.egg-info/PKG-INFO0000644000000000000000000000205014756715127016457 0ustar00rootrootMetadata-Version: 2.1 Name: pylibacl Version: 0.7.2 Summary: POSIX.1e ACLs for python Home-page: https://pylibacl.k1024.org/ Author: Iustin Pop Author-email: iustin@k1024.org License: LGPL Project-URL: Bug Tracker, https://github.com/iustin/pylibacl/issues Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Operating System :: POSIX :: BSD :: FreeBSD Classifier: Operating System :: POSIX :: Linux Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Filesystems Requires-Python: >=3.7 License-File: COPYING This is a C extension module for Python which implements POSIX ACLs manipulation. It is a wrapper on top of the systems's acl C library - see acl(5). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347991.0 pylibacl-0.7.2/pylibacl.egg-info/SOURCES.txt0000644000000000000000000000065014756715127017252 0ustar00rootrootCONTRIBUTING.md COPYING MANIFEST.in Makefile NEWS.md README.md SECURITY.md acl.c posix1e.pyi py.typed setup.cfg setup.py doc/conf.py doc/contributing.md doc/implementation.rst doc/index.rst doc/module.rst doc/news.md doc/readme.md doc/security.md pylibacl.egg-info/PKG-INFO pylibacl.egg-info/SOURCES.txt pylibacl.egg-info/dependency_links.txt pylibacl.egg-info/not-zip-safe pylibacl.egg-info/top_level.txt tests/test_acls.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347991.0 pylibacl-0.7.2/pylibacl.egg-info/dependency_links.txt0000644000000000000000000000000114756715127021433 0ustar00rootroot ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1575044978.0 pylibacl-0.7.2/pylibacl.egg-info/not-zip-safe0000644000000000000000000000000113570243562017603 0ustar00rootroot ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347991.0 pylibacl-0.7.2/pylibacl.egg-info/top_level.txt0000644000000000000000000000001014756715127020106 0ustar00rootrootposix1e ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740347991.964322 pylibacl-0.7.2/setup.cfg0000644000000000000000000000012114756715130013701 0ustar00rootroot[bdist_rpm] release = 1 requires = libacl [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740347927.0 pylibacl-0.7.2/setup.py0000755000000000000000000000456214756715027013617 0ustar00rootroot#!/usr/bin/env python3 import os from setuptools import setup, Extension (u_sysname, u_nodename, u_release, u_version, u_machine) = os.uname() macros = [] libs = [] if u_sysname == "Linux": macros.append(("HAVE_LINUX", None)) macros.append(("HAVE_LEVEL2", None)) macros.append(("HAVE_ACL_COPY_EXT", None)) libs.append("acl") elif u_sysname == "GNU/kFreeBSD": macros.append(("HAVE_LINUX", None)) macros.append(("HAVE_LEVEL2", None)) macros.append(("HAVE_ACL_COPY_EXT", None)) libs.append("acl") elif u_sysname == "FreeBSD": macros.append(("HAVE_FREEBSD", None)) if int(u_release.split(".", 1)[0]) >= 7: macros.append(("HAVE_LEVEL2", None)) elif u_sysname == "Darwin": libs.append("pthread") else: raise ValueError("I don't know your system '%s'." " Please contact the author" % u_sysname) long_desc = """This is a C extension module for Python which implements POSIX ACLs manipulation. It is a wrapper on top of the systems's acl C library - see acl(5).""" version = "0.7.2" setup(name="pylibacl", version=version, description="POSIX.1e ACLs for python", long_description=long_desc, author="Iustin Pop", author_email="iustin@k1024.org", url="https://pylibacl.k1024.org/", license="LGPL", ext_modules=[Extension("posix1e", ["acl.c"], libraries=libs, define_macros=macros, )], python_requires = ">=3.7", # Note: doesn't work since it's not a package. Sigh. package_data = { '': ['py.typed', 'posix1e.pyi'], }, zip_safe=False, project_urls={ "Bug Tracker": "https://github.com/iustin/pylibacl/issues", }, classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Operating System :: POSIX :: BSD :: FreeBSD", "Operating System :: POSIX :: Linux", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Filesystems", ] ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740347991.964322 pylibacl-0.7.2/tests/0000775000000000000000000000000014756715130013232 5ustar00rootroot././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1682366678.0 pylibacl-0.7.2/tests/test_acls.py0000644000000000000000000010316414421560326015562 0ustar00rootroot# # """Unittests for the posix1e module""" # Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA import unittest import os import tempfile import sys import platform import re import errno import operator import pytest # type: ignore import contextlib import pathlib import io import posix1e from posix1e import * TEST_DIR = os.environ.get("TEST_DIR", ".") BASIC_ACL_TEXT = "u::rw,g::r,o::-" TEXT_0755 = "u::rwx,g::rx,o::rx" # Permset permission information PERMSETS = [ (ACL_READ, "read", Permset.read), (ACL_WRITE, "write", Permset.write), (ACL_EXECUTE, "execute", Permset.execute), ] PERMSETS_IDS = [p[1] for p in PERMSETS] ALL_TAGS = [ (posix1e.ACL_USER, "user"), (posix1e.ACL_GROUP, "group"), (posix1e.ACL_USER_OBJ, "user object"), (posix1e.ACL_GROUP_OBJ, "group object"), (posix1e.ACL_MASK, "mask"), (posix1e.ACL_OTHER, "other"), ] ALL_TAG_VALUES = [i[0] for i in ALL_TAGS] ALL_TAG_DESCS = [i[1] for i in ALL_TAGS] # Fixtures and helpers def ignore_ioerror(errnum, fn, *args, **kwargs): """Call a function while ignoring some IOErrors. This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL) when doing certain operations on an invalid ACL. """ try: fn(*args, **kwargs) except IOError as err: if err.errno == errnum: return raise def assert_acl_eq(a, b): if HAS_ACL_CHECK: assert a == b assert str(a) == str(b) @pytest.fixture def testdir(): """per-test temp dir based in TEST_DIR""" with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname: yield dname def get_file(path): fh, fname = tempfile.mkstemp(".test", "xattr-", path) return fh, fname @contextlib.contextmanager def get_file_name(path): fh, fname = get_file(path) os.close(fh) yield fname @contextlib.contextmanager def get_file_fd(path): fd = get_file(path)[0] yield fd os.close(fd) @contextlib.contextmanager def get_file_object(path): fd = get_file(path)[0] with os.fdopen(fd) as f: yield f @contextlib.contextmanager def get_dir(path): yield tempfile.mkdtemp(".test", "xattr-", path) def get_symlink(path, dangling=True): """create a symlink""" fh, fname = get_file(path) os.close(fh) if dangling: os.unlink(fname) sname = fname + ".symlink" os.symlink(fname, sname) return fname, sname @contextlib.contextmanager def get_valid_symlink(path): yield get_symlink(path, dangling=False)[1] @contextlib.contextmanager def get_dangling_symlink(path): yield get_symlink(path, dangling=True)[1] @contextlib.contextmanager def get_file_and_symlink(path): yield get_symlink(path, dangling=False) @contextlib.contextmanager def get_file_and_fobject(path): fh, fname = get_file(path) with os.fdopen(fh) as fo: yield fname, fo # Wrappers that build upon existing values def as_wrapper(call, fn, closer=None): @contextlib.contextmanager def f(path): with call(path) as r: val = fn(r) yield val if closer is not None: closer(val) return f def as_bytes(call): return as_wrapper(call, lambda r: r.encode()) def as_fspath(call): return as_wrapper(call, pathlib.PurePath) def as_iostream(call): opener = lambda f: io.open(f, "r") closer = lambda r: r.close() return as_wrapper(call, opener, closer) NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)", strict=True) NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'", strict=False) require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE") require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK") require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY") require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK") require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE") require_copy_ext = pytest.mark.skipif("not HAS_COPY_EXT") # Note: ACLs are valid only for files/directories, not symbolic links # themselves, so we only create valid symlinks. FILE_P = [ get_file_name, as_bytes(get_file_name), pytest.param(as_fspath(get_file_name), marks=[NOT_BEFORE_36, NOT_PYPY]), get_dir, as_bytes(get_dir), pytest.param(as_fspath(get_dir), marks=[NOT_BEFORE_36, NOT_PYPY]), get_valid_symlink, as_bytes(get_valid_symlink), pytest.param(as_fspath(get_valid_symlink), marks=[NOT_BEFORE_36, NOT_PYPY]), ] FILE_D = [ "file name", "file name (bytes)", "file name (path)", "directory", "directory (bytes)", "directory (path)", "file via symlink", "file via symlink (bytes)", "file via symlink (path)", ] FD_P = [ get_file_fd, get_file_object, as_iostream(get_file_name), ] FD_D = [ "file FD", "file object", "file io stream", ] DIR_D = [ "directory", "directory (bytes)", "directory (path object)", ] DIR_P = [ get_dir, as_bytes(get_dir), pytest.param(as_fspath(get_dir), marks=[NOT_BEFORE_36, NOT_PYPY]), ] ALL_P = FILE_P + FD_P ALL_D = FILE_D + FD_D @pytest.fixture(params=FILE_P, ids=FILE_D) def file_subject(testdir, request): with request.param(testdir) as value: yield value @pytest.fixture(params=FD_P, ids=FD_D) def fd_subject(testdir, request): with request.param(testdir) as value: yield value @pytest.fixture(params=DIR_P, ids=DIR_D) def dir_subject(testdir, request): with request.param(testdir) as value: yield value @pytest.fixture(params=ALL_P, ids=ALL_D) def subject(testdir, request): with request.param(testdir) as value: yield value class TestLoad: """Load/create tests""" def test_from_file(self, file_subject): """Test loading ACLs from a file/directory""" acl = posix1e.ACL(file=file_subject) assert acl.valid() def test_from_dir(self, dir_subject): """Test loading ACLs from a directory""" acl2 = posix1e.ACL(filedef=dir_subject) # default ACLs might or might not be valid; missing ones are # not valid, so we don't test acl2 for validity def test_from_fd(self, fd_subject): """Test loading ACLs from a file descriptor""" acl = posix1e.ACL(fd=fd_subject) assert acl.valid() def test_from_nonexisting(self, testdir): _, fname = get_file(testdir) with pytest.raises(IOError): posix1e.ACL(file="fname"+".no-such-file") with pytest.raises(IOError): posix1e.ACL(filedef="fname"+".no-such-file") def test_from_invalid_fd(self, testdir): fd, _ = get_file(testdir) os.close(fd) with pytest.raises(IOError): posix1e.ACL(fd=fd) def test_from_empty_invalid(self): """Test creating an empty ACL""" acl1 = posix1e.ACL() assert not acl1.valid() def test_from_text(self): """Test creating an ACL from text""" acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) assert acl1.valid() # This is acl_check, but should actually be have_linux... @require_acl_check def test_from_acl(self): """Test creating an ACL from an existing ACL""" acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) acl2 = posix1e.ACL(acl=acl1) assert acl1 == acl2 def test_from_acl_via_str(self): # This is needed for not HAVE_LINUX cases. acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) acl2 = posix1e.ACL(acl=acl1) assert str(acl1) == str(acl2) def test_invalid_creation_params(self, testdir): """Test that creating an ACL from multiple objects fails""" fd, _ = get_file(testdir) with pytest.raises(ValueError): posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd) def test_invalid_value_creation(self): """Test that creating an ACL from wrong specification fails""" with pytest.raises(EnvironmentError): posix1e.ACL(text="foobar") with pytest.raises(TypeError): posix1e.ACL(foo="bar") def test_uninit(self): """Checks that uninit is actually empty init""" acl = posix1e.ACL.__new__(posix1e.ACL) assert not acl.valid() e = acl.append() e.permset acl.delete_entry(e) def test_double_init(self): acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) assert acl1.valid() acl1.__init__(text=BASIC_ACL_TEXT) # type: ignore assert acl1.valid() acl2 = ACL(text=TEXT_0755) assert acl1 != acl2 acl1.__init__(acl=acl2) # type: ignore assert_acl_eq(acl1, acl2) def test_reinit_failure_noop(self): a = posix1e.ACL(text=TEXT_0755) b = posix1e.ACL(acl=a) assert_acl_eq(a, b) with pytest.raises(IOError): a.__init__(text='foobar') assert_acl_eq(a, b) @pytest.mark.xfail(reason="Unreliable test, re-init doesn't always invalidate children") def test_double_init_breaks_children(self): acl = posix1e.ACL() e = acl.append() e.permset.write = True acl.__init__() # type: ignore with pytest.raises(EnvironmentError): e.permset.write = False class TestAclExtensions: """ACL extensions checks""" @require_acl_from_mode def test_from_mode(self): """Test loading ACLs from an octal mode""" acl1 = posix1e.ACL(mode=0o644) assert acl1.valid() @require_acl_check def test_acl_check(self): """Test the acl_check method""" acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) assert not acl1.check() acl2 = posix1e.ACL() c = acl2.check() assert c == (ACL_MISS_ERROR, 0) assert isinstance(c, tuple) assert c[0] == ACL_MISS_ERROR e = acl2.append() c = acl2.check() assert c == (ACL_ENTRY_ERROR, 0) def test_applyto(self, subject): """Test the apply_to function""" # TODO: add read/compare with before, once ACL can be init'ed # from any source. basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) basic_acl.applyto(subject) enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") assert enhanced_acl.valid() enhanced_acl.applyto(subject) def test_apply_to_with_wrong_object(self): acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) assert acl1.valid() with pytest.raises(TypeError): acl1.applyto(object()) with pytest.raises(TypeError): acl1.applyto(object(), object()) # type: ignore def test_apply_to_fail(self, testdir): acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) assert acl1.valid() fd, fname = get_file(testdir) os.close(fd) with pytest.raises(IOError): acl1.applyto(fd) with pytest.raises(IOError, match="no-such-file"): acl1.applyto(fname+".no-such-file") @require_extended_check def test_applyto_extended(self, subject): """Test the acl_extended function""" basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) basic_acl.applyto(subject) assert not has_extended(subject) enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") assert enhanced_acl.valid() enhanced_acl.applyto(subject) assert has_extended(subject) @require_extended_check @pytest.mark.parametrize( "gen", [ get_file_and_symlink, get_file_and_fobject ]) def test_applyto_extended_mixed(self, testdir, gen): """Test the acl_extended function""" with gen(testdir) as (a, b): basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) basic_acl.applyto(a) for item in a, b: assert not has_extended(item) enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") assert enhanced_acl.valid() enhanced_acl.applyto(b) for item in a, b: assert has_extended(item) @require_extended_check def test_extended_fail(self, testdir): fd, fname = get_file(testdir) os.close(fd) with pytest.raises(IOError): has_extended(fd) with pytest.raises(IOError, match="no-such-file"): has_extended(fname+".no-such-file") @require_extended_check def test_extended_arg_handling(self): with pytest.raises(TypeError): has_extended() # type: ignore with pytest.raises(TypeError): has_extended(object()) # type: ignore @require_equiv_mode def test_equiv_mode(self): """Test the equiv_mode function""" if HAS_ACL_FROM_MODE: for mode in 0o644, 0o755: acl = posix1e.ACL(mode=mode) assert acl.equiv_mode() == mode acl = posix1e.ACL(text="u::rw,g::r,o::r") assert acl.equiv_mode() == 0o644 acl = posix1e.ACL(text="u::rx,g::-,o::-") assert acl.equiv_mode() == 0o500 @require_equiv_mode @pytest.mark.xfail(reason="It seems equiv mode always passes, even for empty ACLs") def test_equiv_mode_invalid(self): """Test equiv_mode on invalid ACLs""" a = posix1e.ACL() with pytest.raises(EnvironmentError): a.equiv_mode() @require_acl_check def test_to_any_text(self): acl = posix1e.ACL(text=BASIC_ACL_TEXT) assert b"u::" in \ acl.to_any_text(options=posix1e.TEXT_ABBREVIATE) assert b"user::" in acl.to_any_text() @require_acl_check def test_to_any_text_wrong_args(self): acl = posix1e.ACL(text=BASIC_ACL_TEXT) with pytest.raises(TypeError): acl.to_any_text(foo="bar") # type: ignore @require_acl_check def test_rich_compare(self): acl1 = posix1e.ACL(text="u::rw,g::r,o::r") acl2 = posix1e.ACL(acl=acl1) acl3 = posix1e.ACL(text="u::rw,g::rw,o::r") assert acl1 == acl2 assert acl1 != acl3 with pytest.raises(TypeError): acl1 < acl2 # type: ignore with pytest.raises(TypeError): acl1 >= acl3 # type: ignore assert acl1 != True # type: ignore assert not (acl1 == 1) # type: ignore with pytest.raises(TypeError): acl1 > True # type: ignore @require_acl_entry def test_acl_iterator(self): acl = posix1e.ACL(text=BASIC_ACL_TEXT) for entry in acl: assert entry.parent is acl @require_copy_ext def test_acl_copy_ext(self): a = posix1e.ACL(text=BASIC_ACL_TEXT) b = posix1e.ACL() c = posix1e.ACL(acl=b) assert a != b assert b == c state = a.__getstate__() b.__setstate__(state) assert a == b assert b != c @staticmethod def get_nulled_state(src=None): """Generate a mostly-valid external serialization Passing arbitrary state into acl_copy_int() is dangerous. That C function gets a void * buffer, and then casts that to an ACL structure, irrespective of buffer length; this can lead to segfaults (via unallocated memory indexing). Depending on the exact buffer, the same code might segfault on all architectures, some architectures, all C compiler versions, or some C compilers, or any combination of the above :( To mitigate this, pass a much larger buffer size as returned from the state, just nulled out - in the Linux version of the library, the first byte is the structure size and is tested for correct size, and a null byte will cause failure. """ if src is None: src = posix1e.ACL() state = src.__getstate__() nulled = b'\x00' * (10 * len(state)) return nulled @require_copy_ext def test_acl_copy_int_failure(self): a = posix1e.ACL() nulled = self.get_nulled_state(a) with pytest.raises(IOError): a.__setstate__(nulled) @require_copy_ext def test_acl_copy_int_failure_is_noop(self): a = posix1e.ACL(text=BASIC_ACL_TEXT) b = posix1e.ACL() c = posix1e.ACL(acl=a) assert a == c assert a != b nulled = self.get_nulled_state(b) with pytest.raises(IOError): a.__setstate__(nulled) # Assert that 'a' didn't change in the attempt to restore # invalid state. assert a == c @require_copy_ext def test_acl_copy_int_args(self): a = posix1e.ACL() with pytest.raises(TypeError): a.__setstate__(None) @require_copy_ext def test_acl_init_copy_int(self): a = posix1e.ACL(text=BASIC_ACL_TEXT) b = posix1e.ACL() c = posix1e.ACL(data=a.__getstate__()) assert c != b assert c == a @require_copy_ext def test_acl_init_copy_int_invalid(self): with pytest.raises(IOError): posix1e.ACL(data=self.get_nulled_state()) class TestWrite: """Write tests""" def test_delete_default(self, testdir): """Test removing the default ACL""" with get_dir(testdir) as dname: posix1e.delete_default(dname) def test_delete_default_fail(self, testdir): """Test removing the default ACL""" with get_file_name(testdir) as fname: with pytest.raises(IOError, match="no-such-file"): posix1e.delete_default(fname+".no-such-file") @NOT_PYPY def test_delete_default_wrong_arg(self): with pytest.raises(TypeError): posix1e.delete_default(object()) # type: ignore def test_reapply(self, testdir): """Test re-applying an ACL""" fd, fname = get_file(testdir) acl1 = posix1e.ACL(fd=fd) acl1.applyto(fd) acl1.applyto(fname) with get_dir(testdir) as dname: acl2 = posix1e.ACL(file=fname) acl2.applyto(dname) @require_acl_entry class TestModification: """ACL modification tests""" def checkRef(self, obj): """Checks if a given obj has a 'sane' refcount""" if platform.python_implementation() == "PyPy": return ref_cnt = sys.getrefcount(obj) # FIXME: hardcoded value for the max ref count... but I've # seen it overflow on bad reference counting, so it's better # to be safe if ref_cnt < 2 or ref_cnt > 1024: pytest.fail("Wrong reference count, expected 2-1024 and got %d" % ref_cnt) def test_str(self): """Test str() of an ACL.""" acl = posix1e.ACL(text=BASIC_ACL_TEXT) str_acl = str(acl) self.checkRef(str_acl) def test_append(self): """Test append a new Entry to the ACL""" acl = posix1e.ACL() e = acl.append() e.tag_type = posix1e.ACL_OTHER ignore_ioerror(errno.EINVAL, acl.calc_mask) str_format = str(e) self.checkRef(str_format) e2 = acl.append(e) ignore_ioerror(errno.EINVAL, acl.calc_mask) assert not acl.valid() def test_wrong_append(self): """Test append a new Entry to the ACL based on wrong object type""" acl = posix1e.ACL() with pytest.raises(TypeError): acl.append(object()) # type: ignore @pytest.mark.xfail(reason="Behaviour not conform to specification") def test_append_invalid_source(self): a = posix1e.ACL() b = posix1e.ACL() f = b.append() b.delete_entry(f) with pytest.raises(EnvironmentError): f.permset.write = True with pytest.raises(EnvironmentError): e = a.append(f) def test_entry_creation(self): acl = posix1e.ACL() e = posix1e.Entry(acl) ignore_ioerror(errno.EINVAL, acl.calc_mask) str_format = str(e) self.checkRef(str_format) def test_entry_failed_creation(self): # Checks for partial initialisation and deletion on error # path. with pytest.raises(TypeError): posix1e.Entry(object()) # type: ignore def test_entry_reinitialisations(self): a = posix1e.ACL() b = posix1e.ACL() e = posix1e.Entry(a) e.__init__(a) # type: ignore with pytest.raises(ValueError, match="different parent"): e.__init__(b) # type: ignore @NOT_PYPY def test_entry_reinit_leaks_refcount(self): acl = posix1e.ACL() e = acl.append() ref = sys.getrefcount(acl) e.__init__(acl) # type: ignore assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..." def test_delete(self): """Test delete Entry from the ACL""" acl = posix1e.ACL() e = acl.append() e.tag_type = posix1e.ACL_OTHER ignore_ioerror(errno.EINVAL, acl.calc_mask) acl.delete_entry(e) ignore_ioerror(errno.EINVAL, acl.calc_mask) def test_double_delete(self): """Test delete Entry from the ACL""" # This is not entirely valid/correct, since the entry object # itself is invalid after the first deletion, so we're # actually testing deleting an invalid object, not a # non-existing entry... acl = posix1e.ACL() e = acl.append() e.tag_type = posix1e.ACL_OTHER ignore_ioerror(errno.EINVAL, acl.calc_mask) acl.delete_entry(e) ignore_ioerror(errno.EINVAL, acl.calc_mask) with pytest.raises(EnvironmentError): acl.delete_entry(e) def test_delete_unowned(self): """Test delete Entry from the ACL""" a = posix1e.ACL() b = posix1e.ACL() e = a.append() e.tag_type = posix1e.ACL_OTHER with pytest.raises(ValueError, match="un-owned entry"): b.delete_entry(e) # This currently fails as this deletion seems to be accepted :/ @pytest.mark.xfail(reason="Entry deletion is unreliable") def testDeleteInvalidEntry(self): """Test delete foreign Entry from the ACL""" acl1 = posix1e.ACL() acl2 = posix1e.ACL() e = acl1.append() e.tag_type = posix1e.ACL_OTHER ignore_ioerror(errno.EINVAL, acl1.calc_mask) with pytest.raises(EnvironmentError): acl2.delete_entry(e) def test_delete_invalid_object(self): """Test delete a non-Entry from the ACL""" acl = posix1e.ACL() with pytest.raises(TypeError): acl.delete_entry(object()) # type: ignore def test_double_entries(self): """Test double entries""" acl = posix1e.ACL(text=BASIC_ACL_TEXT) assert acl.valid() for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ, posix1e.ACL_OTHER): e = acl.append() e.tag_type = tag_type e.permset.clear() assert not acl.valid(), ("ACL containing duplicate entries" " should not be valid") acl.delete_entry(e) def test_multiple_good_entries(self): """Test multiple valid entries""" acl = posix1e.ACL(text=BASIC_ACL_TEXT) assert acl.valid() for tag_type in (posix1e.ACL_USER, posix1e.ACL_GROUP): for obj_id in range(5): e = acl.append() e.tag_type = tag_type e.qualifier = obj_id e.permset.clear() acl.calc_mask() assert acl.valid(), ("ACL should be able to hold multiple" " user/group entries") def test_multiple_bad_entries(self): """Test multiple invalid entries""" for tag_type in (posix1e.ACL_USER, posix1e.ACL_GROUP): acl = posix1e.ACL(text=BASIC_ACL_TEXT) assert acl.valid() e1 = acl.append() e1.tag_type = tag_type e1.qualifier = 0 e1.permset.clear() acl.calc_mask() assert acl.valid(), ("ACL should be able to add a" " user/group entry") e2 = acl.append() e2.tag_type = tag_type e2.qualifier = 0 e2.permset.clear() ignore_ioerror(errno.EINVAL, acl.calc_mask) assert not acl.valid(), ("ACL should not validate when" " containing two duplicate entries") acl.delete_entry(e1) # FreeBSD trips over itself here and can't delete the # entry, even though it still exists. ignore_ioerror(errno.EINVAL, acl.delete_entry, e2) def test_copy(self): acl = ACL() e1 = acl.append() e1.tag_type = ACL_USER p1 = e1.permset p1.clear() p1.read = True p1.write = True e2 = acl.append() e2.tag_type = ACL_GROUP p2 = e2.permset p2.clear() p2.read = True assert not p2.write e2.copy(e1) assert p2.write assert e1.tag_type == e2.tag_type def test_copy_wrong_arg(self): acl = ACL() e = acl.append() with pytest.raises(TypeError): e.copy(object()) # type: ignore def test_set_permset(self): acl = ACL() e1 = acl.append() e1.tag_type = ACL_USER p1 = e1.permset p1.clear() p1.read = True p1.write = True e2 = acl.append() e2.tag_type = ACL_GROUP p2 = e2.permset p2.clear() p2.read = True assert not p2.write e2.permset = p1 assert e2.permset.write assert e2.tag_type == ACL_GROUP def test_set_permset_wrong_arg(self): acl = ACL() e = acl.append() with pytest.raises(TypeError): e.permset = object() # type: ignore def test_permset_creation(self): acl = ACL() e = acl.append() p1 = e.permset p2 = Permset(e) #assert p1 == p2 def test_permset_creation_wrong_arg(self): with pytest.raises(TypeError): Permset(object()) # type: ignore def test_permset_reinitialisations(self): a = posix1e.ACL() e = posix1e.Entry(a) f = posix1e.Entry(a) p = e.permset p.__init__(e) # type: ignore with pytest.raises(ValueError, match="different parent"): p.__init__(f) # type: ignore @NOT_PYPY def test_permset_reinit_leaks_refcount(self): acl = posix1e.ACL() e = acl.append() p = e.permset ref = sys.getrefcount(e) p.__init__(e) # type: ignore assert ref == sys.getrefcount(e), "Uh-oh, ref leaks..." @pytest.mark.parametrize("perm, txt, accessor", PERMSETS, ids=PERMSETS_IDS) def test_permset(self, perm, txt, accessor): """Test permissions""" del accessor acl = posix1e.ACL() e = acl.append() ps = e.permset ps.clear() str_ps = str(ps) self.checkRef(str_ps) assert not ps.test(perm), ("Empty permission set should not" " have permission '%s'" % txt) ps.add(perm) assert ps.test(perm), ("Permission '%s' should exist" " after addition" % txt) str_ps = str(ps) self.checkRef(str_ps) ps.delete(perm) assert not ps.test(perm), ("Permission '%s' should not exist" " after deletion" % txt) ps.add(perm) assert ps.test(perm), ("Permission '%s' should exist" " after addition" % txt) ps.clear() assert not ps.test(perm), ("Permission '%s' should not exist" " after clearing" % txt) @pytest.mark.parametrize("perm, txt, accessor", PERMSETS, ids=PERMSETS_IDS) def test_permset_via_accessors(self, perm, txt, accessor): """Test permissions""" acl = posix1e.ACL() e = acl.append() ps = e.permset ps.clear() def getter(): return accessor.__get__(ps) # type: ignore def setter(value): return accessor.__set__(ps, value) # type: ignore str_ps = str(ps) self.checkRef(str_ps) assert not getter(), ("Empty permission set should not" " have permission '%s'" % txt) setter(True) assert ps.test(perm), ("Permission '%s' should exist" " after addition" % txt) assert getter(), ("Permission '%s' should exist" " after addition" % txt) str_ps = str(ps) self.checkRef(str_ps) setter(False) assert not ps.test(perm), ("Permission '%s' should not exist" " after deletion" % txt) assert not getter(), ("Permission '%s' should not exist" " after deletion" % txt) setter(True) assert getter() ps.clear() assert not getter() def test_permset_invalid_type(self): acl = posix1e.ACL() e = acl.append() ps = e.permset ps.clear() with pytest.raises(TypeError): ps.add("foobar") # type: ignore with pytest.raises(TypeError): ps.delete("foobar") # type: ignore with pytest.raises(TypeError): ps.test("foobar") # type: ignore with pytest.raises(ValueError): ps.write = object() # type: ignore @pytest.mark.parametrize("tag", [ACL_USER, ACL_GROUP], ids=["ACL_USER", "ACL_GROUP"]) def test_qualifier_values(self, tag): """Tests qualifier correct store/retrieval""" acl = posix1e.ACL() e = acl.append() qualifier = 1 e.tag_type = tag while True: regex = re.compile("(user|group) with (u|g)id %d" % qualifier) try: e.qualifier = qualifier except OverflowError: # reached overflow condition, break break assert e.qualifier == qualifier assert regex.search(str(e)) is not None qualifier *= 2 def test_qualifier_overflow(self): """Tests qualifier overflow handling""" acl = posix1e.ACL() e = acl.append() # the uid_t/gid_t are unsigned, so they can hold slightly more # than sys.maxsize*2 (on Linux). qualifier = (sys.maxsize + 1) * 2 for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]: e.tag_type = tag with pytest.raises(OverflowError): e.qualifier = qualifier def test_qualifier_underflow(self): """Tests negative qualifier handling""" # Note: this presumes that uid_t/gid_t in C are unsigned... acl = posix1e.ACL() e = acl.append() for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]: e.tag_type = tag for qualifier in [-10, -5, -1]: with pytest.raises(OverflowError): e.qualifier = qualifier def test_invalid_qualifier(self): """Tests invalid qualifier handling""" acl = posix1e.ACL() e = acl.append() with pytest.raises(TypeError): e.qualifier = object() # type: ignore with pytest.raises((TypeError, AttributeError)): del e.qualifier def test_qualifier_on_wrong_tag(self): """Tests qualifier setting on wrong tag""" acl = posix1e.ACL() e = acl.append() e.tag_type = posix1e.ACL_OTHER with pytest.raises(TypeError): e.qualifier = 1 with pytest.raises(TypeError): e.qualifier @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS) def test_tag_types(self, tag): """Tests tag type correct set/get""" acl = posix1e.ACL() e = acl.append() e.tag_type = tag assert e.tag_type == tag # check we can show all tag types without breaking assert str(e) @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS) @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS) def test_tag_overwrite(self, src_tag, dst_tag): """Tests tag type correct set/get""" acl = posix1e.ACL() e = acl.append() e.tag_type = src_tag assert e.tag_type == src_tag assert str(e) e.tag_type = dst_tag assert e.tag_type == dst_tag assert str(e) def test_invalid_tags(self): """Tests tag type incorrect set/get""" acl = posix1e.ACL() e = acl.append() with pytest.raises(TypeError): e.tag_type = object() # type: ignore e.tag_type = posix1e.ACL_USER_OBJ # For some reason, PyPy raises AttributeError. Strange... with pytest.raises((TypeError, AttributeError)): del e.tag_type def test_tag_wrong_overwrite(self): acl = posix1e.ACL() e = acl.append() e.tag_type = posix1e.ACL_USER_OBJ tag = max(ALL_TAG_VALUES) + 1 with pytest.raises(EnvironmentError): e.tag_type = tag # Check tag is still valid. assert e.tag_type == posix1e.ACL_USER_OBJ if __name__ == "__main__": unittest.main()