├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .readthedocs.yaml ├── CONTRIBUTING.md ├── COPYING ├── FUNDING.yml ├── MANIFEST.in ├── Makefile ├── NEWS.md ├── README.md ├── SECURITY.md ├── acl.c ├── doc ├── conf.py ├── contributing.md ├── implementation.rst ├── index.rst ├── module.rst ├── news.md ├── readme.md ├── requirements.txt └── security.md ├── posix1e.pyi ├── py.typed ├── setup.cfg ├── setup.py └── tests └── test_acls.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | # Trigger the workflow on push or 3 | # pull request, but only for the 4 | # main branch. 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | # Allow manual triggering 12 | workflow_dispatch: 13 | # Weekly run to account for 14 | # changed dependencies. 15 | schedule: 16 | - cron: '17 03 * * 0' 17 | 18 | name: CI 19 | jobs: 20 | build: 21 | name: Build and test 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | os: [ubuntu-22.04] 26 | python-version: 27 | - '3.7' 28 | - '3.8' 29 | - '3.9' 30 | - '3.10' 31 | - '3.11' 32 | - '3.12' 33 | - '3.13' 34 | - 'pypy-3.7' 35 | - 'pypy-3.8' 36 | - 'pypy-3.9' 37 | - 'pypy-3.10' 38 | fail-fast: true 39 | 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | 44 | - name: Set up Python ${{ matrix.python-version }} 45 | uses: actions/setup-python@v5 46 | with: 47 | python-version: ${{ matrix.python-version }} 48 | 49 | - name: Cache pip 50 | uses: actions/cache@v4 51 | with: 52 | # This path is specific to Ubuntu 53 | path: ~/.cache/pip 54 | # Look to see if there is a cache hit for the corresponding requirements file 55 | key: v1-pip-${{ runner.os }}-${{ matrix.python-version }} 56 | restore-keys: | 57 | v1-pip-${{ runner.os }} 58 | v1-pip- 59 | 60 | - name: Install dependencies 61 | run: | 62 | pip install setuptools 63 | pip install pytest 64 | pip install sphinx 65 | pip install myst-parser 66 | sudo apt-get install -yy libacl1-dev 67 | 68 | - name: Build the code 69 | run: python ./setup.py build_ext -i 70 | 71 | - name: Run tests 72 | run: python -m pytest tests 73 | 74 | - name: Cleanup 75 | run: make clean 76 | 77 | - name: Re-build with coverage info 78 | run: CFLAGS="-coverage" python ./setup.py build_ext -i 79 | 80 | - name: Test with coverage 81 | run: python -m pytest tests 82 | 83 | - name: Upload coverage to Codecov 84 | uses: codecov/codecov-action@v4 85 | with: 86 | #files: ./coverage1.xml,./coverage2.xml 87 | #directory: ./coverage/reports/ 88 | #flags: unittests 89 | #env_vars: OS,PYTHON 90 | name: codecov-python-${{ matrix.python-version }} 91 | token: ${{ secrets.CODECOV_TOKEN }} 92 | #fail_ci_if_error: true 93 | #verbose: true 94 | 95 | - name: Build documentation 96 | run: make doc 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /MANIFEST 2 | /build 3 | /dist 4 | /doc/doctrees 5 | /doc/html 6 | /posix1e.html 7 | /posix1e.so 8 | /posix1e.txt 9 | /pylibacl.egg-info 10 | /TAGS 11 | *.py[co] 12 | *.so 13 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | apt_packages: 17 | - libacl1-dev 18 | jobs: 19 | pre_build: 20 | - make posix1e.so 21 | 22 | # Build documentation in the "docs/" directory with Sphinx 23 | sphinx: 24 | configuration: doc/conf.py 25 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 26 | # builder: "dirhtml" 27 | # Fail on all warnings to avoid broken references 28 | # fail_on_warning: true 29 | 30 | # Optionally build your docs in additional formats such as PDF and ePub 31 | # formats: 32 | # - pdf 33 | # - epub 34 | 35 | # Optional but recommended, declare the Python requirements required 36 | # to build your documentation 37 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 38 | python: 39 | install: 40 | - requirements: doc/requirements.txt 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to pylibacl 2 | 3 | Hi, and thanks for any and all contributions! 4 | 5 | ## Bugs and patches 6 | 7 | This is a small project, so let's keep things simple: 8 | 9 | - Please file all bug reports on github 10 | (), as this allows 11 | archival and discovery by other people; 12 | - Send patches as pull requests; for larger changes, would be good to 13 | first open a bug to discuss the plans; 14 | 15 | Due to simplicity, there are no old branches being kept alive, but if 16 | it ever happens that a bug is found in older versions and there is 17 | needed to support older Python versions, it is possible to do so. 18 | 19 | ## Code standards 20 | 21 | There are no formal standards, but: 22 | 23 | - Code should be tested - this is why there's a [Codecov 24 | integration](https://app.codecov.io/gh/iustin/pylibacl/tree/main). 25 | - New functions should have good docstrings (in the C code). 26 | - New functions/constants should be listed in the documentation, see 27 | `doc/module.rst` for how to include them. 28 | - All non-trivial changes should be listed in `NEWS.md` for further 29 | inclusion in new releases documentation. Add an "unreleased" section 30 | (if one doesn't exist yet) to list the changes. 31 | 32 | ## Release process 33 | 34 | Right now, due to GPG signing, I'm doing releases and signing them 35 | manually (offline, I mean). Basically, once GitHub workflows are fine: 36 | 37 | - Bump the version in all places - use `git grep -F $OLD_VER` and 38 | update as needed. 39 | - Ensure that `setup.py` has the right Python versions listed (bit me 40 | more than once). 41 | - Update the `NEWS.md` file is up to date (contents), and use the 42 | right date. 43 | - Check that the generated documentation (`make doc`) looks right. 44 | 45 | Then run these steps: 46 | 47 | ``` 48 | $ make clean 49 | $ make distcheck # this leaves things in dist/ 50 | $ git tag -m 'Release pylibacl-0.0.1' --sign v0.0.1 51 | $ gpg --sign -b -a dist/pylibacl-0.0.1.tar.gz 52 | $ python3 -m twine upload dist/* 53 | ``` 54 | 55 | Separately: 56 | 57 | * Upload the `dist/` contents to GitHub and tag a new release. 58 | * Upload the `dist/` contents to the old-style download area, 59 | . 60 | 61 | Hopefully one day all this can be more automated. 62 | 63 | ## Signing key 64 | 65 | The releases are currently signed by my key, see . 66 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | 504 | 505 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: iustin 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYING 2 | include Makefile 3 | include NEWS.md 4 | include README.md 5 | include CONTRIBUTING.md 6 | include SECURITY.md 7 | include acl.c 8 | include setup.cfg 9 | include doc/conf.py 10 | include doc/*.rst 11 | include doc/*.md 12 | include tests/*.py 13 | include py.typed 14 | include posix1e.pyi 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = python3 2 | SPHINXOPTS = -W 3 | SPHINXBUILD = $(PYTHON) -m sphinx 4 | DOCDIR = doc 5 | DOCHTML = $(DOCDIR)/html 6 | DOCTREES = $(DOCDIR)/doctrees 7 | ALLSPHINXOPTS = -d $(DOCTREES) $(SPHINXOPTS) $(DOCDIR) 8 | VERSION = 0.7.2 9 | FULLVER = pylibacl-$(VERSION) 10 | DISTFILE = $(FULLVER).tar.gz 11 | 12 | MODNAME = posix1e.so 13 | DOCFILES = doc/index.rst doc/module.rst doc/news.md doc/readme.md doc/conf.py 14 | 15 | all: doc test 16 | 17 | $(MODNAME): acl.c 18 | $(PYTHON) ./setup.py build_ext --inplace 19 | 20 | $(DOCHTML)/index.html: $(MODNAME) $(DOCFILES) acl.c 21 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DOCHTML) 22 | touch $@ 23 | 24 | doc: $(DOCHTML)/index.html 25 | 26 | dist: 27 | fakeroot $(PYTHON) ./setup.py sdist 28 | 29 | distcheck: dist 30 | set -e; \ 31 | TDIR=$$(mktemp -d) && \ 32 | trap "rm -rf $$TDIR" EXIT; \ 33 | tar xzf dist/$(DISTFILE) -C $$TDIR && \ 34 | (cd $$TDIR/$(FULLVER) && make doc && make test && make dist) && \ 35 | echo "All good, you can upload $(DISTFILE)!" 36 | 37 | test: 38 | @set -e; \ 39 | for ver in 3.7 3.8 3.9 3.10 3.11 3.12; do \ 40 | for flavour in "" "-dbg"; do \ 41 | if type python$$ver$$flavour >/dev/null; then \ 42 | echo Testing with python$$ver$$flavour; \ 43 | python$$ver$$flavour ./setup.py build_ext -i; \ 44 | python$$ver$$flavour -m pytest tests ;\ 45 | fi; \ 46 | done; \ 47 | done; \ 48 | for pp in pypy3; do \ 49 | if type $$pp >/dev/null; then \ 50 | echo Testing with $$pp; \ 51 | $$pp ./setup.py build_ext -i; \ 52 | $$pp -m pytest tests; \ 53 | fi; \ 54 | done 55 | 56 | fast-test: 57 | python3 setup.py build_ext -i 58 | python3 -m pytest tests 59 | 60 | ci: 61 | while inotifywait -e CLOSE_WRITE tests/test_*.py; do \ 62 | python3 -m pytest tests; \ 63 | done 64 | 65 | coverage: 66 | $(MAKE) clean 67 | $(MAKE) test CFLAGS="-coverage" 68 | lcov --capture --no-external --directory . --output-file coverage.info 69 | genhtml coverage.info --output-directory out 70 | 71 | clean: 72 | rm -rf $(DOCHTML) $(DOCTREES) 73 | rm -f $(MODNAME) 74 | rm -f *.so 75 | rm -rf build 76 | 77 | types: 78 | MYPYPATH=. mypy --check-untyped-defs --warn-incomplete-stub tests/test_acls.py 79 | 80 | .PHONY: doc test clean dist coverage ci types 81 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # News 2 | 3 | ## Version 0.7.2 4 | 5 | *released Sun, 23 Feb 2025* 6 | 7 | Single-bugfix release: fixed the typing stub module. Nothing exercised 8 | it, and having been generated with pre-3.6 stubgen, it failed to work 9 | on modern versions. No tests failed (should add some), but the doc 10 | build by Sphinx failed accidentally since the failure to import (which 11 | was ignored) led to a missing title for the module, which Sphinx 12 | complained about. Quite funny :) 13 | 14 | ## Version 0.7.1 15 | 16 | *released Fri, 14 Feb 2025* 17 | 18 | Minor version, with a few test improvements, and updated documentation 19 | building dependencies. No user-visible changes otherwise. 20 | 21 | Tested with CPython versions 3.7-3.13, and PyPy 3.7-3.10. 22 | 23 | ## Version 0.7.0 24 | 25 | *released Sun, 23 Apr 2023* 26 | 27 | Important: Python 3.7 is the minimum supported version, due to 28 | difficulty of testing old releases, and the fact that everything older 29 | has been deprecated a long time ago (e.g. 3.6 at the end of 2021). 30 | 31 | Otherwise, a minor release: 32 | 33 | - Improve error handling in some corner cases (not expected to have 34 | any real-life impact, but who knows). 35 | - Improved testing coverage and test infrastructure. 36 | - Modernise parts of the C code based on recent Python version 37 | guidelines. 38 | - Add a simple security policy and contribution guidelines. 39 | 40 | ## Version 0.6.0 41 | 42 | *released Sun, 29 Nov 2020* 43 | 44 | Major release removing Python 2 support. This allow both code cleanup 45 | and new features, such as: 46 | 47 | - Support for pathlib objects in `apply_to` and `has_extended` 48 | functions when running with Python 3.6 and newer. 49 | - Use of built-in C API functions for bytes/unicode/pathlib conversion 50 | when dealing with file names, removing custom code (with the 51 | associated benefits). 52 | 53 | Important API changes/bug fixes: 54 | 55 | - Initialisation protocol has been changed, to disallow uninitialised 56 | objects; this means that `__new__` will always create valid objects, 57 | to prevent the need for checking initialisation status in all code 58 | paths; this also (implicitly) fixes memory leaks on re-initialisation 59 | (calling `__init__(…)` on an existing object) and segfaults (!) on 60 | non-initialised object attribute access. Note ACL re-initialisation is 61 | tricky and (still) leads to undefined behaviour of existing Entry 62 | objects pointing to it. 63 | - Fix another bug in ACL re-initialisation where failures would result 64 | in invalid objects; now failed re-initialisation does not touch the 65 | original object. 66 | - Restore `__setstate__`/`__getstate__` support on Linux; this was 67 | inadvertently removed due a typo(!) when adding support for it in 68 | FreeBSD. Pickle should work again for ACL instances, although not sure 69 | how stable this serialisation format actually is. 70 | - Additionally, slightly change `__setstate__()` input to not allow 71 | Unicode, since the serialisation format is an opaque binary format. 72 | - Fix (and change) entry qualifier (which is a user/group ID) behaviour: 73 | assume/require that uid_t/gid_t are unsigned types (they are with 74 | glibc, MacOS and FreeBSD at least; the standard doesn't document the 75 | signedness), and convert parsing and returning the qualifier to behave 76 | accordingly. The breakage was most apparent on 32-bit architectures, 77 | in which context the problem was originally reported (see issue #13). 78 | 79 | Minor improvements: 80 | 81 | - Added a `data` keyword argument to `ACL()`, which allows restoring an 82 | ACL directly from a serialised form (as given by `__getstate__()`), 83 | which should simplify some uses cases (`a = ACL(); a.__set 84 | state__(…)`). 85 | - When available, add the file path to I/O error messages, which should 86 | lead to easier debugging. 87 | - The test suite has changed to `pytest`, which allows increased 88 | coverage via parameterisation. 89 | 90 | ## Version 0.5.4 91 | 92 | *released Thu, 14 Nov 2019* 93 | 94 | Maintenance release: 95 | 96 | - Switch build system to Python 3 by default (can be overridden if 97 | needed). 98 | - Internal improvements for better cpychecker support. 99 | - Fix compatibility with PyPy. 100 | - Test improvements (both local and on Travis), testing more variations 101 | (debug, PyPy). 102 | - Improve test coverage, and allow gathering test coverage results. 103 | - Drop support (well, drop testing) for Python lower than 2.7. 104 | - Minor documentation improvements (closes #9, #12). 105 | 106 | ## Version 0.5.3 107 | 108 | *released Thu, 30 Apr 2015* 109 | 110 | FreeBSD fixes: 111 | 112 | - Enable all FreeBSD versions after 7.x at level 2 (thanks to Garrett 113 | Cooper). 114 | - Make test suite pass under FreeBSD, which has a stricter behaviour 115 | with regards to invalid ACLs (which we do exercise in the test suite), 116 | thanks again to Garret for the bug reports. 117 | 118 | ## Version 0.5.2 119 | 120 | *released Sat, 24 May 2014* 121 | 122 | No visible changes release: just fix tests when running under pypy. 123 | 124 | ## Version 0.5.1 125 | 126 | *released Sun, 13 May 2012* 127 | 128 | A bug-fix only release. Critical bugs (memory leaks and possible 129 | segmentation faults) have been fixed thanks to Dave Malcolm and his 130 | ``cpychecker`` tool. Additionally, some compatibility issues with Python 131 | 3.x have been fixed (str() methods returning bytes). 132 | 133 | The documentation has been improved and changed from epydoc to sphinx; 134 | note however that the documentation is still auto-generated from the 135 | docstrings. 136 | 137 | Project reorganisation: the project home page has been moved from 138 | SourceForge to GitHub. 139 | 140 | ## Version 0.5 141 | 142 | *released Sun, 27 Dec 2009* 143 | 144 | Added support for Python 3.x and improved support for Unicode filenames. 145 | 146 | ## Version 0.4 147 | 148 | *released Sat, 28 Jun 2008* 149 | 150 | ### License 151 | 152 | 153 | Starting with this version, pylibacl is licensed under LGPL 2.1, 154 | Febryary 1999 or any later versions (see README.rst and COPYING). 155 | 156 | ### Linux support 157 | 158 | A few more Linux-specific functions: 159 | 160 | - add the ACL.equiv_mode() method, which will return the equivalent 161 | octal mode if this is a basic ACL and raise an IOError exception 162 | otherwise 163 | 164 | - add the acl_extended(...) function, which will check if an fd or path 165 | has an extended ACL 166 | 167 | ### FreeBSD support 168 | 169 | FreeBSD 7.x will have almost all the acl manipulation functions that 170 | Linux has, with the exception of __getstate__/__setstate__. As a 171 | workaround, use the str() and ACL(text=...) methods to pass around 172 | textual representations. 173 | 174 | ### Interface 175 | 176 | At module level there are now a few constants exported for easy-checking 177 | at runtime what features have been compiled in: 178 | 179 | - `HAS_ACL_FROM_MODE`, denoting whether the ACL constructor supports 180 | the `mode=0xxx` parameter 181 | 182 | - `HAS_ACL_CHECK`, denoting whether ACL instances support the 183 | `check()` method 184 | 185 | - `HAS_ACL_ENTRY`, denoting whether ACL manipulation is possible and 186 | the Entry and Permset classes are available 187 | 188 | - `HAS_EXTENEDED_CHECK`, denoting whether the `acl_extended()` 189 | function is supported 190 | 191 | - `HAS_EQUIV_MODE`, denoting whether ACL instances support the 192 | `equiv_mode()` method 193 | 194 | ### Internals 195 | 196 | Many functions have now unittests, which is a good thing. 197 | 198 | 199 | ## Version 0.3 200 | 201 | *released Sun, 21 Oct 2007* 202 | 203 | ### Linux support 204 | 205 | Under Linux, implement more functions from libacl: 206 | 207 | - add `ACL(mode=...)`, implementing `acl_from_mode`. 208 | - add `ACL.to_any_text()`, implementing `acl_to_any_text`. 209 | - add ACL comparison, using `acl_cmp`. 210 | - add `ACL.check()`, which is a more descriptive function than 211 | validate. 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pylibacl 2 | 3 | This is a Python 3.7+ extension module allows you to manipulate the 4 | POSIX.1e Access Control Lists present in some OS/file-systems 5 | combinations. 6 | 7 | Downloads: go to . Latest 8 | version is 0.7.2. The source repository is either at 9 | or at 10 | . 11 | 12 | For any issues, please file bugs at 13 | . 14 | 15 | See the `CONTRIBUTING.md` file for details on how to contribute, or 16 | support me on [ko-fi](https://ko-fi.com/iustin). 17 | 18 | [![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) 19 | [![Codecov](https://img.shields.io/codecov/c/github/iustin/pylibacl)](https://codecov.io/gh/iustin/pylibacl) 20 | [![Read the Docs](https://img.shields.io/readthedocs/pylibacl)](http://pylibacl.readthedocs.io/en/latest/?badge=latest) 21 | [![GitHub issues](https://img.shields.io/github/issues/iustin/pylibacl)](https://github.com/iustin/pylibacl/issues) 22 | ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/iustin/pylibacl) 23 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/iustin/pylibacl)](https://github.com/iustin/pylibacl/releases) 24 | [![PyPI](https://img.shields.io/pypi/v/pylibacl)](https://pypi.org/project/pylibacl/) 25 | ![Debian package](https://img.shields.io/debian/v/python-pylibacl) 26 | ![Ubuntu package](https://img.shields.io/ubuntu/v/python-pylibacl) 27 | ![GitHub Release Date](https://img.shields.io/github/release-date/iustin/pylibacl) 28 | ![GitHub commits since latest release](https://img.shields.io/github/commits-since/iustin/pylibacl/latest) 29 | ![GitHub last commit](https://img.shields.io/github/last-commit/iustin/pylibacl) 30 | 31 | ## Requirements 32 | 33 | pylibacl has been written and tested on Linux, kernel v2.4 or newer, 34 | with XFS filesystems; ext2/ext3 should also work. Since release 0.4.0, 35 | FreeBSD 7 also has quite good support. If any other platform 36 | implements the POSIX.1e draft, pylibacl can be used. I heard that 37 | Solaris does, but I can't test it. 38 | 39 | - Python 3.7 or newer. Python 2.4+ was supported in the 0.5.x branch, 40 | Python 3.4+ in the 0.6 branch. 41 | - Operating system: 42 | - Linux, kernel v2.4 or newer, and the libacl library and 43 | development packages (all modern distributions should have this, 44 | under various names); also the file-systems you use must have 45 | ACLs turned on, either as a compile or mount option. 46 | - FreeBSD 7.0 or newer. 47 | - The sphinx python module, for your python version, if building the 48 | documentation. 49 | 50 | ## FreeBSD 51 | 52 | Note that on FreeBSD, ACLs are not enabled by default (at least on UFS 53 | file systems). To enable them, run `tunefs -a enabled` on the file 54 | system in question (after mounting it read-only). Then install: 55 | 56 | - `pkg install py36-setuptools py36-sphinx` 57 | 58 | or: 59 | 60 | - `pkg install py37-setuptools` 61 | 62 | ## Security 63 | 64 | For reporting security vulnerabilities, please see `SECURITY.md`. 65 | 66 | ## License 67 | 68 | pylibacl is Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop. 69 | 70 | pylibacl is free software; you can redistribute it and/or modify it under the 71 | terms of the GNU Lesser General Public License as published by the Free 72 | Software Foundation; either version 2.1 of the License, or (at your option) any 73 | later version. See the COPYING file for the full license terms. 74 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a (potential or confirmed) security issue, please email 4 | with a description of the issue, steps to reproduce 5 | it, affected versions, and if known, mitigations for the issue. 6 | 7 | Since this is a small project, there's no list of supported 8 | versions. I will attempt to reply to reports within a working week, 9 | and to fix and disclose vulnerabilities within 90 days, but this is 10 | not a guarantee. 11 | 12 | Optionally, you can encrypt the email with my GPG key, see for details 13 | . 14 | 15 | Alternatively, you can use the GitHub "Private vulnerability 16 | reporting" functionality (but note this is beta). 17 | -------------------------------------------------------------------------------- /acl.c: -------------------------------------------------------------------------------- 1 | /* 2 | posix1e - a python module exposing the posix acl functions 3 | 4 | Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this library; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19 | 02110-1301 USA 20 | 21 | */ 22 | 23 | #define PY_SSIZE_T_CLEAN 24 | #include 25 | 26 | #include 27 | #include 28 | 29 | #ifdef HAVE_LINUX 30 | #include 31 | #define get_perm acl_get_perm 32 | #elif HAVE_FREEBSD 33 | #define get_perm acl_get_perm_np 34 | #endif 35 | 36 | /* Used for cpychecker: */ 37 | /* The checker automatically defines this preprocessor name when creating 38 | the custom attribute: */ 39 | #if defined(WITH_CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF_ATTRIBUTE) 40 | #define CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF(typename) \ 41 | __attribute__((cpychecker_type_object_for_typedef(typename))) 42 | #else 43 | /* This handles the case where we're compiling with a "vanilla" 44 | compiler that doesn't supply this attribute: */ 45 | #define CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF(typename) 46 | #endif 47 | 48 | /* The checker automatically defines this preprocessor name when creating 49 | the custom attribute: */ 50 | #if defined(WITH_CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION_ATTRIBUTE) 51 | #define CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION \ 52 | __attribute__((cpychecker_negative_result_sets_exception)) 53 | #else 54 | #define CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION 55 | #endif 56 | 57 | static PyTypeObject ACL_Type 58 | CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF("ACL_Object"); 59 | static PyObject* ACL_applyto(PyObject* obj, PyObject* args); 60 | static PyObject* ACL_valid(PyObject* obj, PyObject* args); 61 | 62 | #ifdef HAVE_ACL_COPY_EXT 63 | static PyObject* ACL_get_state(PyObject *obj, PyObject* args); 64 | static PyObject* ACL_set_state(PyObject *obj, PyObject* args); 65 | #endif 66 | 67 | #ifdef HAVE_LEVEL2 68 | static PyTypeObject Entry_Type 69 | CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF("Entry_Object"); 70 | static PyTypeObject Permset_Type 71 | CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF("Permset_Object"); 72 | static PyObject* Permset_new(PyTypeObject* type, PyObject* args, 73 | PyObject *keywds); 74 | #endif 75 | 76 | static acl_perm_t holder_ACL_EXECUTE = ACL_EXECUTE; 77 | static acl_perm_t holder_ACL_READ = ACL_READ; 78 | static acl_perm_t holder_ACL_WRITE = ACL_WRITE; 79 | 80 | typedef struct { 81 | PyObject_HEAD 82 | acl_t acl; 83 | #ifdef HAVE_LEVEL2 84 | int entry_id; 85 | #endif 86 | } ACL_Object; 87 | 88 | #ifdef HAVE_LEVEL2 89 | 90 | typedef struct { 91 | PyObject_HEAD 92 | PyObject *parent_acl; /* The parent acl, so it won't run out on us */ 93 | acl_entry_t entry; 94 | } Entry_Object; 95 | 96 | typedef struct { 97 | PyObject_HEAD 98 | PyObject *parent_entry; /* The parent entry, so it won't run out on us */ 99 | acl_permset_t permset; 100 | } Permset_Object; 101 | 102 | #endif 103 | 104 | /* Creation of a new ACL instance */ 105 | static PyObject* ACL_new(PyTypeObject* type, PyObject* args, 106 | PyObject *keywds) { 107 | PyObject* newacl; 108 | ACL_Object *acl; 109 | 110 | newacl = type->tp_alloc(type, 0); 111 | 112 | if(newacl == NULL) { 113 | return NULL; 114 | } 115 | acl = (ACL_Object*) newacl; 116 | 117 | acl->acl = acl_init(0); 118 | if (acl->acl == NULL) { 119 | /* LCOV_EXCL_START */ 120 | PyErr_SetFromErrno(PyExc_IOError); 121 | Py_DECREF(newacl); 122 | return NULL; 123 | /* LCOV_EXCL_STOP */ 124 | } 125 | #ifdef HAVE_LEVEL2 126 | acl->entry_id = ACL_FIRST_ENTRY; 127 | #endif 128 | 129 | return newacl; 130 | } 131 | 132 | /* Initialization of a new ACL instance */ 133 | static int ACL_init(PyObject* obj, PyObject* args, PyObject *keywds) { 134 | ACL_Object* self = (ACL_Object*) obj; 135 | static char *kwlist[] = { "file", "fd", "text", "acl", "filedef", 136 | #ifdef HAVE_LINUX 137 | "mode", 138 | #endif 139 | #ifdef HAVE_ACL_COPY_EXT 140 | "data", 141 | #endif 142 | NULL }; 143 | char *format = "|O&OsO!O&" 144 | #ifdef HAVE_LINUX 145 | "i" 146 | #endif 147 | #ifdef HAVE_ACL_COPY_EXT 148 | "y#" 149 | #endif 150 | ; 151 | acl_t new = NULL; 152 | #ifdef HAVE_LINUX 153 | int mode = -1; 154 | #endif 155 | PyObject *file = NULL; 156 | PyObject *filedef = NULL; 157 | char *text = NULL; 158 | PyObject *fd = NULL; 159 | ACL_Object* thesrc = NULL; 160 | #ifdef HAVE_ACL_COPY_EXT 161 | const void *buf = NULL; 162 | Py_ssize_t bufsize; 163 | #endif 164 | int set_err = 0; 165 | 166 | if(!PyTuple_Check(args) || PyTuple_Size(args) != 0 || 167 | (keywds != NULL && PyDict_Check(keywds) && PyDict_Size(keywds) > 1)) { 168 | PyErr_SetString(PyExc_ValueError, "a max of one keyword argument" 169 | " must be passed"); 170 | return -1; 171 | } 172 | if(!PyArg_ParseTupleAndKeywords(args, keywds, format, kwlist, 173 | PyUnicode_FSConverter, &file, 174 | &fd, &text, &ACL_Type, &thesrc, 175 | PyUnicode_FSConverter, &filedef 176 | #ifdef HAVE_LINUX 177 | , &mode 178 | #endif 179 | #ifdef HAVE_ACL_COPY_EXT 180 | , &buf, &bufsize 181 | #endif 182 | )) 183 | return -1; 184 | 185 | if(file != NULL) { 186 | char *path = PyBytes_AS_STRING(file); 187 | new = acl_get_file(path, ACL_TYPE_ACCESS); 188 | // Set custom exception on this failure path which includes 189 | // the filename. 190 | if (new == NULL) { 191 | PyErr_SetFromErrnoWithFilename(PyExc_IOError, path); 192 | set_err = 1; 193 | } 194 | Py_DECREF(file); 195 | } else if(text != NULL) 196 | new = acl_from_text(text); 197 | else if(fd != NULL) { 198 | int fdval; 199 | if ((fdval = PyObject_AsFileDescriptor(fd)) != -1) { 200 | new = acl_get_fd(fdval); 201 | } 202 | } else if(thesrc != NULL) 203 | new = acl_dup(thesrc->acl); 204 | else if(filedef != NULL) { 205 | char *path = PyBytes_AS_STRING(filedef); 206 | new = acl_get_file(path, ACL_TYPE_DEFAULT); 207 | // Set custom exception on this failure path which includes 208 | // the filename. 209 | if (new == NULL) { 210 | PyErr_SetFromErrnoWithFilename(PyExc_IOError, path); 211 | set_err = 1; 212 | } 213 | Py_DECREF(filedef); 214 | } 215 | #ifdef HAVE_LINUX 216 | else if(mode != -1) 217 | new = acl_from_mode(mode); 218 | #endif 219 | #ifdef HAVE_ACL_COPY_EXT 220 | else if(buf != NULL) { 221 | new = acl_copy_int(buf); 222 | } 223 | #endif 224 | else 225 | new = acl_init(0); 226 | 227 | if(new == NULL) { 228 | if (!set_err) { 229 | PyErr_SetFromErrno(PyExc_IOError); 230 | } 231 | return -1; 232 | } 233 | 234 | /* Free the old acl_t without checking for error, we don't 235 | * care right now */ 236 | if(self->acl != NULL) 237 | acl_free(self->acl); 238 | 239 | self->acl = new; 240 | 241 | return 0; 242 | } 243 | 244 | /* Standard type functions */ 245 | static void ACL_dealloc(PyObject* obj) { 246 | ACL_Object *self = (ACL_Object*) obj; 247 | PyObject *err_type, *err_value, *err_traceback; 248 | 249 | PyErr_Fetch(&err_type, &err_value, &err_traceback); 250 | if(self->acl != NULL && acl_free(self->acl) != 0) 251 | PyErr_WriteUnraisable(obj); /* LCOV_EXCL_LINE */ 252 | PyErr_Restore(err_type, err_value, err_traceback); 253 | Py_TYPE(obj)->tp_free(obj); 254 | } 255 | 256 | /* Converts the acl to a text format */ 257 | static PyObject* ACL_str(PyObject *obj) { 258 | char *text; 259 | ACL_Object *self = (ACL_Object*) obj; 260 | PyObject *ret; 261 | 262 | text = acl_to_text(self->acl, NULL); 263 | if(text == NULL) { 264 | /* LCOV_EXCL_START */ 265 | return PyErr_SetFromErrno(PyExc_IOError); 266 | /* LCOV_EXCL_STOP */ 267 | } 268 | ret = PyUnicode_FromString(text); 269 | if(acl_free(text) != 0) { 270 | /* LCOV_EXCL_START */ 271 | Py_XDECREF(ret); 272 | return PyErr_SetFromErrno(PyExc_IOError); 273 | /* LCOV_EXCL_STOP */ 274 | } 275 | return ret; 276 | } 277 | 278 | #ifdef HAVE_LINUX 279 | static char __to_any_text_doc__[] = 280 | "to_any_text([prefix='', separator='n', options=0])\n" 281 | "Convert the ACL to a custom text format.\n" 282 | "\n" 283 | "This method encapsulates the ``acl_to_any_text()`` function.\n" 284 | "It allows a customized text format to be generated for the ACL. See\n" 285 | ":manpage:`acl_to_any_text(3)` for more details.\n" 286 | "\n" 287 | ":param string prefix: if given, this string will be pre-pended to\n" 288 | " all lines\n" 289 | ":param string separator: a single character (defaults to '\\n'); this will" 290 | " be used to separate the entries in the ACL\n" 291 | ":param options: a bitwise combination of:\n\n" 292 | " - :py:data:`TEXT_ABBREVIATE`: use 'u' instead of 'user', 'g' \n" 293 | " instead of 'group', etc.\n" 294 | " - :py:data:`TEXT_NUMERIC_IDS`: User and group IDs are included as\n" 295 | " decimal numbers instead of names\n" 296 | " - :py:data:`TEXT_SOME_EFFECTIVE`: Include comments denoting the\n" 297 | " effective permissions when some are masked\n" 298 | " - :py:data:`TEXT_ALL_EFFECTIVE`: Include comments after all ACL\n" 299 | " entries affected by an ACL_MASK entry\n" 300 | " - :py:data:`TEXT_SMART_INDENT`: Used in combination with the\n" 301 | " _EFFECTIVE options, this will ensure that comments are aligned\n" 302 | " to the fourth tab position (assuming one tab equals eight spaces)\n" 303 | ":rtype: string\n" 304 | ; 305 | 306 | /* Converts the acl to a custom text format */ 307 | static PyObject* ACL_to_any_text(PyObject *obj, PyObject *args, 308 | PyObject *kwds) { 309 | char *text; 310 | ACL_Object *self = (ACL_Object*) obj; 311 | PyObject *ret; 312 | const char *arg_prefix = NULL; 313 | char arg_separator = '\n'; 314 | int arg_options = 0; 315 | static char *kwlist[] = {"prefix", "separator", "options", NULL}; 316 | 317 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "|sci", kwlist, &arg_prefix, 318 | &arg_separator, &arg_options)) 319 | return NULL; 320 | 321 | text = acl_to_any_text(self->acl, arg_prefix, arg_separator, arg_options); 322 | if(text == NULL) { 323 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 324 | } 325 | ret = PyBytes_FromString(text); 326 | if(acl_free(text) != 0) { 327 | /* LCOV_EXCL_START */ 328 | Py_XDECREF(ret); 329 | return PyErr_SetFromErrno(PyExc_IOError); 330 | /* LCOV_EXCL_STOP */ 331 | } 332 | return ret; 333 | } 334 | 335 | static char __check_doc__[] = 336 | "Check the ACL validity.\n" 337 | "\n" 338 | "This is a non-portable, Linux specific extension that allow more\n" 339 | "information to be retrieved in case an ACL is not valid than via the\n" 340 | ":py:func:`valid` method.\n" 341 | "\n" 342 | "This method will return either False (the ACL is valid), or a tuple\n" 343 | "with two elements. The first element is one of the following\n" 344 | "constants:\n\n" 345 | " - :py:data:`ACL_MULTI_ERROR`: The ACL contains multiple entries that\n" 346 | " have a tag type that may occur at most once\n" 347 | " - :py:data:`ACL_DUPLICATE_ERROR`: The ACL contains multiple \n" 348 | " :py:data:`ACL_USER` or :py:data:`ACL_GROUP` entries with the\n" 349 | " same ID\n" 350 | " - :py:data:`ACL_MISS_ERROR`: A required entry is missing\n" 351 | " - :py:data:`ACL_ENTRY_ERROR`: The ACL contains an invalid entry\n" 352 | " tag type\n" 353 | "\n" 354 | "The second element of the tuple is the index of the entry that is\n" 355 | "invalid (in the same order as by iterating over the ACL entry)\n" 356 | ; 357 | 358 | /* The acl_check method */ 359 | static PyObject* ACL_check(PyObject* obj, PyObject* args) { 360 | ACL_Object *self = (ACL_Object*) obj; 361 | int result; 362 | int eindex; 363 | 364 | if((result = acl_check(self->acl, &eindex)) == -1) 365 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 366 | if(result == 0) { 367 | Py_RETURN_FALSE; 368 | } 369 | return Py_BuildValue("(ii)", result, eindex); 370 | } 371 | 372 | /* Implementation of the rich compare for ACLs */ 373 | static PyObject* ACL_richcompare(PyObject* o1, PyObject* o2, int op) { 374 | ACL_Object *acl1, *acl2; 375 | int n; 376 | PyObject *ret; 377 | 378 | if(!PyObject_IsInstance(o2, (PyObject*)&ACL_Type)) { 379 | if(op == Py_EQ) 380 | Py_RETURN_FALSE; 381 | if(op == Py_NE) 382 | Py_RETURN_TRUE; 383 | PyErr_SetString(PyExc_TypeError, "can only compare to an ACL"); 384 | return NULL; 385 | } 386 | 387 | acl1 = (ACL_Object*)o1; 388 | acl2 = (ACL_Object*)o2; 389 | if((n=acl_cmp(acl1->acl, acl2->acl))==-1) 390 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 391 | switch(op) { 392 | case Py_EQ: 393 | ret = n == 0 ? Py_True : Py_False; 394 | break; 395 | case Py_NE: 396 | ret = n == 1 ? Py_True : Py_False; 397 | break; 398 | default: 399 | PyErr_SetString(PyExc_TypeError, "ACLs are not orderable"); 400 | return NULL; 401 | } 402 | Py_INCREF(ret); 403 | return ret; 404 | } 405 | 406 | static char __equiv_mode_doc__[] = 407 | "Return the octal mode the ACL is equivalent to.\n" 408 | "\n" 409 | "This is a non-portable, Linux specific extension that checks\n" 410 | "if the ACL is a basic ACL and returns the corresponding mode.\n" 411 | "\n" 412 | ":rtype: integer\n" 413 | ":raise IOError: An IOerror exception will be raised if the ACL is\n" 414 | " an extended ACL.\n" 415 | ; 416 | 417 | /* The acl_equiv_mode method */ 418 | static PyObject* ACL_equiv_mode(PyObject* obj, PyObject* args) { 419 | ACL_Object *self = (ACL_Object*) obj; 420 | mode_t mode; 421 | 422 | if(acl_equiv_mode(self->acl, &mode) == -1) 423 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 424 | return PyLong_FromLong(mode); 425 | } 426 | #endif 427 | 428 | /* Custom methods */ 429 | static char __applyto_doc__[] = 430 | "applyto(item[, flag=ACL_TYPE_ACCESS])\n" 431 | "Apply the ACL to a file or filehandle.\n" 432 | "\n" 433 | ":param item: either a filename or a file-like object or an integer;\n" 434 | " this represents the filesystem object on which to act\n" 435 | ":param flag: optional flag representing the type of ACL to set, either\n" 436 | " :py:data:`ACL_TYPE_ACCESS` (default) or :py:data:`ACL_TYPE_DEFAULT`\n" 437 | ; 438 | 439 | /* Applies the ACL to a file */ 440 | static PyObject* ACL_applyto(PyObject* obj, PyObject* args) { 441 | ACL_Object *self = (ACL_Object*) obj; 442 | PyObject *target, *tmp; 443 | acl_type_t type = ACL_TYPE_ACCESS; 444 | int nret; 445 | int fd; 446 | 447 | if (!PyArg_ParseTuple(args, "O|I", &target, &type)) 448 | return NULL; 449 | if ((fd = PyObject_AsFileDescriptor(target)) != -1) { 450 | if((nret = acl_set_fd(fd, self->acl)) == -1) { 451 | PyErr_SetFromErrno(PyExc_IOError); 452 | } 453 | } else { 454 | // PyObject_AsFileDescriptor sets an error when failing, so clear 455 | // it such that further code works; some method lookups fail if an 456 | // error already occured when called, which breaks at least 457 | // PyOS_FSPath (called by FSConverter). 458 | PyErr_Clear(); 459 | if(PyUnicode_FSConverter(target, &tmp)) { 460 | char *filename = PyBytes_AS_STRING(tmp); 461 | if ((nret = acl_set_file(filename, type, self->acl)) == -1) { 462 | PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename); 463 | } 464 | Py_DECREF(tmp); 465 | } else { 466 | nret = -1; 467 | } 468 | } 469 | if (nret < 0) { 470 | return NULL; 471 | } else { 472 | Py_RETURN_NONE; 473 | } 474 | } 475 | 476 | static char __valid_doc__[] = 477 | "Test the ACL for validity.\n" 478 | "\n" 479 | "This method tests the ACL to see if it is a valid ACL\n" 480 | "in terms of the file-system. More precisely, it checks that:\n" 481 | "\n" 482 | "The ACL contains exactly one entry with each of the\n" 483 | ":py:data:`ACL_USER_OBJ`, :py:data:`ACL_GROUP_OBJ`, and \n" 484 | ":py:data:`ACL_OTHER` tag types. Entries\n" 485 | "with :py:data:`ACL_USER` and :py:data:`ACL_GROUP` tag types may\n" 486 | "appear zero or more\n" 487 | "times in an ACL. An ACL that contains entries of :py:data:`ACL_USER` or\n" 488 | ":py:data:`ACL_GROUP` tag types must contain exactly one entry of the \n" 489 | ":py:data:`ACL_MASK` tag type. If an ACL contains no entries of\n" 490 | ":py:data:`ACL_USER` or :py:data:`ACL_GROUP` tag types, the\n" 491 | ":py:data:`ACL_MASK` entry is optional.\n" 492 | "\n" 493 | "All user ID qualifiers must be unique among all entries of\n" 494 | "the :py:data:`ACL_USER` tag type, and all group IDs must be unique\n" 495 | "among all entries of :py:data:`ACL_GROUP` tag type.\n" 496 | "\n" 497 | "The method will return 1 for a valid ACL and 0 for an invalid one.\n" 498 | "This has been chosen because the specification for\n" 499 | ":manpage:`acl_valid(3)`\n" 500 | "in the POSIX.1e standard documents only one possible value for errno\n" 501 | "in case of an invalid ACL, so we can't differentiate between\n" 502 | "classes of errors. Other suggestions are welcome.\n" 503 | "\n" 504 | ":return: 0 or 1\n" 505 | ":rtype: integer\n" 506 | ; 507 | 508 | /* Checks the ACL for validity */ 509 | static PyObject* ACL_valid(PyObject* obj, PyObject* args) { 510 | ACL_Object *self = (ACL_Object*) obj; 511 | 512 | if(acl_valid(self->acl) == -1) { 513 | Py_RETURN_FALSE; 514 | } else { 515 | Py_RETURN_TRUE; 516 | } 517 | } 518 | 519 | #ifdef HAVE_ACL_COPY_EXT 520 | static PyObject* ACL_get_state(PyObject *obj, PyObject* args) { 521 | ACL_Object *self = (ACL_Object*) obj; 522 | PyObject *ret; 523 | ssize_t size, nsize; 524 | char *buf; 525 | 526 | size = acl_size(self->acl); 527 | if(size == -1) 528 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 529 | 530 | if((ret = PyBytes_FromStringAndSize(NULL, size)) == NULL) 531 | return NULL; 532 | buf = PyBytes_AsString(ret); 533 | 534 | if((nsize = acl_copy_ext(buf, self->acl, size)) == -1) { 535 | /* LCOV_EXCL_START */ 536 | Py_DECREF(ret); 537 | return PyErr_SetFromErrno(PyExc_IOError); 538 | /* LCOV_EXCL_STOP */ 539 | } 540 | 541 | return ret; 542 | } 543 | 544 | static PyObject* ACL_set_state(PyObject *obj, PyObject* args) { 545 | ACL_Object *self = (ACL_Object*) obj; 546 | const void *buf; 547 | Py_ssize_t bufsize; 548 | acl_t ptr; 549 | 550 | /* Parse the argument */ 551 | if (!PyArg_ParseTuple(args, "y#", &buf, &bufsize)) 552 | return NULL; 553 | 554 | /* Try to import the external representation */ 555 | if((ptr = acl_copy_int(buf)) == NULL) 556 | return PyErr_SetFromErrno(PyExc_IOError); 557 | 558 | if(self->acl != NULL) { 559 | /* Ignore errors in freeing the previous acl. We already 560 | allocated the new acl, and the state of the previous one is 561 | suspect if freeing failed (in Linux's libacl, deallocating 562 | a valid ACL can't actually happen, so this path is 563 | unlikely. */ 564 | acl_free(self->acl); /* LCOV_EXCL_LINE */ 565 | } 566 | 567 | self->acl = ptr; 568 | 569 | Py_RETURN_NONE; 570 | } 571 | #endif 572 | 573 | #ifdef HAVE_LEVEL2 574 | 575 | /* tp_iter for the ACL type; since it can be iterated only 576 | * destructively, the type is its iterator 577 | */ 578 | static PyObject* ACL_iter(PyObject *obj) { 579 | ACL_Object *self = (ACL_Object*)obj; 580 | self->entry_id = ACL_FIRST_ENTRY; 581 | Py_INCREF(obj); 582 | return obj; 583 | } 584 | 585 | /* the tp_iternext function for the ACL type */ 586 | static PyObject* ACL_iternext(PyObject *obj) { 587 | ACL_Object *self = (ACL_Object*)obj; 588 | acl_entry_t the_entry_t; 589 | Entry_Object *the_entry_obj; 590 | int nerr; 591 | 592 | nerr = acl_get_entry(self->acl, self->entry_id, &the_entry_t); 593 | self->entry_id = ACL_NEXT_ENTRY; 594 | if(nerr == -1) 595 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 596 | else if(nerr == 0) { 597 | /* Docs says this is not needed */ 598 | /*PyErr_SetObject(PyExc_StopIteration, Py_None);*/ 599 | return NULL; 600 | } 601 | 602 | the_entry_obj = (Entry_Object*) PyType_GenericNew(&Entry_Type, NULL, NULL); 603 | if(the_entry_obj == NULL) 604 | return NULL; 605 | 606 | the_entry_obj->entry = the_entry_t; 607 | 608 | the_entry_obj->parent_acl = obj; 609 | Py_INCREF(obj); /* For the reference we have in entry->parent */ 610 | 611 | return (PyObject*)the_entry_obj; 612 | } 613 | 614 | static char __ACL_delete_entry_doc__[] = 615 | "delete_entry(entry)\n" 616 | "Deletes an entry from the ACL.\n" 617 | "\n" 618 | ".. note:: Only available with level 2.\n" 619 | "\n" 620 | ":param entry: the Entry object which should be deleted; note that after\n" 621 | " this function is called, that object is unusable any longer\n" 622 | " and should be deleted\n" 623 | ; 624 | 625 | /* Deletes an entry from the ACL */ 626 | static PyObject* ACL_delete_entry(PyObject *obj, PyObject *args) { 627 | ACL_Object *self = (ACL_Object*)obj; 628 | Entry_Object *e; 629 | 630 | if (!PyArg_ParseTuple(args, "O!", &Entry_Type, &e)) 631 | return NULL; 632 | 633 | if (e->parent_acl != obj) { 634 | PyErr_SetString(PyExc_ValueError, 635 | "Can't remove un-owned entry"); 636 | return NULL; 637 | } 638 | if(acl_delete_entry(self->acl, e->entry) == -1) 639 | return PyErr_SetFromErrno(PyExc_IOError); 640 | 641 | Py_RETURN_NONE; 642 | } 643 | 644 | static char __ACL_calc_mask_doc__[] = 645 | "Compute the file group class mask.\n" 646 | "\n" 647 | "The calc_mask() method calculates and sets the permissions \n" 648 | "associated with the :py:data:`ACL_MASK` Entry of the ACL.\n" 649 | "The value of the new permissions is the union of the permissions \n" 650 | "granted by all entries of tag type :py:data:`ACL_GROUP`, \n" 651 | ":py:data:`ACL_GROUP_OBJ`, or \n" 652 | ":py:data:`ACL_USER`. If the ACL already contains an :py:data:`ACL_MASK`\n" 653 | "entry, its \n" 654 | "permissions are overwritten; if it does not contain an \n" 655 | ":py:data:`ACL_MASK` Entry, one is added.\n" 656 | "\n" 657 | "The order of existing entries in the ACL is undefined after this \n" 658 | "function.\n" 659 | ; 660 | 661 | /* Updates the mask entry in the ACL */ 662 | static PyObject* ACL_calc_mask(PyObject *obj, PyObject *args) { 663 | ACL_Object *self = (ACL_Object*)obj; 664 | 665 | if(acl_calc_mask(&self->acl) == -1) 666 | return PyErr_SetFromErrno(PyExc_IOError); 667 | 668 | Py_RETURN_NONE; 669 | } 670 | 671 | static char __ACL_append_doc__[] = 672 | "append([entry])\n" 673 | "Append a new Entry to the ACL and return it.\n" 674 | "\n" 675 | "This is a convenience function to create a new Entry \n" 676 | "and append it to the ACL.\n" 677 | "If a parameter of type Entry instance is given, the \n" 678 | "entry will be a copy of that one (as if copied with \n" 679 | ":py:func:`Entry.copy`), otherwise, the new entry will be empty.\n" 680 | "\n" 681 | ":rtype: :py:class:`Entry`\n" 682 | ":returns: the newly created entry\n" 683 | ; 684 | 685 | /* Convenience method to create a new Entry */ 686 | static PyObject* ACL_append(PyObject *obj, PyObject *args) { 687 | Entry_Object* newentry; 688 | Entry_Object* oldentry = NULL; 689 | int nret; 690 | 691 | if (!PyArg_ParseTuple(args, "|O!", &Entry_Type, &oldentry)) { 692 | return NULL; 693 | } 694 | 695 | PyObject *new_arglist = Py_BuildValue("(O)", obj); 696 | if (new_arglist == NULL) { 697 | return NULL; 698 | } 699 | newentry = (Entry_Object*) PyObject_CallObject((PyObject*)&Entry_Type, new_arglist); 700 | Py_DECREF(new_arglist); 701 | if(newentry == NULL) { 702 | return NULL; 703 | } 704 | 705 | if(oldentry != NULL) { 706 | nret = acl_copy_entry(newentry->entry, oldentry->entry); 707 | if(nret == -1) { 708 | /* LCOV_EXCL_START */ 709 | Py_DECREF(newentry); 710 | return PyErr_SetFromErrno(PyExc_IOError); 711 | /* LCOV_EXCL_STOP */ 712 | } 713 | } 714 | 715 | return (PyObject*)newentry; 716 | } 717 | 718 | /***** Entry type *****/ 719 | 720 | typedef struct { 721 | acl_tag_t tag; 722 | union { 723 | uid_t uid; 724 | gid_t gid; 725 | }; 726 | } tag_qual; 727 | 728 | /* Pre-declaring the function is more friendly to cpychecker, sigh. */ 729 | static int get_tag_qualifier(acl_entry_t entry, tag_qual *tq) 730 | CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; 731 | 732 | /* Helper function to get the tag and qualifier of an Entry at the 733 | same time. This is "needed" because the acl_get_qualifier function 734 | returns a pointer to different types, based on the tag value, and 735 | thus it's not straightforward to get the right type. 736 | 737 | It sets a Python exception if an error occurs, and returns -1 in 738 | this case. If successful, the tag is set to the tag type, the 739 | qualifier (if any) to either the uid or the gid entry in the 740 | tag_qual structure, and the return value is 0. 741 | */ 742 | static int get_tag_qualifier(acl_entry_t entry, tag_qual *tq) { 743 | void *p; 744 | 745 | if(acl_get_tag_type(entry, &tq->tag) == -1) { 746 | /* LCOV_EXCL_START */ 747 | PyErr_SetFromErrno(PyExc_IOError); 748 | return -1; 749 | /* LCOV_EXCL_STOP */ 750 | } 751 | if (tq->tag == ACL_USER || tq->tag == ACL_GROUP) { 752 | if((p = acl_get_qualifier(entry)) == NULL) { 753 | /* LCOV_EXCL_START */ 754 | PyErr_SetFromErrno(PyExc_IOError); 755 | return -1; 756 | /* LCOV_EXCL_STOP */ 757 | } 758 | if (tq->tag == ACL_USER) { 759 | tq->uid = *(uid_t*)p; 760 | } else { 761 | tq->gid = *(gid_t*)p; 762 | } 763 | acl_free(p); 764 | } 765 | return 0; 766 | } 767 | 768 | #define ENTRY_SET_CHECK(self, attr, value) \ 769 | if (value == NULL) { \ 770 | PyErr_SetString(PyExc_TypeError, \ 771 | attr " deletion is not supported"); \ 772 | return -1; \ 773 | } 774 | 775 | /* Creation of a new Entry instance */ 776 | static PyObject* Entry_new(PyTypeObject* type, PyObject* args, 777 | PyObject *keywds) { 778 | PyObject* newentry; 779 | Entry_Object* entry; 780 | ACL_Object* parent = NULL; 781 | 782 | if (!PyArg_ParseTuple(args, "O!", &ACL_Type, &parent)) 783 | return NULL; 784 | 785 | newentry = PyType_GenericNew(type, args, keywds); 786 | 787 | if(newentry == NULL) { 788 | return NULL; 789 | } 790 | 791 | entry = (Entry_Object*)newentry; 792 | 793 | if(acl_create_entry(&parent->acl, &entry->entry) == -1) { 794 | /* LCOV_EXCL_START */ 795 | PyErr_SetFromErrno(PyExc_IOError); 796 | Py_DECREF(newentry); 797 | return NULL; 798 | /* LCOV_EXCL_STOP */ 799 | } 800 | Py_INCREF(parent); 801 | entry->parent_acl = (PyObject*)parent; 802 | return newentry; 803 | } 804 | 805 | /* Initialization of a new Entry instance */ 806 | static int Entry_init(PyObject* obj, PyObject* args, PyObject *keywds) { 807 | Entry_Object* self = (Entry_Object*) obj; 808 | ACL_Object* parent = NULL; 809 | 810 | if (!PyArg_ParseTuple(args, "O!", &ACL_Type, &parent)) 811 | return -1; 812 | 813 | if ((PyObject*)parent != self->parent_acl) { 814 | PyErr_SetString(PyExc_ValueError, 815 | "Can't reinitialize with a different parent"); 816 | return -1; 817 | } 818 | return 0; 819 | } 820 | 821 | /* Free the Entry instance */ 822 | static void Entry_dealloc(PyObject* obj) { 823 | Entry_Object *self = (Entry_Object*) obj; 824 | PyObject *err_type, *err_value, *err_traceback; 825 | 826 | PyErr_Fetch(&err_type, &err_value, &err_traceback); 827 | if(self->parent_acl != NULL) { 828 | Py_DECREF(self->parent_acl); 829 | self->parent_acl = NULL; 830 | } 831 | PyErr_Restore(err_type, err_value, err_traceback); 832 | Py_TYPE(obj)->tp_free(obj); 833 | } 834 | 835 | /* Converts the entry to a text format */ 836 | static PyObject* Entry_str(PyObject *obj) { 837 | PyObject *format, *kind; 838 | Entry_Object *self = (Entry_Object*) obj; 839 | tag_qual tq; 840 | 841 | if(get_tag_qualifier(self->entry, &tq) < 0) { 842 | return NULL; 843 | } 844 | 845 | format = PyUnicode_FromString("ACL entry for "); 846 | if(format == NULL) 847 | return NULL; 848 | switch(tq.tag) { 849 | case ACL_UNDEFINED_TAG: 850 | kind = PyUnicode_FromString("undefined type"); 851 | break; 852 | case ACL_USER_OBJ: 853 | kind = PyUnicode_FromString("the owner"); 854 | break; 855 | case ACL_GROUP_OBJ: 856 | kind = PyUnicode_FromString("the group"); 857 | break; 858 | case ACL_OTHER: 859 | kind = PyUnicode_FromString("the others"); 860 | break; 861 | case ACL_USER: 862 | /* FIXME: here and in the group case, we're formatting with 863 | unsigned, because there's no way to automatically determine 864 | the signed-ness of the types; on Linux(glibc) they're 865 | unsigned, so we'll go along with that */ 866 | kind = PyUnicode_FromFormat("user with uid %u", tq.uid); 867 | break; 868 | case ACL_GROUP: 869 | kind = PyUnicode_FromFormat("group with gid %u", tq.gid); 870 | break; 871 | case ACL_MASK: 872 | kind = PyUnicode_FromString("the mask"); 873 | break; 874 | default: /* LCOV_EXCL_START */ 875 | kind = PyUnicode_FromString("UNKNOWN_TAG_TYPE!"); 876 | break; 877 | /* LCOV_EXCL_STOP */ 878 | } 879 | if (kind == NULL) { 880 | /* LCOV_EXCL_START */ 881 | Py_DECREF(format); 882 | return NULL; 883 | /* LCOV_EXCL_STOP */ 884 | } 885 | PyObject *ret = PyUnicode_Concat(format, kind); 886 | Py_DECREF(format); 887 | Py_DECREF(kind); 888 | return ret; 889 | } 890 | 891 | /* Sets the tag type of the entry */ 892 | static int Entry_set_tag_type(PyObject* obj, PyObject* value, void* arg) { 893 | Entry_Object *self = (Entry_Object*) obj; 894 | 895 | ENTRY_SET_CHECK(self, "tag type", value); 896 | 897 | if(!PyLong_Check(value)) { 898 | PyErr_SetString(PyExc_TypeError, 899 | "tag type must be integer"); 900 | return -1; 901 | } 902 | if(acl_set_tag_type(self->entry, (acl_tag_t)PyLong_AsLong(value)) == -1) { 903 | PyErr_SetFromErrno(PyExc_IOError); 904 | return -1; 905 | } 906 | 907 | return 0; 908 | } 909 | 910 | /* Returns the tag type of the entry */ 911 | static PyObject* Entry_get_tag_type(PyObject *obj, void* arg) { 912 | Entry_Object *self = (Entry_Object*) obj; 913 | acl_tag_t value; 914 | 915 | if(acl_get_tag_type(self->entry, &value) == -1) { 916 | /* LCOV_EXCL_START */ 917 | PyErr_SetFromErrno(PyExc_IOError); 918 | return NULL; 919 | /* LCOV_EXCL_STOP */ 920 | } 921 | 922 | return PyLong_FromLong(value); 923 | } 924 | 925 | /* Sets the qualifier (either uid_t or gid_t) for the entry, 926 | * usable only if the tag type if ACL_USER or ACL_GROUP 927 | */ 928 | static int Entry_set_qualifier(PyObject* obj, PyObject* value, void* arg) { 929 | Entry_Object *self = (Entry_Object*) obj; 930 | unsigned long uidgid; 931 | uid_t uid; 932 | gid_t gid; 933 | void *p; 934 | acl_tag_t tag; 935 | 936 | ENTRY_SET_CHECK(self, "qualifier", value); 937 | 938 | if(!PyLong_Check(value)) { 939 | PyErr_SetString(PyExc_TypeError, 940 | "qualifier must be integer"); 941 | return -1; 942 | } 943 | /* This is the negative value check, and larger than long 944 | check. If uid_t/gid_t are long-sized, this is enough to check 945 | for both over and underflow. */ 946 | if((uidgid = PyLong_AsUnsignedLong(value)) == (unsigned long) -1) { 947 | if(PyErr_Occurred() != NULL) { 948 | return -1; 949 | } 950 | } 951 | /* Due to how acl_set_qualifier takes its argument, we have to do 952 | this ugly dance with two variables and a pointer that will 953 | point to one of them. */ 954 | if(acl_get_tag_type(self->entry, &tag) == -1) { 955 | /* LCOV_EXCL_START */ 956 | PyErr_SetFromErrno(PyExc_IOError); 957 | return -1; 958 | /* LCOV_EXCL_STOP */ 959 | } 960 | uid = uidgid; 961 | gid = uidgid; 962 | /* This is an extra overflow check, in case uid_t/gid_t are 963 | int-sized (and int size smaller than long size). */ 964 | switch(tag) { 965 | case ACL_USER: 966 | if((unsigned long)uid != uidgid) { 967 | PyErr_SetString(PyExc_OverflowError, "Can't assign given qualifier"); 968 | return -1; 969 | } else { 970 | p = &uid; 971 | } 972 | break; 973 | case ACL_GROUP: 974 | if((unsigned long)gid != uidgid) { 975 | PyErr_SetString(PyExc_OverflowError, "Can't assign given qualifier"); 976 | return -1; 977 | } else { 978 | p = &gid; 979 | } 980 | break; 981 | default: 982 | PyErr_SetString(PyExc_TypeError, 983 | "Can only set qualifiers on ACL_USER or ACL_GROUP entries"); 984 | return -1; 985 | } 986 | if(acl_set_qualifier(self->entry, p) == -1) { 987 | /* LCOV_EXCL_START */ 988 | PyErr_SetFromErrno(PyExc_IOError); 989 | return -1; 990 | /* LCOV_EXCL_STOP */ 991 | } 992 | 993 | return 0; 994 | } 995 | 996 | /* Returns the qualifier of the entry */ 997 | static PyObject* Entry_get_qualifier(PyObject *obj, void* arg) { 998 | Entry_Object *self = (Entry_Object*) obj; 999 | unsigned long value; 1000 | tag_qual tq; 1001 | 1002 | if(get_tag_qualifier(self->entry, &tq) < 0) { 1003 | return NULL; 1004 | } 1005 | if (tq.tag == ACL_USER) { 1006 | value = tq.uid; 1007 | } else if (tq.tag == ACL_GROUP) { 1008 | value = tq.gid; 1009 | } else { 1010 | PyErr_SetString(PyExc_TypeError, 1011 | "Given entry doesn't have an user or" 1012 | " group tag"); 1013 | return NULL; 1014 | } 1015 | return PyLong_FromUnsignedLong(value); 1016 | } 1017 | 1018 | /* Returns the parent ACL of the entry */ 1019 | static PyObject* Entry_get_parent(PyObject *obj, void* arg) { 1020 | Entry_Object *self = (Entry_Object*) obj; 1021 | 1022 | Py_INCREF(self->parent_acl); 1023 | return self->parent_acl; 1024 | } 1025 | 1026 | /* Returns the a new Permset representing the permset of the entry 1027 | * FIXME: Should return a new reference to the same object, which 1028 | * should be created at init time! 1029 | */ 1030 | static PyObject* Entry_get_permset(PyObject *obj, void* arg) { 1031 | PyObject *p; 1032 | 1033 | PyObject *perm_arglist = Py_BuildValue("(O)", obj); 1034 | if (perm_arglist == NULL) { 1035 | return NULL; 1036 | } 1037 | p = PyObject_CallObject((PyObject*)&Permset_Type, perm_arglist); 1038 | Py_DECREF(perm_arglist); 1039 | return p; 1040 | } 1041 | 1042 | /* Sets the permset of the entry to the passed Permset */ 1043 | static int Entry_set_permset(PyObject* obj, PyObject* value, void* arg) { 1044 | Entry_Object *self = (Entry_Object*)obj; 1045 | Permset_Object *p; 1046 | 1047 | ENTRY_SET_CHECK(self, "permset", value); 1048 | 1049 | if(!PyObject_IsInstance(value, (PyObject*)&Permset_Type)) { 1050 | PyErr_SetString(PyExc_TypeError, "argument 1 must be posix1e.Permset"); 1051 | return -1; 1052 | } 1053 | p = (Permset_Object*)value; 1054 | if(acl_set_permset(self->entry, p->permset) == -1) { 1055 | /* LCOV_EXCL_START */ 1056 | PyErr_SetFromErrno(PyExc_IOError); 1057 | return -1; 1058 | /* LCOV_EXCL_STOP */ 1059 | } 1060 | return 0; 1061 | } 1062 | 1063 | static char __Entry_copy_doc__[] = 1064 | "copy(src)\n" 1065 | "Copies an ACL entry.\n" 1066 | "\n" 1067 | "This method sets all the parameters to those of another\n" 1068 | "entry (either of the same ACL or belonging to another ACL).\n" 1069 | "\n" 1070 | ":param Entry src: instance of type Entry\n" 1071 | ; 1072 | 1073 | /* Sets all the entry parameters to another entry */ 1074 | static PyObject* Entry_copy(PyObject *obj, PyObject *args) { 1075 | Entry_Object *self = (Entry_Object*)obj; 1076 | Entry_Object *other; 1077 | 1078 | if(!PyArg_ParseTuple(args, "O!", &Entry_Type, &other)) 1079 | return NULL; 1080 | 1081 | if(acl_copy_entry(self->entry, other->entry) == -1) 1082 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 1083 | 1084 | Py_RETURN_NONE; 1085 | } 1086 | 1087 | /**** Permset type *****/ 1088 | 1089 | /* Creation of a new Permset instance */ 1090 | static PyObject* Permset_new(PyTypeObject* type, PyObject* args, 1091 | PyObject *keywds) { 1092 | PyObject* newpermset; 1093 | Permset_Object* permset; 1094 | Entry_Object* parent = NULL; 1095 | 1096 | if (!PyArg_ParseTuple(args, "O!", &Entry_Type, &parent)) { 1097 | return NULL; 1098 | } 1099 | 1100 | newpermset = PyType_GenericNew(type, args, keywds); 1101 | 1102 | if(newpermset == NULL) { 1103 | return NULL; 1104 | } 1105 | 1106 | permset = (Permset_Object*)newpermset; 1107 | 1108 | if(acl_get_permset(parent->entry, &permset->permset) == -1) { 1109 | PyErr_SetFromErrno(PyExc_IOError); 1110 | Py_DECREF(newpermset); 1111 | return NULL; 1112 | } 1113 | 1114 | permset->parent_entry = (PyObject*)parent; 1115 | Py_INCREF(parent); 1116 | 1117 | return newpermset; 1118 | } 1119 | 1120 | /* Initialization of a new Permset instance */ 1121 | static int Permset_init(PyObject* obj, PyObject* args, PyObject *keywds) { 1122 | Permset_Object* self = (Permset_Object*) obj; 1123 | Entry_Object* parent = NULL; 1124 | 1125 | if (!PyArg_ParseTuple(args, "O!", &Entry_Type, &parent)) 1126 | return -1; 1127 | 1128 | if ((PyObject*)parent != self->parent_entry) { 1129 | PyErr_SetString(PyExc_ValueError, 1130 | "Can't reinitialize with a different parent"); 1131 | return -1; 1132 | } 1133 | 1134 | return 0; 1135 | } 1136 | 1137 | /* Free the Permset instance */ 1138 | static void Permset_dealloc(PyObject* obj) { 1139 | Permset_Object *self = (Permset_Object*) obj; 1140 | PyObject *err_type, *err_value, *err_traceback; 1141 | 1142 | PyErr_Fetch(&err_type, &err_value, &err_traceback); 1143 | if(self->parent_entry != NULL) { 1144 | Py_DECREF(self->parent_entry); 1145 | self->parent_entry = NULL; 1146 | } 1147 | PyErr_Restore(err_type, err_value, err_traceback); 1148 | Py_TYPE(obj)->tp_free((PyObject *)obj); 1149 | } 1150 | 1151 | /* Permset string representation */ 1152 | static PyObject* Permset_str(PyObject *obj) { 1153 | Permset_Object *self = (Permset_Object*) obj; 1154 | char pstr[3]; 1155 | 1156 | pstr[0] = get_perm(self->permset, ACL_READ) ? 'r' : '-'; 1157 | pstr[1] = get_perm(self->permset, ACL_WRITE) ? 'w' : '-'; 1158 | pstr[2] = get_perm(self->permset, ACL_EXECUTE) ? 'x' : '-'; 1159 | return PyUnicode_FromStringAndSize(pstr, 3); 1160 | } 1161 | 1162 | static char __Permset_clear_doc__[] = 1163 | "Clears all permissions from the permission set.\n" 1164 | ; 1165 | 1166 | /* Clears all permissions from the permset */ 1167 | static PyObject* Permset_clear(PyObject* obj, PyObject* args) { 1168 | Permset_Object *self = (Permset_Object*) obj; 1169 | 1170 | if(acl_clear_perms(self->permset) == -1) 1171 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 1172 | 1173 | Py_RETURN_NONE; 1174 | } 1175 | 1176 | static PyObject* Permset_get_right(PyObject *obj, void* arg) { 1177 | Permset_Object *self = (Permset_Object*) obj; 1178 | 1179 | if(get_perm(self->permset, *(acl_perm_t*)arg)) { 1180 | Py_RETURN_TRUE; 1181 | } else { 1182 | Py_RETURN_FALSE; 1183 | } 1184 | } 1185 | 1186 | static int Permset_set_right(PyObject* obj, PyObject* value, void* arg) { 1187 | Permset_Object *self = (Permset_Object*) obj; 1188 | int on; 1189 | int nerr; 1190 | 1191 | if(!PyLong_Check(value)) { 1192 | PyErr_SetString(PyExc_ValueError, "invalid argument, an integer" 1193 | " is expected"); 1194 | return -1; 1195 | } 1196 | on = PyLong_AsLong(value); 1197 | if(on) 1198 | nerr = acl_add_perm(self->permset, *(acl_perm_t*)arg); 1199 | else 1200 | nerr = acl_delete_perm(self->permset, *(acl_perm_t*)arg); 1201 | if(nerr == -1) { 1202 | /* LCOV_EXCL_START */ 1203 | PyErr_SetFromErrno(PyExc_IOError); 1204 | return -1; 1205 | /* LCOV_EXCL_STOP */ 1206 | } 1207 | return 0; 1208 | } 1209 | 1210 | static char __Permset_add_doc__[] = 1211 | "add(perm)\n" 1212 | "Add a permission to the permission set.\n" 1213 | "\n" 1214 | "This function adds the permission contained in \n" 1215 | "the argument perm to the permission set. An attempt \n" 1216 | "to add a permission that is already contained in the \n" 1217 | "permission set is not considered an error.\n" 1218 | "\n" 1219 | ":param perm: a permission (:py:data:`ACL_WRITE`, :py:data:`ACL_READ`,\n" 1220 | " :py:data:`ACL_EXECUTE`, ...)\n" 1221 | ":raises IOError: in case the argument is not a valid descriptor\n" 1222 | ; 1223 | 1224 | static PyObject* Permset_add(PyObject* obj, PyObject* args) { 1225 | Permset_Object *self = (Permset_Object*) obj; 1226 | int right; 1227 | 1228 | if (!PyArg_ParseTuple(args, "i", &right)) 1229 | return NULL; 1230 | 1231 | if(acl_add_perm(self->permset, (acl_perm_t) right) == -1) 1232 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 1233 | 1234 | Py_RETURN_NONE; 1235 | } 1236 | 1237 | static char __Permset_delete_doc__[] = 1238 | "delete(perm)\n" 1239 | "Delete a permission from the permission set.\n" 1240 | "\n" 1241 | "This function deletes the permission contained in \n" 1242 | "the argument perm from the permission set. An attempt \n" 1243 | "to delete a permission that is not contained in the \n" 1244 | "permission set is not considered an error.\n" 1245 | "\n" 1246 | ":param perm: a permission (:py:data:`ACL_WRITE`, :py:data:`ACL_READ`,\n" 1247 | " :py:data:`ACL_EXECUTE`, ...)\n" 1248 | ":raises IOError: in case the argument is not a valid descriptor\n" 1249 | ; 1250 | 1251 | static PyObject* Permset_delete(PyObject* obj, PyObject* args) { 1252 | Permset_Object *self = (Permset_Object*) obj; 1253 | int right; 1254 | 1255 | if (!PyArg_ParseTuple(args, "i", &right)) 1256 | return NULL; 1257 | 1258 | if(acl_delete_perm(self->permset, (acl_perm_t) right) == -1) 1259 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 1260 | 1261 | Py_RETURN_NONE; 1262 | } 1263 | 1264 | static char __Permset_test_doc__[] = 1265 | "test(perm)\n" 1266 | "Test if a permission exists in the permission set.\n" 1267 | "\n" 1268 | "The test() function tests if the permission represented by\n" 1269 | "the argument perm exists in the permission set.\n" 1270 | "\n" 1271 | ":param perm: a permission (:py:data:`ACL_WRITE`, :py:data:`ACL_READ`,\n" 1272 | " :py:data:`ACL_EXECUTE`, ...)\n" 1273 | ":rtype: Boolean\n" 1274 | ":raises IOError: in case the argument is not a valid descriptor\n" 1275 | ; 1276 | 1277 | static PyObject* Permset_test(PyObject* obj, PyObject* args) { 1278 | Permset_Object *self = (Permset_Object*) obj; 1279 | int right; 1280 | int ret; 1281 | 1282 | if (!PyArg_ParseTuple(args, "i", &right)) 1283 | return NULL; 1284 | 1285 | ret = get_perm(self->permset, (acl_perm_t) right); 1286 | if(ret == -1) 1287 | return PyErr_SetFromErrno(PyExc_IOError); /* LCOV_EXCL_LINE */ 1288 | 1289 | if(ret) { 1290 | Py_RETURN_TRUE; 1291 | } else { 1292 | Py_RETURN_FALSE; 1293 | } 1294 | } 1295 | 1296 | #endif 1297 | 1298 | static char __ACL_Type_doc__[] = 1299 | "Type which represents a POSIX ACL\n" 1300 | "\n" 1301 | ".. note:: only one keyword parameter should be provided\n" 1302 | "\n" 1303 | ":param string/bytes/path-like file: creates an ACL representing\n" 1304 | " the access ACL of the specified file or directory.\n" 1305 | ":param string/bytes/path-like filedef: creates an ACL representing\n" 1306 | " the default ACL of the given directory.\n" 1307 | ":param int/iostream fd: creates an ACL representing\n" 1308 | " the access ACL of the given file descriptor.\n" 1309 | ":param string text: creates an ACL from a \n" 1310 | " textual description; note the ACL must be valid, which\n" 1311 | " means including a mask for extended ACLs, similar to\n" 1312 | " ``setfacl --no-mask``\n" 1313 | ":param ACL acl: creates a copy of an existing ACL instance.\n" 1314 | ":param int mode: creates an ACL from a numeric mode\n" 1315 | " (e.g. ``mode=0644``); this is valid only when the C library\n" 1316 | " provides the ``acl_from_mode call``, and\n" 1317 | " note that no validation is done on the given value.\n" 1318 | ":param bytes data: creates an ACL from a serialised form,\n" 1319 | " as provided by calling ``__getstate__()`` on an existing ACL\n" 1320 | "\n" 1321 | "If no parameters are passed, an empty ACL will be created; this\n" 1322 | "makes sense only when your OS supports ACL modification\n" 1323 | "(i.e. it implements full POSIX.1e support), otherwise the ACL won't\n" 1324 | "be useful.\n" 1325 | ; 1326 | 1327 | /* ACL type methods */ 1328 | static PyMethodDef ACL_methods[] = { 1329 | {"applyto", ACL_applyto, METH_VARARGS, __applyto_doc__}, 1330 | {"valid", ACL_valid, METH_NOARGS, __valid_doc__}, 1331 | #ifdef HAVE_LINUX 1332 | {"to_any_text", (PyCFunction)ACL_to_any_text, METH_VARARGS | METH_KEYWORDS, 1333 | __to_any_text_doc__}, 1334 | {"check", ACL_check, METH_NOARGS, __check_doc__}, 1335 | {"equiv_mode", ACL_equiv_mode, METH_NOARGS, __equiv_mode_doc__}, 1336 | #endif 1337 | #ifdef HAVE_ACL_COPY_EXT 1338 | {"__getstate__", ACL_get_state, METH_NOARGS, 1339 | "Dumps the ACL to an external format."}, 1340 | {"__setstate__", ACL_set_state, METH_VARARGS, 1341 | "Loads the ACL from an external format."}, 1342 | #endif 1343 | #ifdef HAVE_LEVEL2 1344 | {"delete_entry", ACL_delete_entry, METH_VARARGS, __ACL_delete_entry_doc__}, 1345 | {"calc_mask", ACL_calc_mask, METH_NOARGS, __ACL_calc_mask_doc__}, 1346 | {"append", ACL_append, METH_VARARGS, __ACL_append_doc__}, 1347 | #endif 1348 | {NULL, NULL, 0, NULL} 1349 | }; 1350 | 1351 | 1352 | /* The definition of the ACL Type */ 1353 | static PyTypeObject ACL_Type = { 1354 | PyVarObject_HEAD_INIT(NULL, 0) 1355 | .tp_name = "posix1e.ACL", 1356 | .tp_basicsize = sizeof(ACL_Object), 1357 | .tp_itemsize = 0, 1358 | .tp_dealloc = ACL_dealloc, 1359 | .tp_str = ACL_str, 1360 | .tp_flags = Py_TPFLAGS_DEFAULT, 1361 | .tp_doc = __ACL_Type_doc__, 1362 | #ifdef HAVE_LINUX 1363 | .tp_richcompare = ACL_richcompare, 1364 | #endif 1365 | #ifdef HAVE_LEVEL2 1366 | .tp_iter = ACL_iter, 1367 | .tp_iternext = ACL_iternext, 1368 | #endif 1369 | .tp_methods = ACL_methods, 1370 | .tp_init = ACL_init, 1371 | .tp_new = ACL_new, 1372 | }; 1373 | 1374 | #ifdef HAVE_LEVEL2 1375 | 1376 | /* Entry type methods */ 1377 | static PyMethodDef Entry_methods[] = { 1378 | {"copy", Entry_copy, METH_VARARGS, __Entry_copy_doc__}, 1379 | {NULL, NULL, 0, NULL} 1380 | }; 1381 | 1382 | static char __Entry_tagtype_doc__[] = 1383 | "The tag type of the current entry\n" 1384 | "\n" 1385 | "This is one of:\n" 1386 | " - :py:data:`ACL_UNDEFINED_TAG`\n" 1387 | " - :py:data:`ACL_USER_OBJ`\n" 1388 | " - :py:data:`ACL_USER`\n" 1389 | " - :py:data:`ACL_GROUP_OBJ`\n" 1390 | " - :py:data:`ACL_GROUP`\n" 1391 | " - :py:data:`ACL_MASK`\n" 1392 | " - :py:data:`ACL_OTHER`\n" 1393 | ; 1394 | 1395 | static char __Entry_qualifier_doc__[] = 1396 | "The qualifier of the current entry\n" 1397 | "\n" 1398 | "If the tag type is :py:data:`ACL_USER`, this should be a user id.\n" 1399 | "If the tag type if :py:data:`ACL_GROUP`, this should be a group id.\n" 1400 | "Else it doesn't matter.\n" 1401 | ; 1402 | 1403 | static char __Entry_parent_doc__[] = 1404 | "The parent ACL of this entry\n" 1405 | ; 1406 | 1407 | static char __Entry_permset_doc__[] = 1408 | "The permission set of this ACL entry\n" 1409 | ; 1410 | 1411 | /* Entry getset */ 1412 | static PyGetSetDef Entry_getsets[] = { 1413 | {"tag_type", Entry_get_tag_type, Entry_set_tag_type, 1414 | __Entry_tagtype_doc__}, 1415 | {"qualifier", Entry_get_qualifier, Entry_set_qualifier, 1416 | __Entry_qualifier_doc__}, 1417 | {"parent", Entry_get_parent, NULL, __Entry_parent_doc__}, 1418 | {"permset", Entry_get_permset, Entry_set_permset, __Entry_permset_doc__}, 1419 | {NULL} 1420 | }; 1421 | 1422 | static char __Entry_Type_doc__[] = 1423 | "Type which represents an entry in an ACL.\n" 1424 | "\n" 1425 | "The type exists only if the OS has full support for POSIX.1e\n" 1426 | "Can be created either by:\n" 1427 | "\n" 1428 | " >>> e = posix1e.Entry(myACL) # this creates a new entry in the ACL\n" 1429 | " >>> e = myACL.append() # another way for doing the same thing\n" 1430 | "\n" 1431 | "or by:\n" 1432 | "\n" 1433 | " >>> for entry in myACL:\n" 1434 | " ... print entry\n" 1435 | "\n" 1436 | "Note that the Entry keeps a reference to its ACL, so even if \n" 1437 | "you delete the ACL, it won't be cleaned up and will continue to \n" 1438 | "exist until its Entry(ies) will be deleted.\n" 1439 | ; 1440 | /* The definition of the Entry Type */ 1441 | static PyTypeObject Entry_Type = { 1442 | PyVarObject_HEAD_INIT(NULL, 0) 1443 | .tp_name = "posix1e.Entry", 1444 | .tp_basicsize = sizeof(Entry_Object), 1445 | .tp_itemsize = 0, 1446 | .tp_dealloc = Entry_dealloc, 1447 | .tp_str = Entry_str, 1448 | .tp_flags = Py_TPFLAGS_DEFAULT, 1449 | .tp_doc = __Entry_Type_doc__, 1450 | .tp_methods = Entry_methods, 1451 | .tp_getset = Entry_getsets, 1452 | .tp_init = Entry_init, 1453 | .tp_new = Entry_new 1454 | }; 1455 | 1456 | /* Permset type methods */ 1457 | static PyMethodDef Permset_methods[] = { 1458 | {"clear", Permset_clear, METH_NOARGS, __Permset_clear_doc__, }, 1459 | {"add", Permset_add, METH_VARARGS, __Permset_add_doc__, }, 1460 | {"delete", Permset_delete, METH_VARARGS, __Permset_delete_doc__, }, 1461 | {"test", Permset_test, METH_VARARGS, __Permset_test_doc__, }, 1462 | {NULL, NULL, 0, NULL} 1463 | }; 1464 | 1465 | static char __Permset_execute_doc__[] = 1466 | "Execute permission property\n" 1467 | "\n" 1468 | "This is a convenience method of retrieving and setting the execute\n" 1469 | "permission in the permission set; the \n" 1470 | "same effect can be achieved using the functions\n" 1471 | "add(), test(), delete(), and those can take any \n" 1472 | "permission defined by your platform.\n" 1473 | ; 1474 | 1475 | static char __Permset_read_doc__[] = 1476 | "Read permission property\n" 1477 | "\n" 1478 | "This is a convenience method of retrieving and setting the read\n" 1479 | "permission in the permission set; the \n" 1480 | "same effect can be achieved using the functions\n" 1481 | "add(), test(), delete(), and those can take any \n" 1482 | "permission defined by your platform.\n" 1483 | ; 1484 | 1485 | static char __Permset_write_doc__[] = 1486 | "Write permission property\n" 1487 | "\n" 1488 | "This is a convenience method of retrieving and setting the write\n" 1489 | "permission in the permission set; the \n" 1490 | "same effect can be achieved using the functions\n" 1491 | "add(), test(), delete(), and those can take any \n" 1492 | "permission defined by your platform.\n" 1493 | ; 1494 | 1495 | /* Permset getset */ 1496 | static PyGetSetDef Permset_getsets[] = { 1497 | {"execute", Permset_get_right, Permset_set_right, 1498 | __Permset_execute_doc__, &holder_ACL_EXECUTE}, 1499 | {"read", Permset_get_right, Permset_set_right, 1500 | __Permset_read_doc__, &holder_ACL_READ}, 1501 | {"write", Permset_get_right, Permset_set_right, 1502 | __Permset_write_doc__, &holder_ACL_WRITE}, 1503 | {NULL} 1504 | }; 1505 | 1506 | static char __Permset_Type_doc__[] = 1507 | "Type which represents the permission set in an ACL entry\n" 1508 | "\n" 1509 | "The type exists only if the OS has full support for POSIX.1e\n" 1510 | "Can be retrieved either by:\n\n" 1511 | ">>> perms = myEntry.permset\n" 1512 | "\n" 1513 | "or by:\n\n" 1514 | ">>> perms = posix1e.Permset(myEntry)\n" 1515 | "\n" 1516 | "Note that the Permset keeps a reference to its Entry, so even if \n" 1517 | "you delete the entry, it won't be cleaned up and will continue to \n" 1518 | "exist until its Permset will be deleted.\n" 1519 | ; 1520 | 1521 | /* The definition of the Permset Type */ 1522 | static PyTypeObject Permset_Type = { 1523 | PyVarObject_HEAD_INIT(NULL, 0) 1524 | .tp_name = "posix1e.Permset", 1525 | .tp_basicsize = sizeof(Permset_Object), 1526 | .tp_itemsize = 0, 1527 | .tp_dealloc = Permset_dealloc, 1528 | .tp_str = Permset_str, 1529 | .tp_flags = Py_TPFLAGS_DEFAULT, 1530 | .tp_doc = __Permset_Type_doc__, 1531 | .tp_methods = Permset_methods, 1532 | .tp_getset = Permset_getsets, 1533 | .tp_init = Permset_init, 1534 | .tp_new = Permset_new, 1535 | }; 1536 | 1537 | #endif 1538 | 1539 | /* Module methods */ 1540 | 1541 | static char __deletedef_doc__[] = 1542 | "delete_default(path)\n" 1543 | "Delete the default ACL from a directory.\n" 1544 | "\n" 1545 | "This function deletes the default ACL associated with\n" 1546 | "a directory (the ACL which will be ANDed with the mode\n" 1547 | "parameter to the open, creat functions).\n" 1548 | "\n" 1549 | ":param string path: the directory whose default ACL should be deleted\n" 1550 | ; 1551 | 1552 | /* Deletes the default ACL from a directory */ 1553 | static PyObject* aclmodule_delete_default(PyObject* obj, PyObject* args) { 1554 | char *filename; 1555 | 1556 | /* Parse the arguments */ 1557 | if (!PyArg_ParseTuple(args, "et", NULL, &filename)) 1558 | return NULL; 1559 | 1560 | if(acl_delete_def_file(filename) == -1) { 1561 | return PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename); 1562 | } 1563 | 1564 | Py_RETURN_NONE; 1565 | } 1566 | 1567 | #ifdef HAVE_LINUX 1568 | static char __has_extended_doc__[] = 1569 | "has_extended(item)\n" 1570 | "Check if a file or file handle has an extended ACL.\n" 1571 | "\n" 1572 | ":param item: either a file name or a file-like object or an integer;\n" 1573 | " it represents the file-system object on which to act\n" 1574 | ; 1575 | 1576 | /* Check for extended ACL a file or fd */ 1577 | static PyObject* aclmodule_has_extended(PyObject* obj, PyObject* args) { 1578 | PyObject *item, *tmp; 1579 | int nret; 1580 | int fd; 1581 | 1582 | if (!PyArg_ParseTuple(args, "O", &item)) 1583 | return NULL; 1584 | 1585 | if((fd = PyObject_AsFileDescriptor(item)) != -1) { 1586 | if((nret = acl_extended_fd(fd)) == -1) { 1587 | PyErr_SetFromErrno(PyExc_IOError); 1588 | } 1589 | } else { 1590 | // PyObject_AsFileDescriptor sets an error when failing, so clear 1591 | // it such that further code works; some method lookups fail if an 1592 | // error already occured when called, which breaks at least 1593 | // PyOS_FSPath (called by FSConverter). 1594 | PyErr_Clear(); 1595 | if(PyUnicode_FSConverter(item, &tmp)) { 1596 | char *filename = PyBytes_AS_STRING(tmp); 1597 | if ((nret = acl_extended_file(filename)) == -1) { 1598 | PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename); 1599 | } 1600 | Py_DECREF(tmp); 1601 | } else { 1602 | nret = -1; 1603 | } 1604 | } 1605 | 1606 | if (nret < 0) { 1607 | return NULL; 1608 | } else { 1609 | return PyBool_FromLong(nret); 1610 | } 1611 | } 1612 | #endif 1613 | 1614 | /* The module methods */ 1615 | static PyMethodDef aclmodule_methods[] = { 1616 | {"delete_default", aclmodule_delete_default, METH_VARARGS, 1617 | __deletedef_doc__}, 1618 | #ifdef HAVE_LINUX 1619 | {"has_extended", aclmodule_has_extended, METH_VARARGS, 1620 | __has_extended_doc__}, 1621 | #endif 1622 | {NULL, NULL, 0, NULL} 1623 | }; 1624 | 1625 | static char __posix1e_doc__[] = 1626 | "POSIX.1e ACLs manipulation\n" 1627 | "==========================\n" 1628 | "\n" 1629 | "This module provides support for manipulating POSIX.1e ACLS\n" 1630 | "\n" 1631 | "Depending on the operating system support for POSIX.1e, \n" 1632 | "the ACL type will have more or less capabilities:\n\n" 1633 | " - level 1, only basic support, you can create\n" 1634 | " ACLs from files and text descriptions;\n" 1635 | " once created, the type is immutable\n" 1636 | " - level 2, complete support, you can alter\n" 1637 | " the ACL once it is created\n" 1638 | "\n" 1639 | "Also, in level 2, more types are available, corresponding\n" 1640 | "to acl_entry_t (the Entry type), acl_permset_t (the Permset type).\n" 1641 | "\n" 1642 | "The existence of level 2 support and other extensions can be\n" 1643 | "checked by the constants:\n\n" 1644 | " - :py:data:`HAS_ACL_ENTRY` for level 2 and the Entry/Permset classes\n" 1645 | " - :py:data:`HAS_ACL_FROM_MODE` for ``ACL(mode=...)`` usage\n" 1646 | " - :py:data:`HAS_ACL_CHECK` for the :py:func:`ACL.check` function\n" 1647 | " - :py:data:`HAS_EXTENDED_CHECK` for the module-level\n" 1648 | " :py:func:`has_extended` function\n" 1649 | " - :py:data:`HAS_EQUIV_MODE` for the :py:func:`ACL.equiv_mode` method\n" 1650 | " - :py:data:`HAS_COPY_EXT` for the :py:func:`ACL.__getstate__` and\n" 1651 | " :py:func:`ACL.__setstate__` functions (pickle protocol)\n" 1652 | "\n" 1653 | "Example:\n" 1654 | "\n" 1655 | ">>> import posix1e\n" 1656 | ">>> acl1 = posix1e.ACL(file=\"file.txt\") \n" 1657 | ">>> print acl1\n" 1658 | "user::rw-\n" 1659 | "group::rw-\n" 1660 | "other::r--\n" 1661 | ">>>\n" 1662 | ">>> b = posix1e.ACL(text=\"u::rx,g::-,o::-\")\n" 1663 | ">>> print b\n" 1664 | "user::r-x\n" 1665 | "group::---\n" 1666 | "other::---\n" 1667 | ">>>\n" 1668 | ">>> b.applyto(\"file.txt\")\n" 1669 | ">>> print posix1e.ACL(file=\"file.txt\")\n" 1670 | "user::r-x\n" 1671 | "group::---\n" 1672 | "other::---\n" 1673 | ">>>\n" 1674 | "\n" 1675 | ".. py:data:: ACL_USER\n\n" 1676 | " Denotes a specific user entry in an ACL.\n" 1677 | "\n" 1678 | ".. py:data:: ACL_USER_OBJ\n\n" 1679 | " Denotes the user owner entry in an ACL.\n" 1680 | "\n" 1681 | ".. py:data:: ACL_GROUP\n\n" 1682 | " Denotes the a group entry in an ACL.\n" 1683 | "\n" 1684 | ".. py:data:: ACL_GROUP_OBJ\n\n" 1685 | " Denotes the group owner entry in an ACL.\n" 1686 | "\n" 1687 | ".. py:data:: ACL_OTHER\n\n" 1688 | " Denotes the 'others' entry in an ACL.\n" 1689 | "\n" 1690 | ".. py:data:: ACL_MASK\n\n" 1691 | " Denotes the mask entry in an ACL, representing the maximum\n" 1692 | " access granted other users, the owner group and other groups.\n" 1693 | "\n" 1694 | ".. py:data:: ACL_UNDEFINED_TAG\n\n" 1695 | " An undefined tag in an ACL.\n" 1696 | "\n" 1697 | ".. py:data:: ACL_READ\n\n" 1698 | " Read permission in a permission set.\n" 1699 | "\n" 1700 | ".. py:data:: ACL_WRITE\n\n" 1701 | " Write permission in a permission set.\n" 1702 | "\n" 1703 | ".. py:data:: ACL_EXECUTE\n\n" 1704 | " Execute permission in a permission set.\n" 1705 | "\n" 1706 | ".. py:data:: HAS_ACL_ENTRY\n\n" 1707 | " denotes support for level 2 and the Entry/Permset classes\n" 1708 | "\n" 1709 | ".. py:data:: HAS_ACL_FROM_MODE\n\n" 1710 | " denotes support for building an ACL from an octal mode\n" 1711 | "\n" 1712 | ".. py:data:: HAS_ACL_CHECK\n\n" 1713 | " denotes support for extended checks of an ACL's validity\n" 1714 | "\n" 1715 | ".. py:data:: HAS_EXTENDED_CHECK\n\n" 1716 | " denotes support for checking whether an ACL is basic or extended\n" 1717 | "\n" 1718 | ".. py:data:: HAS_EQUIV_MODE\n\n" 1719 | " denotes support for the equiv_mode function\n" 1720 | "\n" 1721 | ".. py:data:: HAS_COPY_EXT\n\n" 1722 | " denotes support for __getstate__()/__setstate__() on an ACL\n" 1723 | "\n" 1724 | ; 1725 | 1726 | static struct PyModuleDef posix1emodule = { 1727 | PyModuleDef_HEAD_INIT, 1728 | .m_name = "posix1e", 1729 | .m_doc = __posix1e_doc__, 1730 | .m_size = 0, 1731 | .m_methods = aclmodule_methods, 1732 | }; 1733 | 1734 | PyMODINIT_FUNC 1735 | PyInit_posix1e(void) 1736 | { 1737 | PyObject *m, *d; 1738 | 1739 | if(PyType_Ready(&ACL_Type) < 0) 1740 | return NULL; 1741 | 1742 | #ifdef HAVE_LEVEL2 1743 | if(PyType_Ready(&Entry_Type) < 0) 1744 | return NULL; 1745 | 1746 | if(PyType_Ready(&Permset_Type) < 0) 1747 | return NULL; 1748 | #endif 1749 | 1750 | m = PyModule_Create(&posix1emodule); 1751 | if (m==NULL) 1752 | return NULL; 1753 | 1754 | d = PyModule_GetDict(m); 1755 | if (d == NULL) 1756 | return NULL; 1757 | 1758 | Py_INCREF(&ACL_Type); 1759 | if (PyDict_SetItemString(d, "ACL", 1760 | (PyObject *) &ACL_Type) < 0) 1761 | return NULL; 1762 | 1763 | /* 23.3.6 acl_type_t values */ 1764 | PyModule_AddIntConstant(m, "ACL_TYPE_ACCESS", ACL_TYPE_ACCESS); 1765 | PyModule_AddIntConstant(m, "ACL_TYPE_DEFAULT", ACL_TYPE_DEFAULT); 1766 | 1767 | 1768 | #ifdef HAVE_LEVEL2 1769 | Py_INCREF(&Entry_Type); 1770 | if (PyDict_SetItemString(d, "Entry", 1771 | (PyObject *) &Entry_Type) < 0) 1772 | return NULL; 1773 | 1774 | Py_INCREF(&Permset_Type); 1775 | if (PyDict_SetItemString(d, "Permset", 1776 | (PyObject *) &Permset_Type) < 0) 1777 | return NULL; 1778 | 1779 | /* 23.2.2 acl_perm_t values */ 1780 | PyModule_AddIntConstant(m, "ACL_READ", ACL_READ); 1781 | PyModule_AddIntConstant(m, "ACL_WRITE", ACL_WRITE); 1782 | PyModule_AddIntConstant(m, "ACL_EXECUTE", ACL_EXECUTE); 1783 | 1784 | /* 23.2.5 acl_tag_t values */ 1785 | PyModule_AddIntConstant(m, "ACL_UNDEFINED_TAG", ACL_UNDEFINED_TAG); 1786 | PyModule_AddIntConstant(m, "ACL_USER_OBJ", ACL_USER_OBJ); 1787 | PyModule_AddIntConstant(m, "ACL_USER", ACL_USER); 1788 | PyModule_AddIntConstant(m, "ACL_GROUP_OBJ", ACL_GROUP_OBJ); 1789 | PyModule_AddIntConstant(m, "ACL_GROUP", ACL_GROUP); 1790 | PyModule_AddIntConstant(m, "ACL_MASK", ACL_MASK); 1791 | PyModule_AddIntConstant(m, "ACL_OTHER", ACL_OTHER); 1792 | 1793 | /* Document extended functionality via easy-to-use constants */ 1794 | PyModule_AddIntConstant(m, "HAS_ACL_ENTRY", 1); 1795 | #else 1796 | PyModule_AddIntConstant(m, "HAS_ACL_ENTRY", 0); 1797 | #endif 1798 | 1799 | #ifdef HAVE_LINUX 1800 | /* Linux libacl specific acl_to_any_text constants */ 1801 | PyModule_AddIntConstant(m, "TEXT_ABBREVIATE", TEXT_ABBREVIATE); 1802 | PyModule_AddIntConstant(m, "TEXT_NUMERIC_IDS", TEXT_NUMERIC_IDS); 1803 | PyModule_AddIntConstant(m, "TEXT_SOME_EFFECTIVE", TEXT_SOME_EFFECTIVE); 1804 | PyModule_AddIntConstant(m, "TEXT_ALL_EFFECTIVE", TEXT_ALL_EFFECTIVE); 1805 | PyModule_AddIntConstant(m, "TEXT_SMART_INDENT", TEXT_SMART_INDENT); 1806 | 1807 | /* Linux libacl specific acl_check constants */ 1808 | PyModule_AddIntConstant(m, "ACL_MULTI_ERROR", ACL_MULTI_ERROR); 1809 | PyModule_AddIntConstant(m, "ACL_DUPLICATE_ERROR", ACL_DUPLICATE_ERROR); 1810 | PyModule_AddIntConstant(m, "ACL_MISS_ERROR", ACL_MISS_ERROR); 1811 | PyModule_AddIntConstant(m, "ACL_ENTRY_ERROR", ACL_ENTRY_ERROR); 1812 | 1813 | #define LINUX_EXT_VAL 1 1814 | #else 1815 | #define LINUX_EXT_VAL 0 1816 | #endif 1817 | /* declare the Linux extensions */ 1818 | PyModule_AddIntConstant(m, "HAS_ACL_FROM_MODE", LINUX_EXT_VAL); 1819 | PyModule_AddIntConstant(m, "HAS_ACL_CHECK", LINUX_EXT_VAL); 1820 | PyModule_AddIntConstant(m, "HAS_EXTENDED_CHECK", LINUX_EXT_VAL); 1821 | PyModule_AddIntConstant(m, "HAS_EQUIV_MODE", LINUX_EXT_VAL); 1822 | 1823 | PyModule_AddIntConstant(m, "HAS_COPY_EXT", 1824 | #ifdef HAVE_ACL_COPY_EXT 1825 | 1 1826 | #else 1827 | 0 1828 | #endif 1829 | ); 1830 | return m; 1831 | } 1832 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pylibacl documentation build configuration file, created by 4 | # sphinx-quickstart on Sun May 13 01:05:18 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('../')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'myst_parser'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = ['.rst', '.md'] 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'pylibacl' 44 | copyright = u'2002-2009, 2012, 2014, 2015, Iustin Pop' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.7.2' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.7.2' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build', 'html'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | default_domain = 'python' 73 | 74 | # If true, '()' will be appended to :func: etc. cross-reference text. 75 | #add_function_parentheses = True 76 | 77 | # If true, the current module name will be prepended to all description 78 | # unit titles (such as .. function::). 79 | #add_module_names = True 80 | 81 | # If true, sectionauthor and moduleauthor directives will be shown in the 82 | # output. They are ignored by default. 83 | #show_authors = False 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # A list of ignored prefixes for module index sorting. 89 | #modindex_common_prefix = [] 90 | 91 | keep_warnings = True 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'default' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | #html_static_path = ['_static'] 127 | html_static_path = [] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_domain_indices = True 146 | html_domain_indices = False 147 | 148 | # If false, no index is generated. 149 | #html_use_index = True 150 | html_use_index = False 151 | 152 | # If true, the index is split into individual pages for each letter. 153 | #html_split_index = False 154 | 155 | # If true, links to the reST sources are added to the pages. 156 | #html_show_sourcelink = True 157 | html_show_sourcelink = False 158 | 159 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 160 | #html_show_sphinx = True 161 | 162 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 163 | #html_show_copyright = True 164 | 165 | # If true, an OpenSearch description file will be output, and all pages will 166 | # contain a tag referring to it. The value of this option must be the 167 | # base URL from which the finished HTML is served. 168 | #html_use_opensearch = '' 169 | 170 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 171 | #html_file_suffix = None 172 | 173 | # Output file base name for HTML help builder. 174 | htmlhelp_basename = 'pylibacldoc' 175 | 176 | 177 | # -- Options for LaTeX output -------------------------------------------------- 178 | 179 | latex_elements = { 180 | # The paper size ('letterpaper' or 'a4paper'). 181 | #'papersize': 'letterpaper', 182 | 183 | # The font size ('10pt', '11pt' or '12pt'). 184 | #'pointsize': '10pt', 185 | 186 | # Additional stuff for the LaTeX preamble. 187 | #'preamble': '', 188 | } 189 | 190 | # Grouping the document tree into LaTeX files. List of tuples 191 | # (source start file, target name, title, author, documentclass [howto/manual]). 192 | latex_documents = [ 193 | ('index', 'pylibacl.tex', u'pylibacl Documentation', 194 | u'Iustin Pop', 'manual'), 195 | ] 196 | 197 | # The name of an image file (relative to this directory) to place at the top of 198 | # the title page. 199 | #latex_logo = None 200 | 201 | # For "manual" documents, if this is true, then toplevel headings are parts, 202 | # not chapters. 203 | #latex_use_parts = False 204 | 205 | # If true, show page references after internal links. 206 | #latex_show_pagerefs = False 207 | 208 | # If true, show URL addresses after external links. 209 | #latex_show_urls = False 210 | 211 | # Documents to append as an appendix to all manuals. 212 | #latex_appendices = [] 213 | 214 | # If false, no module index is generated. 215 | #latex_domain_indices = True 216 | 217 | 218 | # -- Options for manual page output -------------------------------------------- 219 | 220 | # One entry per manual page. List of tuples 221 | # (source start file, name, description, authors, manual section). 222 | man_pages = [ 223 | ('index', 'pylibacl', u'pylibacl Documentation', 224 | [u'Iustin Pop'], 1) 225 | ] 226 | 227 | # If true, show URL addresses after external links. 228 | #man_show_urls = False 229 | 230 | 231 | # -- Options for Texinfo output ------------------------------------------------ 232 | 233 | # Grouping the document tree into Texinfo files. List of tuples 234 | # (source start file, target name, title, author, 235 | # dir menu entry, description, category) 236 | texinfo_documents = [ 237 | ('index', 'pylibacl', u'pylibacl Documentation', 238 | u'Iustin Pop', 'pylibacl', 'One line description of project.', 239 | 'Miscellaneous'), 240 | ] 241 | 242 | # Documents to append as an appendix to all manuals. 243 | #texinfo_appendices = [] 244 | 245 | # If false, no module index is generated. 246 | #texinfo_domain_indices = True 247 | 248 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 249 | #texinfo_show_urls = 'footnote' 250 | 251 | autodoc_member_order = 'alphabetical' 252 | -------------------------------------------------------------------------------- /doc/contributing.md: -------------------------------------------------------------------------------- 1 | ../CONTRIBUTING.md -------------------------------------------------------------------------------- /doc/implementation.rst: -------------------------------------------------------------------------------- 1 | Implementation details 2 | ====================== 3 | 4 | Functionality level 5 | ------------------- 6 | 7 | The IEEE 1003.1e draft 17 ("POSIX.1e") describes a set of 28 functions. 8 | These are grouped into three groups, based on their portability: 9 | 10 | - first group, the most portable one. All systems which claim to support 11 | POSIX.1e should implement these: 12 | 13 | acl_delete_def_file(3), acl_dup(3), acl_free(3), acl_from_text(3), 14 | acl_get_fd(3), acl_get_file(3), acl_init(3), acl_set_fd(3), 15 | acl_set_file(3), acl_to_text(3), acl_valid(3) 16 | 17 | - second group, containing the rest of the POSIX ACL functions. Systems 18 | which claim to fully implement POSIX.1e should implement these: 19 | 20 | acl_add_perm(3), acl_calc_mask(3), acl_clear_perms(3), 21 | acl_copy_entry(3), acl_copy_ext(3), acl_copy_int(3), 22 | acl_create_entry(3), acl_delete_entry(3), acl_delete_perm(3), 23 | acl_get_entry(3), acl_get_permset(3), acl_get_qualifier(3), 24 | acl_get_tag_type(3), acl_set_permset(3), acl_set_qualifier(3), 25 | acl_set_tag_type(3), acl_size(3) 26 | 27 | - third group, containing extra functions implemented by each OS. These 28 | are non-portable version. Both Linux and FreeBSD implement some extra 29 | functions. 30 | 31 | Thus we have the level of compliance. Depending on whether the system 32 | library support the second group, you get some extra methods for the ACL 33 | object. 34 | 35 | The implementation of the second group of function can be tested by 36 | checking the module-level constant HAS_ACL_ENTRY. The extra 37 | functionality available on Linux can be tested by additional HAS_* 38 | constants. 39 | 40 | Internal structure 41 | ------------------ 42 | 43 | The POSIX draft has the following stuff (correct me if I'm wrong): 44 | 45 | - an ACL is denoted by acl_t 46 | - an ACL contains many acl_entry_t, these are the individual entries in 47 | the list; they always(!) belong to an acl_t 48 | - each entry_t has a qualifier (think uid_t or gid_t), whose type is 49 | denoted by the acl_tag_t type, and an acl_permset_t 50 | - the acl_permset_t can contain acl_perm_t value (ACL_READ, ACL_WRITE, 51 | ACL_EXECUTE, ACL_ADD, ACL_DELETE, ...) 52 | - functions to manipulate all these, and functions to manipulate files 53 | 54 | Currently supported platforms 55 | ----------------------------- 56 | 57 | For any other platforms, volunteers are welcome. 58 | 59 | Linux 60 | ~~~~~ 61 | 62 | It needs kernel 2.4 or higher and the libacl library installed (with 63 | development headers, if installing from rpm). This library is available 64 | on all modern distributions. 65 | 66 | The level of compliance is level 2 (see IMPLEMENTATION), plus some extra 67 | functions; and as my development is done on Linux, I try to implement 68 | these extensions when it makes sense. 69 | 70 | 71 | FreeBSD 72 | ~~~~~~~ 73 | 74 | The current tested version is 7.0. FreeBSD supports all the standards 75 | functions, but 7.0-RELEASE seems to have some issues regarding the 76 | acl_valid() function when the qualifier of an ACL_USER or ACL_GROUP 77 | entry is the same as the current uid. By my interpretation, this should 78 | be a valid ACL, but FreeBSD declares the ACL invalid. As such, some 79 | unittests fail on FreeBSD. 80 | 81 | Porting to other platforms 82 | -------------------------- 83 | 84 | First, determine if your OS supports the full 28 functions of the 85 | POSIX.1e draft (if so, define HAVE_LEVEL2) or only the first 11 86 | functions (most common case, meaning only HAVE_LEVEL1). 87 | 88 | If your OS supports only LEVEL1, modify ``setup.py`` as appropriately; 89 | unfortunately, the functionality of the module is quite low. 90 | 91 | If your OS supports LEVEL2, there is a function which you must define: 92 | testing if an acl_permset_t contains a given permission. For example, 93 | under Linux, the acl library defines:: 94 | 95 | int acl_get_perm(acl_permset_t permset_d, acl_perm_t perm); 96 | 97 | under FreeBSD, the library defines ``acl_get_perm_np`` with a similar 98 | syntax. So just see how this is implemented in your platform and either 99 | define a simple macro or a full function with the syntax:: 100 | 101 | static int get_perm(acl_permset_t permset_d, acl_perm_t perm); 102 | 103 | which must return 1 if the permset contains perm and 0 otherwise. 104 | 105 | 106 | .. Local Variables: 107 | .. mode: rst 108 | .. fill-column: 72 109 | .. End: 110 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | ====================================== 2 | Welcome to pylibacl's documentation! 3 | ====================================== 4 | 5 | See the :doc:`README ` for start, or the detailed :doc:`module 6 | ` information. 7 | 8 | Contents 9 | -------- 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | readme.md 15 | contributing.md 16 | security.md 17 | module.rst 18 | implementation.rst 19 | news.md 20 | 21 | Also see the :ref:`search`. 22 | -------------------------------------------------------------------------------- /doc/module.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: posix1e 2 | :members: 3 | :undoc-members: 4 | 5 | -------------------------------------------------------------------------------- /doc/news.md: -------------------------------------------------------------------------------- 1 | ../NEWS.md -------------------------------------------------------------------------------- /doc/readme.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | myst-parser 2 | -------------------------------------------------------------------------------- /doc/security.md: -------------------------------------------------------------------------------- 1 | ../SECURITY.md -------------------------------------------------------------------------------- /posix1e.pyi: -------------------------------------------------------------------------------- 1 | # Stubs for posix1e (Python 3) 2 | # 3 | # NOTE: This dynamically typed stub was automatically generated by stubgen. 4 | 5 | from typing import Optional, Union, Tuple, TypeVar 6 | from os import PathLike 7 | 8 | from typing import overload 9 | ACL_DUPLICATE_ERROR: int 10 | ACL_ENTRY_ERROR: int 11 | ACL_EXECUTE: int 12 | ACL_GROUP: int 13 | ACL_GROUP_OBJ: int 14 | ACL_MASK: int 15 | ACL_MISS_ERROR: int 16 | ACL_MULTI_ERROR: int 17 | ACL_OTHER: int 18 | ACL_READ: int 19 | ACL_TYPE_ACCESS: int 20 | ACL_TYPE_DEFAULT: int 21 | ACL_UNDEFINED_TAG: int 22 | ACL_USER: int 23 | ACL_USER_OBJ: int 24 | ACL_WRITE: int 25 | HAS_ACL_CHECK: int 26 | HAS_ACL_ENTRY: int 27 | HAS_ACL_FROM_MODE: int 28 | HAS_COPY_EXT: int 29 | HAS_EQUIV_MODE: int 30 | HAS_EXTENDED_CHECK: int 31 | TEXT_ABBREVIATE: int 32 | TEXT_ALL_EFFECTIVE: int 33 | TEXT_NUMERIC_IDS: int 34 | TEXT_SMART_INDENT: int 35 | TEXT_SOME_EFFECTIVE: int 36 | 37 | S = TypeVar('S', str, bytes, int, PathLike) 38 | 39 | def delete_default(path: str) -> None: ... 40 | def has_extended(item: S) -> bool: ... 41 | 42 | class ACL: 43 | def __init__(*args, **kwargs) -> None: ... 44 | @overload 45 | def append(self) -> 'Entry': ... 46 | @overload 47 | def append(self, __entry: 'Entry') -> 'Entry': ... 48 | def applyto(self, __item, flag: Optional[int]=0) -> None: ... 49 | def calc_mask(self) -> None: ... 50 | def check(self) -> Union[bool, Tuple[int, int]]: ... 51 | def delete_entry(self, __entry: 'Entry') -> None: ... 52 | def equiv_mode(self) -> int: ... 53 | def to_any_text(self, prefix: str=..., separator: str=..., options: int=...) -> bytes: ... 54 | def valid(self) -> bool: ... 55 | def __iter__(self) -> 'ACL': ... 56 | def __next__(self) -> 'Entry': ... 57 | def __getstate__(self) -> bytes: ... 58 | def __setstate__(self, state: bytes) -> None: ... 59 | 60 | class Entry: 61 | parent: ACL = ... 62 | permset: 'Permset' = ... 63 | qualifier: int = ... 64 | tag_type: int = ... 65 | def __init__(self, __acl: ACL) -> None: ... 66 | def copy(self, __src: 'Entry') -> None: ... 67 | 68 | class Permset: 69 | execute: bool = ... 70 | read: bool = ... 71 | write: bool = ... 72 | def __init__(self, __entry: Entry) -> None: ... 73 | @classmethod 74 | def add(self, perm: int) -> None: ... 75 | def clear(self) -> None: ... 76 | def delete(self, perm: int) -> None: ... 77 | def test(self, perm: int) -> bool: ... 78 | -------------------------------------------------------------------------------- /py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iustin/pylibacl/e40d267ed95c9aedce8838571ca7b26f70808cad/py.typed -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_rpm] 2 | release = 1 3 | requires = libacl 4 | ;build_requires = libacl libacl-devel 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from setuptools import setup, Extension 5 | 6 | (u_sysname, u_nodename, u_release, u_version, u_machine) = os.uname() 7 | 8 | macros = [] 9 | libs = [] 10 | if u_sysname == "Linux": 11 | macros.append(("HAVE_LINUX", None)) 12 | macros.append(("HAVE_LEVEL2", None)) 13 | macros.append(("HAVE_ACL_COPY_EXT", None)) 14 | libs.append("acl") 15 | elif u_sysname == "GNU/kFreeBSD": 16 | macros.append(("HAVE_LINUX", None)) 17 | macros.append(("HAVE_LEVEL2", None)) 18 | macros.append(("HAVE_ACL_COPY_EXT", None)) 19 | libs.append("acl") 20 | elif u_sysname == "FreeBSD": 21 | macros.append(("HAVE_FREEBSD", None)) 22 | if int(u_release.split(".", 1)[0]) >= 7: 23 | macros.append(("HAVE_LEVEL2", None)) 24 | elif u_sysname == "Darwin": 25 | libs.append("pthread") 26 | else: 27 | raise ValueError("I don't know your system '%s'." 28 | " Please contact the author" % u_sysname) 29 | 30 | long_desc = """This is a C extension module for Python which 31 | implements POSIX ACLs manipulation. It is a wrapper on top 32 | of the systems's acl C library - see acl(5).""" 33 | 34 | version = "0.7.2" 35 | 36 | setup(name="pylibacl", 37 | version=version, 38 | description="POSIX.1e ACLs for python", 39 | long_description=long_desc, 40 | author="Iustin Pop", 41 | author_email="iustin@k1024.org", 42 | url="https://pylibacl.k1024.org/", 43 | license="LGPL", 44 | ext_modules=[Extension("posix1e", ["acl.c"], 45 | libraries=libs, 46 | define_macros=macros, 47 | )], 48 | python_requires = ">=3.7", 49 | # Note: doesn't work since it's not a package. Sigh. 50 | package_data = { 51 | '': ['py.typed', 'posix1e.pyi'], 52 | }, 53 | zip_safe=False, 54 | project_urls={ 55 | "Bug Tracker": "https://github.com/iustin/pylibacl/issues", 56 | }, 57 | classifiers = [ 58 | "Development Status :: 5 - Production/Stable", 59 | "Intended Audience :: Developers", 60 | "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", 61 | "Programming Language :: Python :: 3 :: Only", 62 | "Programming Language :: Python :: Implementation :: CPython", 63 | "Programming Language :: Python :: Implementation :: PyPy", 64 | "Operating System :: POSIX :: BSD :: FreeBSD", 65 | "Operating System :: POSIX :: Linux", 66 | "Topic :: Software Development :: Libraries :: Python Modules", 67 | "Topic :: System :: Filesystems", 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /tests/test_acls.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | 4 | """Unittests for the posix1e module""" 5 | 6 | # Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop 7 | # 8 | # This library is free software; you can redistribute it and/or 9 | # modify it under the terms of the GNU Lesser General Public 10 | # License as published by the Free Software Foundation; either 11 | # version 2.1 of the License, or (at your option) any later version. 12 | # 13 | # This library is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 | # Lesser General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Lesser General Public 19 | # License along with this library; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 21 | # 02110-1301 USA 22 | 23 | 24 | import unittest 25 | import os 26 | import tempfile 27 | import sys 28 | import platform 29 | import re 30 | import errno 31 | import operator 32 | import pytest # type: ignore 33 | import contextlib 34 | import pathlib 35 | import io 36 | 37 | import posix1e 38 | from posix1e import * 39 | 40 | TEST_DIR = os.environ.get("TEST_DIR", ".") 41 | 42 | BASIC_ACL_TEXT = "u::rw,g::r,o::-" 43 | TEXT_0755 = "u::rwx,g::rx,o::rx" 44 | 45 | # Permset permission information 46 | PERMSETS = [ 47 | (ACL_READ, "read", Permset.read), 48 | (ACL_WRITE, "write", Permset.write), 49 | (ACL_EXECUTE, "execute", Permset.execute), 50 | ] 51 | 52 | PERMSETS_IDS = [p[1] for p in PERMSETS] 53 | 54 | ALL_TAGS = [ 55 | (posix1e.ACL_USER, "user"), 56 | (posix1e.ACL_GROUP, "group"), 57 | (posix1e.ACL_USER_OBJ, "user object"), 58 | (posix1e.ACL_GROUP_OBJ, "group object"), 59 | (posix1e.ACL_MASK, "mask"), 60 | (posix1e.ACL_OTHER, "other"), 61 | ] 62 | 63 | ALL_TAG_VALUES = [i[0] for i in ALL_TAGS] 64 | ALL_TAG_DESCS = [i[1] for i in ALL_TAGS] 65 | 66 | # Fixtures and helpers 67 | 68 | def ignore_ioerror(errnum, fn, *args, **kwargs): 69 | """Call a function while ignoring some IOErrors. 70 | 71 | This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL) 72 | when doing certain operations on an invalid ACL. 73 | 74 | """ 75 | try: 76 | fn(*args, **kwargs) 77 | except IOError as err: 78 | if err.errno == errnum: 79 | return 80 | raise 81 | 82 | def assert_acl_eq(a, b): 83 | if HAS_ACL_CHECK: 84 | assert a == b 85 | assert str(a) == str(b) 86 | 87 | @pytest.fixture 88 | def testdir(): 89 | """per-test temp dir based in TEST_DIR""" 90 | with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname: 91 | yield dname 92 | 93 | def get_file(path): 94 | fh, fname = tempfile.mkstemp(".test", "xattr-", path) 95 | return fh, fname 96 | 97 | @contextlib.contextmanager 98 | def get_file_name(path): 99 | fh, fname = get_file(path) 100 | os.close(fh) 101 | yield fname 102 | 103 | @contextlib.contextmanager 104 | def get_file_fd(path): 105 | fd = get_file(path)[0] 106 | yield fd 107 | os.close(fd) 108 | 109 | @contextlib.contextmanager 110 | def get_file_object(path): 111 | fd = get_file(path)[0] 112 | with os.fdopen(fd) as f: 113 | yield f 114 | 115 | @contextlib.contextmanager 116 | def get_dir(path): 117 | yield tempfile.mkdtemp(".test", "xattr-", path) 118 | 119 | def get_symlink(path, dangling=True): 120 | """create a symlink""" 121 | fh, fname = get_file(path) 122 | os.close(fh) 123 | if dangling: 124 | os.unlink(fname) 125 | sname = fname + ".symlink" 126 | os.symlink(fname, sname) 127 | return fname, sname 128 | 129 | @contextlib.contextmanager 130 | def get_valid_symlink(path): 131 | yield get_symlink(path, dangling=False)[1] 132 | 133 | @contextlib.contextmanager 134 | def get_dangling_symlink(path): 135 | yield get_symlink(path, dangling=True)[1] 136 | 137 | @contextlib.contextmanager 138 | def get_file_and_symlink(path): 139 | yield get_symlink(path, dangling=False) 140 | 141 | @contextlib.contextmanager 142 | def get_file_and_fobject(path): 143 | fh, fname = get_file(path) 144 | with os.fdopen(fh) as fo: 145 | yield fname, fo 146 | 147 | # Wrappers that build upon existing values 148 | 149 | def as_wrapper(call, fn, closer=None): 150 | @contextlib.contextmanager 151 | def f(path): 152 | with call(path) as r: 153 | val = fn(r) 154 | yield val 155 | if closer is not None: 156 | closer(val) 157 | return f 158 | 159 | def as_bytes(call): 160 | return as_wrapper(call, lambda r: r.encode()) 161 | 162 | def as_fspath(call): 163 | return as_wrapper(call, pathlib.PurePath) 164 | 165 | def as_iostream(call): 166 | opener = lambda f: io.open(f, "r") 167 | closer = lambda r: r.close() 168 | return as_wrapper(call, opener, closer) 169 | 170 | NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)", 171 | strict=True) 172 | NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'", 173 | strict=False) 174 | 175 | require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE") 176 | require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK") 177 | require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY") 178 | require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK") 179 | require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE") 180 | require_copy_ext = pytest.mark.skipif("not HAS_COPY_EXT") 181 | 182 | # Note: ACLs are valid only for files/directories, not symbolic links 183 | # themselves, so we only create valid symlinks. 184 | FILE_P = [ 185 | get_file_name, 186 | as_bytes(get_file_name), 187 | pytest.param(as_fspath(get_file_name), 188 | marks=[NOT_BEFORE_36, NOT_PYPY]), 189 | get_dir, 190 | as_bytes(get_dir), 191 | pytest.param(as_fspath(get_dir), 192 | marks=[NOT_BEFORE_36, NOT_PYPY]), 193 | get_valid_symlink, 194 | as_bytes(get_valid_symlink), 195 | pytest.param(as_fspath(get_valid_symlink), 196 | marks=[NOT_BEFORE_36, NOT_PYPY]), 197 | ] 198 | 199 | FILE_D = [ 200 | "file name", 201 | "file name (bytes)", 202 | "file name (path)", 203 | "directory", 204 | "directory (bytes)", 205 | "directory (path)", 206 | "file via symlink", 207 | "file via symlink (bytes)", 208 | "file via symlink (path)", 209 | ] 210 | 211 | FD_P = [ 212 | get_file_fd, 213 | get_file_object, 214 | as_iostream(get_file_name), 215 | ] 216 | 217 | FD_D = [ 218 | "file FD", 219 | "file object", 220 | "file io stream", 221 | ] 222 | 223 | DIR_D = [ 224 | "directory", 225 | "directory (bytes)", 226 | "directory (path object)", 227 | ] 228 | 229 | DIR_P = [ 230 | get_dir, 231 | as_bytes(get_dir), 232 | pytest.param(as_fspath(get_dir), 233 | marks=[NOT_BEFORE_36, NOT_PYPY]), 234 | ] 235 | 236 | ALL_P = FILE_P + FD_P 237 | ALL_D = FILE_D + FD_D 238 | 239 | @pytest.fixture(params=FILE_P, ids=FILE_D) 240 | def file_subject(testdir, request): 241 | with request.param(testdir) as value: 242 | yield value 243 | 244 | @pytest.fixture(params=FD_P, ids=FD_D) 245 | def fd_subject(testdir, request): 246 | with request.param(testdir) as value: 247 | yield value 248 | 249 | @pytest.fixture(params=DIR_P, ids=DIR_D) 250 | def dir_subject(testdir, request): 251 | with request.param(testdir) as value: 252 | yield value 253 | 254 | @pytest.fixture(params=ALL_P, ids=ALL_D) 255 | def subject(testdir, request): 256 | with request.param(testdir) as value: 257 | yield value 258 | 259 | 260 | class TestLoad: 261 | """Load/create tests""" 262 | def test_from_file(self, file_subject): 263 | """Test loading ACLs from a file/directory""" 264 | acl = posix1e.ACL(file=file_subject) 265 | assert acl.valid() 266 | 267 | def test_from_dir(self, dir_subject): 268 | """Test loading ACLs from a directory""" 269 | acl2 = posix1e.ACL(filedef=dir_subject) 270 | # default ACLs might or might not be valid; missing ones are 271 | # not valid, so we don't test acl2 for validity 272 | 273 | def test_from_fd(self, fd_subject): 274 | """Test loading ACLs from a file descriptor""" 275 | acl = posix1e.ACL(fd=fd_subject) 276 | assert acl.valid() 277 | 278 | def test_from_nonexisting(self, testdir): 279 | _, fname = get_file(testdir) 280 | with pytest.raises(IOError): 281 | posix1e.ACL(file="fname"+".no-such-file") 282 | with pytest.raises(IOError): 283 | posix1e.ACL(filedef="fname"+".no-such-file") 284 | 285 | def test_from_invalid_fd(self, testdir): 286 | fd, _ = get_file(testdir) 287 | os.close(fd) 288 | with pytest.raises(IOError): 289 | posix1e.ACL(fd=fd) 290 | 291 | def test_from_empty_invalid(self): 292 | """Test creating an empty ACL""" 293 | acl1 = posix1e.ACL() 294 | assert not acl1.valid() 295 | 296 | def test_from_text(self): 297 | """Test creating an ACL from text""" 298 | acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) 299 | assert acl1.valid() 300 | 301 | # This is acl_check, but should actually be have_linux... 302 | @require_acl_check 303 | def test_from_acl(self): 304 | """Test creating an ACL from an existing ACL""" 305 | acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) 306 | acl2 = posix1e.ACL(acl=acl1) 307 | assert acl1 == acl2 308 | 309 | def test_from_acl_via_str(self): 310 | # This is needed for not HAVE_LINUX cases. 311 | acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) 312 | acl2 = posix1e.ACL(acl=acl1) 313 | assert str(acl1) == str(acl2) 314 | 315 | def test_invalid_creation_params(self, testdir): 316 | """Test that creating an ACL from multiple objects fails""" 317 | fd, _ = get_file(testdir) 318 | with pytest.raises(ValueError): 319 | posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd) 320 | 321 | def test_invalid_value_creation(self): 322 | """Test that creating an ACL from wrong specification fails""" 323 | with pytest.raises(EnvironmentError): 324 | posix1e.ACL(text="foobar") 325 | with pytest.raises(TypeError): 326 | posix1e.ACL(foo="bar") 327 | 328 | def test_uninit(self): 329 | """Checks that uninit is actually empty init""" 330 | acl = posix1e.ACL.__new__(posix1e.ACL) 331 | assert not acl.valid() 332 | e = acl.append() 333 | e.permset 334 | acl.delete_entry(e) 335 | 336 | def test_double_init(self): 337 | acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) 338 | assert acl1.valid() 339 | acl1.__init__(text=BASIC_ACL_TEXT) # type: ignore 340 | assert acl1.valid() 341 | acl2 = ACL(text=TEXT_0755) 342 | assert acl1 != acl2 343 | acl1.__init__(acl=acl2) # type: ignore 344 | assert_acl_eq(acl1, acl2) 345 | 346 | def test_reinit_failure_noop(self): 347 | a = posix1e.ACL(text=TEXT_0755) 348 | b = posix1e.ACL(acl=a) 349 | assert_acl_eq(a, b) 350 | with pytest.raises(IOError): 351 | a.__init__(text='foobar') 352 | assert_acl_eq(a, b) 353 | 354 | @pytest.mark.xfail(reason="Unreliable test, re-init doesn't always invalidate children") 355 | def test_double_init_breaks_children(self): 356 | acl = posix1e.ACL() 357 | e = acl.append() 358 | e.permset.write = True 359 | acl.__init__() # type: ignore 360 | with pytest.raises(EnvironmentError): 361 | e.permset.write = False 362 | 363 | 364 | class TestAclExtensions: 365 | """ACL extensions checks""" 366 | 367 | @require_acl_from_mode 368 | def test_from_mode(self): 369 | """Test loading ACLs from an octal mode""" 370 | acl1 = posix1e.ACL(mode=0o644) 371 | assert acl1.valid() 372 | 373 | @require_acl_check 374 | def test_acl_check(self): 375 | """Test the acl_check method""" 376 | acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) 377 | assert not acl1.check() 378 | acl2 = posix1e.ACL() 379 | c = acl2.check() 380 | assert c == (ACL_MISS_ERROR, 0) 381 | assert isinstance(c, tuple) 382 | assert c[0] == ACL_MISS_ERROR 383 | e = acl2.append() 384 | c = acl2.check() 385 | assert c == (ACL_ENTRY_ERROR, 0) 386 | 387 | def test_applyto(self, subject): 388 | """Test the apply_to function""" 389 | # TODO: add read/compare with before, once ACL can be init'ed 390 | # from any source. 391 | basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) 392 | basic_acl.applyto(subject) 393 | enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") 394 | assert enhanced_acl.valid() 395 | enhanced_acl.applyto(subject) 396 | 397 | def test_apply_to_with_wrong_object(self): 398 | acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) 399 | assert acl1.valid() 400 | with pytest.raises(TypeError): 401 | acl1.applyto(object()) 402 | with pytest.raises(TypeError): 403 | acl1.applyto(object(), object()) # type: ignore 404 | 405 | def test_apply_to_fail(self, testdir): 406 | acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) 407 | assert acl1.valid() 408 | fd, fname = get_file(testdir) 409 | os.close(fd) 410 | with pytest.raises(IOError): 411 | acl1.applyto(fd) 412 | with pytest.raises(IOError, match="no-such-file"): 413 | acl1.applyto(fname+".no-such-file") 414 | 415 | @require_extended_check 416 | def test_applyto_extended(self, subject): 417 | """Test the acl_extended function""" 418 | basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) 419 | basic_acl.applyto(subject) 420 | assert not has_extended(subject) 421 | enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") 422 | assert enhanced_acl.valid() 423 | enhanced_acl.applyto(subject) 424 | assert has_extended(subject) 425 | 426 | @require_extended_check 427 | @pytest.mark.parametrize( 428 | "gen", [ get_file_and_symlink, get_file_and_fobject ]) 429 | def test_applyto_extended_mixed(self, testdir, gen): 430 | """Test the acl_extended function""" 431 | with gen(testdir) as (a, b): 432 | basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) 433 | basic_acl.applyto(a) 434 | for item in a, b: 435 | assert not has_extended(item) 436 | enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") 437 | assert enhanced_acl.valid() 438 | enhanced_acl.applyto(b) 439 | for item in a, b: 440 | assert has_extended(item) 441 | 442 | @require_extended_check 443 | def test_extended_fail(self, testdir): 444 | fd, fname = get_file(testdir) 445 | os.close(fd) 446 | with pytest.raises(IOError): 447 | has_extended(fd) 448 | with pytest.raises(IOError, match="no-such-file"): 449 | has_extended(fname+".no-such-file") 450 | 451 | @require_extended_check 452 | def test_extended_arg_handling(self): 453 | with pytest.raises(TypeError): 454 | has_extended() # type: ignore 455 | with pytest.raises(TypeError): 456 | has_extended(object()) # type: ignore 457 | 458 | @require_equiv_mode 459 | def test_equiv_mode(self): 460 | """Test the equiv_mode function""" 461 | if HAS_ACL_FROM_MODE: 462 | for mode in 0o644, 0o755: 463 | acl = posix1e.ACL(mode=mode) 464 | assert acl.equiv_mode() == mode 465 | acl = posix1e.ACL(text="u::rw,g::r,o::r") 466 | assert acl.equiv_mode() == 0o644 467 | acl = posix1e.ACL(text="u::rx,g::-,o::-") 468 | assert acl.equiv_mode() == 0o500 469 | 470 | @require_equiv_mode 471 | @pytest.mark.xfail(reason="It seems equiv mode always passes, even for empty ACLs") 472 | def test_equiv_mode_invalid(self): 473 | """Test equiv_mode on invalid ACLs""" 474 | a = posix1e.ACL() 475 | with pytest.raises(EnvironmentError): 476 | a.equiv_mode() 477 | 478 | @require_acl_check 479 | def test_to_any_text(self): 480 | acl = posix1e.ACL(text=BASIC_ACL_TEXT) 481 | assert b"u::" in \ 482 | acl.to_any_text(options=posix1e.TEXT_ABBREVIATE) 483 | assert b"user::" in acl.to_any_text() 484 | 485 | @require_acl_check 486 | def test_to_any_text_wrong_args(self): 487 | acl = posix1e.ACL(text=BASIC_ACL_TEXT) 488 | with pytest.raises(TypeError): 489 | acl.to_any_text(foo="bar") # type: ignore 490 | 491 | 492 | @require_acl_check 493 | def test_rich_compare(self): 494 | acl1 = posix1e.ACL(text="u::rw,g::r,o::r") 495 | acl2 = posix1e.ACL(acl=acl1) 496 | acl3 = posix1e.ACL(text="u::rw,g::rw,o::r") 497 | assert acl1 == acl2 498 | assert acl1 != acl3 499 | with pytest.raises(TypeError): 500 | acl1 < acl2 # type: ignore 501 | with pytest.raises(TypeError): 502 | acl1 >= acl3 # type: ignore 503 | assert acl1 != True # type: ignore 504 | assert not (acl1 == 1) # type: ignore 505 | with pytest.raises(TypeError): 506 | acl1 > True # type: ignore 507 | 508 | @require_acl_entry 509 | def test_acl_iterator(self): 510 | acl = posix1e.ACL(text=BASIC_ACL_TEXT) 511 | for entry in acl: 512 | assert entry.parent is acl 513 | 514 | @require_copy_ext 515 | def test_acl_copy_ext(self): 516 | a = posix1e.ACL(text=BASIC_ACL_TEXT) 517 | b = posix1e.ACL() 518 | c = posix1e.ACL(acl=b) 519 | assert a != b 520 | assert b == c 521 | state = a.__getstate__() 522 | b.__setstate__(state) 523 | assert a == b 524 | assert b != c 525 | 526 | @staticmethod 527 | def get_nulled_state(src=None): 528 | """Generate a mostly-valid external serialization 529 | 530 | Passing arbitrary state into acl_copy_int() is dangerous. That 531 | C function gets a void * buffer, and then casts that to an ACL 532 | structure, irrespective of buffer length; this can lead to 533 | segfaults (via unallocated memory indexing). Depending on the 534 | exact buffer, the same code might segfault on all 535 | architectures, some architectures, all C compiler versions, or 536 | some C compilers, or any combination of the above :( 537 | 538 | To mitigate this, pass a much larger buffer size as returned 539 | from the state, just nulled out - in the Linux version of the 540 | library, the first byte is the structure size and is tested 541 | for correct size, and a null byte will cause failure. 542 | 543 | """ 544 | if src is None: 545 | src = posix1e.ACL() 546 | state = src.__getstate__() 547 | nulled = b'\x00' * (10 * len(state)) 548 | return nulled 549 | 550 | @require_copy_ext 551 | def test_acl_copy_int_failure(self): 552 | a = posix1e.ACL() 553 | nulled = self.get_nulled_state(a) 554 | with pytest.raises(IOError): 555 | a.__setstate__(nulled) 556 | 557 | @require_copy_ext 558 | def test_acl_copy_int_failure_is_noop(self): 559 | a = posix1e.ACL(text=BASIC_ACL_TEXT) 560 | b = posix1e.ACL() 561 | c = posix1e.ACL(acl=a) 562 | assert a == c 563 | assert a != b 564 | nulled = self.get_nulled_state(b) 565 | with pytest.raises(IOError): 566 | a.__setstate__(nulled) 567 | # Assert that 'a' didn't change in the attempt to restore 568 | # invalid state. 569 | assert a == c 570 | 571 | @require_copy_ext 572 | def test_acl_copy_int_args(self): 573 | a = posix1e.ACL() 574 | with pytest.raises(TypeError): 575 | a.__setstate__(None) 576 | 577 | @require_copy_ext 578 | def test_acl_init_copy_int(self): 579 | a = posix1e.ACL(text=BASIC_ACL_TEXT) 580 | b = posix1e.ACL() 581 | c = posix1e.ACL(data=a.__getstate__()) 582 | assert c != b 583 | assert c == a 584 | 585 | @require_copy_ext 586 | def test_acl_init_copy_int_invalid(self): 587 | with pytest.raises(IOError): 588 | posix1e.ACL(data=self.get_nulled_state()) 589 | 590 | 591 | class TestWrite: 592 | """Write tests""" 593 | 594 | def test_delete_default(self, testdir): 595 | """Test removing the default ACL""" 596 | with get_dir(testdir) as dname: 597 | posix1e.delete_default(dname) 598 | 599 | def test_delete_default_fail(self, testdir): 600 | """Test removing the default ACL""" 601 | with get_file_name(testdir) as fname: 602 | with pytest.raises(IOError, match="no-such-file"): 603 | posix1e.delete_default(fname+".no-such-file") 604 | 605 | @NOT_PYPY 606 | def test_delete_default_wrong_arg(self): 607 | with pytest.raises(TypeError): 608 | posix1e.delete_default(object()) # type: ignore 609 | 610 | def test_reapply(self, testdir): 611 | """Test re-applying an ACL""" 612 | fd, fname = get_file(testdir) 613 | acl1 = posix1e.ACL(fd=fd) 614 | acl1.applyto(fd) 615 | acl1.applyto(fname) 616 | with get_dir(testdir) as dname: 617 | acl2 = posix1e.ACL(file=fname) 618 | acl2.applyto(dname) 619 | 620 | 621 | 622 | @require_acl_entry 623 | class TestModification: 624 | """ACL modification tests""" 625 | 626 | def checkRef(self, obj): 627 | """Checks if a given obj has a 'sane' refcount""" 628 | if platform.python_implementation() == "PyPy": 629 | return 630 | ref_cnt = sys.getrefcount(obj) 631 | # FIXME: hardcoded value for the max ref count... but I've 632 | # seen it overflow on bad reference counting, so it's better 633 | # to be safe 634 | if ref_cnt < 2 or ref_cnt > 1024: 635 | pytest.fail("Wrong reference count, expected 2-1024 and got %d" % 636 | ref_cnt) 637 | 638 | def test_str(self): 639 | """Test str() of an ACL.""" 640 | acl = posix1e.ACL(text=BASIC_ACL_TEXT) 641 | str_acl = str(acl) 642 | self.checkRef(str_acl) 643 | 644 | def test_append(self): 645 | """Test append a new Entry to the ACL""" 646 | acl = posix1e.ACL() 647 | e = acl.append() 648 | e.tag_type = posix1e.ACL_OTHER 649 | ignore_ioerror(errno.EINVAL, acl.calc_mask) 650 | str_format = str(e) 651 | self.checkRef(str_format) 652 | e2 = acl.append(e) 653 | ignore_ioerror(errno.EINVAL, acl.calc_mask) 654 | assert not acl.valid() 655 | 656 | def test_wrong_append(self): 657 | """Test append a new Entry to the ACL based on wrong object type""" 658 | acl = posix1e.ACL() 659 | with pytest.raises(TypeError): 660 | acl.append(object()) # type: ignore 661 | 662 | @pytest.mark.xfail(reason="Behaviour not conform to specification") 663 | def test_append_invalid_source(self): 664 | a = posix1e.ACL() 665 | b = posix1e.ACL() 666 | f = b.append() 667 | b.delete_entry(f) 668 | with pytest.raises(EnvironmentError): 669 | f.permset.write = True 670 | with pytest.raises(EnvironmentError): 671 | e = a.append(f) 672 | 673 | def test_entry_creation(self): 674 | acl = posix1e.ACL() 675 | e = posix1e.Entry(acl) 676 | ignore_ioerror(errno.EINVAL, acl.calc_mask) 677 | str_format = str(e) 678 | self.checkRef(str_format) 679 | 680 | def test_entry_failed_creation(self): 681 | # Checks for partial initialisation and deletion on error 682 | # path. 683 | with pytest.raises(TypeError): 684 | posix1e.Entry(object()) # type: ignore 685 | 686 | def test_entry_reinitialisations(self): 687 | a = posix1e.ACL() 688 | b = posix1e.ACL() 689 | e = posix1e.Entry(a) 690 | e.__init__(a) # type: ignore 691 | with pytest.raises(ValueError, match="different parent"): 692 | e.__init__(b) # type: ignore 693 | 694 | @NOT_PYPY 695 | def test_entry_reinit_leaks_refcount(self): 696 | acl = posix1e.ACL() 697 | e = acl.append() 698 | ref = sys.getrefcount(acl) 699 | e.__init__(acl) # type: ignore 700 | assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..." 701 | 702 | def test_delete(self): 703 | """Test delete Entry from the ACL""" 704 | acl = posix1e.ACL() 705 | e = acl.append() 706 | e.tag_type = posix1e.ACL_OTHER 707 | ignore_ioerror(errno.EINVAL, acl.calc_mask) 708 | acl.delete_entry(e) 709 | ignore_ioerror(errno.EINVAL, acl.calc_mask) 710 | 711 | def test_double_delete(self): 712 | """Test delete Entry from the ACL""" 713 | # This is not entirely valid/correct, since the entry object 714 | # itself is invalid after the first deletion, so we're 715 | # actually testing deleting an invalid object, not a 716 | # non-existing entry... 717 | acl = posix1e.ACL() 718 | e = acl.append() 719 | e.tag_type = posix1e.ACL_OTHER 720 | ignore_ioerror(errno.EINVAL, acl.calc_mask) 721 | acl.delete_entry(e) 722 | ignore_ioerror(errno.EINVAL, acl.calc_mask) 723 | with pytest.raises(EnvironmentError): 724 | acl.delete_entry(e) 725 | 726 | def test_delete_unowned(self): 727 | """Test delete Entry from the ACL""" 728 | a = posix1e.ACL() 729 | b = posix1e.ACL() 730 | e = a.append() 731 | e.tag_type = posix1e.ACL_OTHER 732 | with pytest.raises(ValueError, match="un-owned entry"): 733 | b.delete_entry(e) 734 | 735 | # This currently fails as this deletion seems to be accepted :/ 736 | @pytest.mark.xfail(reason="Entry deletion is unreliable") 737 | def testDeleteInvalidEntry(self): 738 | """Test delete foreign Entry from the ACL""" 739 | acl1 = posix1e.ACL() 740 | acl2 = posix1e.ACL() 741 | e = acl1.append() 742 | e.tag_type = posix1e.ACL_OTHER 743 | ignore_ioerror(errno.EINVAL, acl1.calc_mask) 744 | with pytest.raises(EnvironmentError): 745 | acl2.delete_entry(e) 746 | 747 | def test_delete_invalid_object(self): 748 | """Test delete a non-Entry from the ACL""" 749 | acl = posix1e.ACL() 750 | with pytest.raises(TypeError): 751 | acl.delete_entry(object()) # type: ignore 752 | 753 | def test_double_entries(self): 754 | """Test double entries""" 755 | acl = posix1e.ACL(text=BASIC_ACL_TEXT) 756 | assert acl.valid() 757 | for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ, 758 | posix1e.ACL_OTHER): 759 | e = acl.append() 760 | e.tag_type = tag_type 761 | e.permset.clear() 762 | assert not acl.valid(), ("ACL containing duplicate entries" 763 | " should not be valid") 764 | acl.delete_entry(e) 765 | 766 | def test_multiple_good_entries(self): 767 | """Test multiple valid entries""" 768 | acl = posix1e.ACL(text=BASIC_ACL_TEXT) 769 | assert acl.valid() 770 | for tag_type in (posix1e.ACL_USER, 771 | posix1e.ACL_GROUP): 772 | for obj_id in range(5): 773 | e = acl.append() 774 | e.tag_type = tag_type 775 | e.qualifier = obj_id 776 | e.permset.clear() 777 | acl.calc_mask() 778 | assert acl.valid(), ("ACL should be able to hold multiple" 779 | " user/group entries") 780 | 781 | def test_multiple_bad_entries(self): 782 | """Test multiple invalid entries""" 783 | for tag_type in (posix1e.ACL_USER, 784 | posix1e.ACL_GROUP): 785 | acl = posix1e.ACL(text=BASIC_ACL_TEXT) 786 | assert acl.valid() 787 | e1 = acl.append() 788 | e1.tag_type = tag_type 789 | e1.qualifier = 0 790 | e1.permset.clear() 791 | acl.calc_mask() 792 | assert acl.valid(), ("ACL should be able to add a" 793 | " user/group entry") 794 | e2 = acl.append() 795 | e2.tag_type = tag_type 796 | e2.qualifier = 0 797 | e2.permset.clear() 798 | ignore_ioerror(errno.EINVAL, acl.calc_mask) 799 | assert not acl.valid(), ("ACL should not validate when" 800 | " containing two duplicate entries") 801 | acl.delete_entry(e1) 802 | # FreeBSD trips over itself here and can't delete the 803 | # entry, even though it still exists. 804 | ignore_ioerror(errno.EINVAL, acl.delete_entry, e2) 805 | 806 | def test_copy(self): 807 | acl = ACL() 808 | e1 = acl.append() 809 | e1.tag_type = ACL_USER 810 | p1 = e1.permset 811 | p1.clear() 812 | p1.read = True 813 | p1.write = True 814 | e2 = acl.append() 815 | e2.tag_type = ACL_GROUP 816 | p2 = e2.permset 817 | p2.clear() 818 | p2.read = True 819 | assert not p2.write 820 | e2.copy(e1) 821 | assert p2.write 822 | assert e1.tag_type == e2.tag_type 823 | 824 | def test_copy_wrong_arg(self): 825 | acl = ACL() 826 | e = acl.append() 827 | with pytest.raises(TypeError): 828 | e.copy(object()) # type: ignore 829 | 830 | def test_set_permset(self): 831 | acl = ACL() 832 | e1 = acl.append() 833 | e1.tag_type = ACL_USER 834 | p1 = e1.permset 835 | p1.clear() 836 | p1.read = True 837 | p1.write = True 838 | e2 = acl.append() 839 | e2.tag_type = ACL_GROUP 840 | p2 = e2.permset 841 | p2.clear() 842 | p2.read = True 843 | assert not p2.write 844 | e2.permset = p1 845 | assert e2.permset.write 846 | assert e2.tag_type == ACL_GROUP 847 | 848 | def test_set_permset_wrong_arg(self): 849 | acl = ACL() 850 | e = acl.append() 851 | with pytest.raises(TypeError): 852 | e.permset = object() # type: ignore 853 | 854 | def test_permset_creation(self): 855 | acl = ACL() 856 | e = acl.append() 857 | p1 = e.permset 858 | p2 = Permset(e) 859 | #assert p1 == p2 860 | 861 | def test_permset_creation_wrong_arg(self): 862 | with pytest.raises(TypeError): 863 | Permset(object()) # type: ignore 864 | 865 | def test_permset_reinitialisations(self): 866 | a = posix1e.ACL() 867 | e = posix1e.Entry(a) 868 | f = posix1e.Entry(a) 869 | p = e.permset 870 | p.__init__(e) # type: ignore 871 | with pytest.raises(ValueError, match="different parent"): 872 | p.__init__(f) # type: ignore 873 | 874 | @NOT_PYPY 875 | def test_permset_reinit_leaks_refcount(self): 876 | acl = posix1e.ACL() 877 | e = acl.append() 878 | p = e.permset 879 | ref = sys.getrefcount(e) 880 | p.__init__(e) # type: ignore 881 | assert ref == sys.getrefcount(e), "Uh-oh, ref leaks..." 882 | 883 | @pytest.mark.parametrize("perm, txt, accessor", 884 | PERMSETS, ids=PERMSETS_IDS) 885 | def test_permset(self, perm, txt, accessor): 886 | """Test permissions""" 887 | del accessor 888 | acl = posix1e.ACL() 889 | e = acl.append() 890 | ps = e.permset 891 | ps.clear() 892 | str_ps = str(ps) 893 | self.checkRef(str_ps) 894 | assert not ps.test(perm), ("Empty permission set should not" 895 | " have permission '%s'" % txt) 896 | ps.add(perm) 897 | assert ps.test(perm), ("Permission '%s' should exist" 898 | " after addition" % txt) 899 | str_ps = str(ps) 900 | self.checkRef(str_ps) 901 | ps.delete(perm) 902 | assert not ps.test(perm), ("Permission '%s' should not exist" 903 | " after deletion" % txt) 904 | ps.add(perm) 905 | assert ps.test(perm), ("Permission '%s' should exist" 906 | " after addition" % txt) 907 | ps.clear() 908 | assert not ps.test(perm), ("Permission '%s' should not exist" 909 | " after clearing" % txt) 910 | 911 | 912 | 913 | @pytest.mark.parametrize("perm, txt, accessor", 914 | PERMSETS, ids=PERMSETS_IDS) 915 | def test_permset_via_accessors(self, perm, txt, accessor): 916 | """Test permissions""" 917 | acl = posix1e.ACL() 918 | e = acl.append() 919 | ps = e.permset 920 | ps.clear() 921 | def getter(): 922 | return accessor.__get__(ps) # type: ignore 923 | def setter(value): 924 | return accessor.__set__(ps, value) # type: ignore 925 | str_ps = str(ps) 926 | self.checkRef(str_ps) 927 | assert not getter(), ("Empty permission set should not" 928 | " have permission '%s'" % txt) 929 | setter(True) 930 | assert ps.test(perm), ("Permission '%s' should exist" 931 | " after addition" % txt) 932 | assert getter(), ("Permission '%s' should exist" 933 | " after addition" % txt) 934 | str_ps = str(ps) 935 | self.checkRef(str_ps) 936 | setter(False) 937 | assert not ps.test(perm), ("Permission '%s' should not exist" 938 | " after deletion" % txt) 939 | assert not getter(), ("Permission '%s' should not exist" 940 | " after deletion" % txt) 941 | setter(True) 942 | assert getter() 943 | ps.clear() 944 | assert not getter() 945 | 946 | def test_permset_invalid_type(self): 947 | acl = posix1e.ACL() 948 | e = acl.append() 949 | ps = e.permset 950 | ps.clear() 951 | with pytest.raises(TypeError): 952 | ps.add("foobar") # type: ignore 953 | with pytest.raises(TypeError): 954 | ps.delete("foobar") # type: ignore 955 | with pytest.raises(TypeError): 956 | ps.test("foobar") # type: ignore 957 | with pytest.raises(ValueError): 958 | ps.write = object() # type: ignore 959 | 960 | @pytest.mark.parametrize("tag", [ACL_USER, ACL_GROUP], 961 | ids=["ACL_USER", "ACL_GROUP"]) 962 | def test_qualifier_values(self, tag): 963 | """Tests qualifier correct store/retrieval""" 964 | acl = posix1e.ACL() 965 | e = acl.append() 966 | qualifier = 1 967 | e.tag_type = tag 968 | while True: 969 | regex = re.compile("(user|group) with (u|g)id %d" % qualifier) 970 | try: 971 | e.qualifier = qualifier 972 | except OverflowError: 973 | # reached overflow condition, break 974 | break 975 | assert e.qualifier == qualifier 976 | assert regex.search(str(e)) is not None 977 | qualifier *= 2 978 | 979 | def test_qualifier_overflow(self): 980 | """Tests qualifier overflow handling""" 981 | acl = posix1e.ACL() 982 | e = acl.append() 983 | # the uid_t/gid_t are unsigned, so they can hold slightly more 984 | # than sys.maxsize*2 (on Linux). 985 | qualifier = (sys.maxsize + 1) * 2 986 | for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]: 987 | e.tag_type = tag 988 | with pytest.raises(OverflowError): 989 | e.qualifier = qualifier 990 | 991 | def test_qualifier_underflow(self): 992 | """Tests negative qualifier handling""" 993 | # Note: this presumes that uid_t/gid_t in C are unsigned... 994 | acl = posix1e.ACL() 995 | e = acl.append() 996 | for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]: 997 | e.tag_type = tag 998 | for qualifier in [-10, -5, -1]: 999 | with pytest.raises(OverflowError): 1000 | e.qualifier = qualifier 1001 | 1002 | def test_invalid_qualifier(self): 1003 | """Tests invalid qualifier handling""" 1004 | acl = posix1e.ACL() 1005 | e = acl.append() 1006 | with pytest.raises(TypeError): 1007 | e.qualifier = object() # type: ignore 1008 | with pytest.raises((TypeError, AttributeError)): 1009 | del e.qualifier 1010 | 1011 | def test_qualifier_on_wrong_tag(self): 1012 | """Tests qualifier setting on wrong tag""" 1013 | acl = posix1e.ACL() 1014 | e = acl.append() 1015 | e.tag_type = posix1e.ACL_OTHER 1016 | with pytest.raises(TypeError): 1017 | e.qualifier = 1 1018 | with pytest.raises(TypeError): 1019 | e.qualifier 1020 | 1021 | @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS) 1022 | def test_tag_types(self, tag): 1023 | """Tests tag type correct set/get""" 1024 | acl = posix1e.ACL() 1025 | e = acl.append() 1026 | e.tag_type = tag 1027 | assert e.tag_type == tag 1028 | # check we can show all tag types without breaking 1029 | assert str(e) 1030 | 1031 | @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS) 1032 | @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS) 1033 | def test_tag_overwrite(self, src_tag, dst_tag): 1034 | """Tests tag type correct set/get""" 1035 | acl = posix1e.ACL() 1036 | e = acl.append() 1037 | e.tag_type = src_tag 1038 | assert e.tag_type == src_tag 1039 | assert str(e) 1040 | e.tag_type = dst_tag 1041 | assert e.tag_type == dst_tag 1042 | assert str(e) 1043 | 1044 | def test_invalid_tags(self): 1045 | """Tests tag type incorrect set/get""" 1046 | acl = posix1e.ACL() 1047 | e = acl.append() 1048 | with pytest.raises(TypeError): 1049 | e.tag_type = object() # type: ignore 1050 | e.tag_type = posix1e.ACL_USER_OBJ 1051 | # For some reason, PyPy raises AttributeError. Strange... 1052 | with pytest.raises((TypeError, AttributeError)): 1053 | del e.tag_type 1054 | 1055 | def test_tag_wrong_overwrite(self): 1056 | acl = posix1e.ACL() 1057 | e = acl.append() 1058 | e.tag_type = posix1e.ACL_USER_OBJ 1059 | tag = max(ALL_TAG_VALUES) + 1 1060 | with pytest.raises(EnvironmentError): 1061 | e.tag_type = tag 1062 | # Check tag is still valid. 1063 | assert e.tag_type == posix1e.ACL_USER_OBJ 1064 | 1065 | if __name__ == "__main__": 1066 | unittest.main() 1067 | --------------------------------------------------------------------------------