pax_global_header00006660000000000000000000000064142005343770014517gustar00rootroot0000000000000052 comment=d6035fe2c490c801a1b0c2774f93df7f0dbfd136 freeipa-healthcheck-0.10/000077500000000000000000000000001420053437700153135ustar00rootroot00000000000000freeipa-healthcheck-0.10/.github/000077500000000000000000000000001420053437700166535ustar00rootroot00000000000000freeipa-healthcheck-0.10/.github/workflows/000077500000000000000000000000001420053437700207105ustar00rootroot00000000000000freeipa-healthcheck-0.10/.github/workflows/pipelines.yml000066400000000000000000000034761420053437700234350ustar00rootroot00000000000000name: CI on: pull_request: branches: [ master ] jobs: lint: runs-on: ubuntu-latest strategy: matrix: python-version: [3.9] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox - name: Lint with flake8 run: | tox -vv -eflake8 - name: Lint with pep8 run: | tox -vv -epep8 container: needs: lint runs-on: ubuntu-latest strategy: matrix: fedora-release: [33, 34] steps: - uses: actions/checkout@v2 - name: Lint with pylint fedora:${{ matrix.fedora-release }} run: | docker pull fedora:${{ matrix.fedora-release }} docker run \ -v ${GITHUB_WORKSPACE}:/root/src/ fedora:${{ matrix.fedora-release }} \ /bin/bash -c "\ dnf -y install \ freeipa-server \ freeipa-server-trust-ad \ tox \ python3-pylint \ python3-pytest \ ; \ cd /root/src; \ tox -vv -elint; \ " - name: pytest fedora:${{ matrix.fedora-release }} run: | docker pull fedora:${{ matrix.fedora-release }} docker run \ -v ${GITHUB_WORKSPACE}:/root/src/ fedora:${{ matrix.fedora-release }} \ /bin/bash -c "\ dnf -y install \ freeipa-server \ freeipa-server-trust-ad \ tox \ python3-pylint \ python3-pytest \ ; \ cd /root/src; \ tox -vv -epy3; \ " freeipa-healthcheck-0.10/.gitignore000066400000000000000000000000751420053437700173050ustar00rootroot00000000000000*.pyc *.egg-info /venv *,cover .coverage .pytest_cache .tox freeipa-healthcheck-0.10/COPYING000066400000000000000000001045131420053437700163520ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . freeipa-healthcheck-0.10/README.md000066400000000000000000000760531420053437700166050ustar00rootroot00000000000000# What is healthcheck? It is an attempt to answer the question "Is my IPA installation working properly." Major pain points in an IPA installation were identified and tests written to verify that the system is configured or running with expected settings. The major areas currently covered are: * Certificate configuration and expiration dates * Replication errors * Replication topology * AD Trust configuration * Service status * File permissions of important configuration files * File system space # How to use it? The simplest way to use Healthcheck is to run it from the command-line as root as ipa-healthcheck. Running from the command-line will display the output to the console unless --output-file=FILENAME is used. There is output for _all_ tests so we can be sure that an error condition isn't providing a false positive. The command-line option --failures-only will skip printing the SUCCESS conditions. If running in a tty and not using --output-file then --failures-only defaults to True. The --all option will display all output if you want/need it. To automate running Healthcheck every day a systemd timer can be used. The default destination directory for healthcheck logs is `/var/log/ipa/healthcheck` and this can be the input into a monitoring system to track changes over time or to alert if a test goes from working to error or warning. A systemd timer is provided but is not enabled by default. To enable it: # systemctl enable ipa-healthcheck.timer # systemctl start ipa-healthcheck.timer logrotate will handle log rotation and keep up to 30 days of history. This can be configured via the `/etc/logrotate.d/ipahealthcheck` file. If using upstream or if your distribution's package does not include the timer, it can be installed manually as follows. First create the destination log directory: # mkdir /var/log/ipa/healthcheck Then copy the systemd configuration into place: # cp systemd/ipa-healthcheck.timer /usr/lib/systemd/system # cp systemd/ipa-healthcheck.service /usr/lib/systemd/system Put a shell script in place to do the invocation: # cp systemd/ipa-healthcheck.sh /usr/libexec/ipa Tell systemd about it and enable it: # systemctl daemon-reload # systemctl enable ipa-healthcheck.timer # systemctl start ipa-healthcheck.timer Finally add a proper logrotate configuration: # cp logrotate/ipahealthcheck /etc/logrotate.d/ Note that logrotate requires crond to be started+enabled. To test: # systemctl start ipa-healthcheck # What if I get an error or warning? In general the output should contain enough information to provide a basic idea of why it is considered an error. If a specific value is expected then that will be provided along with the observed value. For example a number of files are checked for owner, group and permissions. If a value differs from the expected value then the expected and got values will be reported. Running from the command-line will aid in ensuring that the condition is correct to what is expected. The basic idea is that it would be iterative: 1. ipa-healthcheck 2. manually address any errors Repeat until until no errors are reported. # What about false positives? It is possible that some tests will need to be tweaked to accommodate real world situations. If you observe false positives then please open an issue at [https://github.com/freeipa/freeipa-healthcheck/issues](URL) There is no way to suppress an error without making a change either in the test or in the system to accommodate the test requirements. # Organization In order to gauge the health of a system one needs to check any number of things. These things, or checks, can be logically grouped together. This is a source. A source consists of 1..n checks. A check should be as atomic as possible to limit the scope and complexity, ideally returning a yes/no whether the check passes or fails. This is not always possible and that's ok. At a higher level than source is product. The hierarchy looks like: ipahealthcheck product source check check ... source check ... A source provides a registry so its checks are discoverable. # Writing a check module The base class for a check is ipahealthcheck.core.plugin::Plugin The only method that needs to be implemented is check(). This implements the test against the system and should yield a Result object. Because check() is a generator multiple results can be yielded from a single check. Typically each source defines its own plugin.py which contains the registry. This looks like: from ipahealthcheck.core.plugin import Registry registry = Registry() A basic check module consists of: from ipahealthcheck.core.plugin import Plugin, Result from ipahealthcheck.core import constants from ipahealthcheck.mymodule.plugin import registry @registry class MyPlugin(Plugin): def check(self): yield Result(self, constants.SUCCESS) # Return value A check yields a Result. This contains the outcome of the check including: * result as defined in ipahealthcheck/core/constants.py * kw, a python dictionary of name value pairs that provide details on the error The kw dict is meant to provide context for the check. Err on the side of too much information. Some predefined keys of the kw dictionary are: * key: some checks can have multiple tests. This provides for uniqueness. * msg: A message that can take other keywords as input * exception: used when a check raises an exception kw is optional if result is SUCCESS. If a check consist of only a single test then it is not required to yield a Result, one marking the check as successful will be added automatically. If a check is complex enough that it checks multiple values then it should yield a SUCCESS Result() for each one. A Result is required for every test done so that one can know that the check was executed. The run time duration of each check will be calculated. The mechanism differs depending on complexity. A check should normally use the @duration decorator to track the duration it took to execute the check. @registry class MyPlugin(Plugin): @duration def check(self): yield Result(self, constants.SUCCESS) # Registering a source The list of sources is stored in setup.py in the top-level of the tree. Assuming it is contained in-tree it takes the form of: 'ipahealthcheck.': [ 'name = ipahealthcheck..' ] For example, to add replication to the src/ipahealthcheck/ipa directory 'ipahealthcheck.ipa': [ 'ipacerts = ipahealthcheck.ipa.certs', 'ipafiles = ipahealthcheck.ipa.files', 'ipakerberos = ipahealthcheck.ipa.kerberos', 'replication = ipahealthcheck.ipa.replication', ], If a new branch of sources is added a new registry is needed. This is added into the ipahealthcheck.registry section in setup.py. If we decided that replication didn't belong under ipahealthcheck.ipa but instead in ipahealthcheck.ds it would look like: 'ipahealthcheck.registry': [ 'ipahealthcheck.ipa = ipahealthcheck.ipa.plugin:registry', 'ipahealthcheck.dogtag = ipahealthcheck.dogtag.plugin:registry', 'ipahealthcheck.meta = ipahealthcheck.meta.plugin:registry', 'ipahealthcheck.ds = ipahealthcheck.ds.plugin:registry', ], and 'ipahealthcheck.ds': [ 'replication = ipahealthcheck.ds.replication', ], # Execution It is possible to execute a single check or all checks in a single source by passing --source and/or --check on the command-line. This is intended to help user's quickly ensure that something is fixed by re-running a check after making a change. `--source`, when used on its own i.e. without `--check`, can also accept a module *namespace* and all sources within that namespace shall be executed. # Output Output is controlled via Output plugins. These take the global Results object and iterate over it to produce output in the desired format. The result is returned as a string. A custom Output class must implement the generate method which generates the output. A bare-bones output class is: @output_registry class Basic(Output): def generate(self, data): output = [x for x in data.output()] return output An output object can declare its own options by adding a tuple named options to the class in the form of (arg_name, dict(argparse options). An example to provide an option to indent the text to make it more readable. options = ( (--indent', dict(dest='indent', help='How deeply to indent')), ) # Meta The meta source is intended to collect basic information about the run such as the host it is run on and the time it was run. # Useful to diagnose a failed installation? No. healthcheck compares a known state to the state of the installation. If the installation failed then you are guaranteed to get a ton of false positives and all it will tell you is that your installation failed. # Testing and development The package can be tested and developed in a python virtual environment. It requires a full freeIPA deployment so full set of system packages need to be installed and an IPA master running. To create the virtual environment run: % python3 -m venv --system-site-packages venv % venv/bin/pip install -e . To use the environment % source venv/bin/activate To run the healthchecks (must be done as root for proper results): # source venv/bin/activate # ipa-healthcheck To run the tests execute the virtual environment: % pip install pytest % pytest The configuration file and directory are not yet created so you'll need to do that manually: # mkdir /etc/ipahealthcheck # echo "[default]" > /etc/ipahealthcheck/ipahealthcheck.conf # Understanding the results Here is some basic guidance on what a non-SUCCESS message from a check means. How to fix any particular result is heavily dependent on the error(s) discovered and their context. A single failure may be detected by multiple checks. ## ipahealthcheck.dogtag.ca ### DogtagCertsConfigCheck Compares the value of the CA (and KRA if installed) certificates with the value found in CS.cfg. If they don't match then the CA will likely fail to start. { "source": "ipahealthcheck.dogtag.ca", "check": "DogtagCertsConfigCheck", "result": "ERROR", "kw": { "key": "ocspSigningCert cert-pki-ca", "directive": "ca.ocsp_signing.cert", "configfile": "/var/lib/pki/pki-tomcat/conf/ca/CS.cfg", "msg": "Certificate 'ocspSigningCert cert-pki-ca' does not match the value of ca.ocsp_signing.cert in /var/lib/pki/pki-tomcat/conf/ca/CS.cfg" } } ### DogtagCertsConnectivityCheck Runs the equivalent of ipa cert-show 1 to verify basic connectivity. { "source": "ipahealthcheck.dogtag.ca", "check": "DogtagCertsConnectivityCheck", "result": "ERROR", "kw": { "msg": "Request for certificate failed, Certificate operation cannot be completed: Unable to communicate with CMS (503)" } } ## ipahealthcheck.ds.replication ### ReplicationConflictCheck Searches for entries in LDAP matching (&(!(objectclass=nstombstone))(nsds5ReplConflict=*)) { "source": "ipahealthcheck.ds.replication", "check": "ReplicationConflictCheck", "result": "ERROR", "kw": { "key": "nsuniqueid=66446001-1dd211b2+uid=bjenkins,cn=users,cn=accounts,dc=example,dc=test", "conflict": "namingConflict", "msg": "Replication conflict" } } ## ipahealthcheck.ipa.certs ### IPACertmongerExpirationCheck Loops through all expected certmonger requests and checks expiration based on what certmonger knows about the certificate. A warning is issued if the certificate expires in cert_expiration_days (the default is 28). Expired certificate: { "source": "ipahealthcheck.ipa.certs", "check": "IPACertmongerExpirationCheck", "result": "ERROR", "kw": { "key": 1234, "expiration_date", "20160101001704Z", "msg": "Request id 1234 expired on 20160101001704Z" } } Expiring certificate: { "source": "ipahealthcheck.ipa.certs", "check": "IPACertmongerExpirationCheck", "result": "WARNING", "kw": { "key": 1234, "expiration_date", "20160101001704Z", "days": 9, "msg": "Request id 1234 expires in 9 days" } } ### IPACertfileExpirationCheck Similar to IPACertmongerExpirationCheck except the certificate is pulled from the PEM file or NSS database and re-verified. This is in case the certmonger tracking becomes out-of-sync with the certificate on disk. The certificate file cannot be opened: { "source": "ipahealthcheck.ipa.certs", "check": "IPACertfileExpirationCheck", "result": "ERROR", "kw": { "key": 1234, "certfile": "/path/to/cert.pem", "error": [error], "msg": "Unable to open cert file '/path/to/cert.pem': [error]" } } The NSS database cannot be opened: { "source": "ipahealthcheck.ipa.certs", "check": "IPACertfileExpirationCheck", "result": "ERROR", "kw": { "key": 1234, "dbdir": "/path/to/nssdb", "error": [error], "msg": "Unable to open NSS database '/path/to/nssdb': [error]" } } The tracked nickname cannot be found in the NSS database: { "source": "ipahealthcheck.ipa.certs", "check": "IPACertfileExpirationCheck", "result": "ERROR", "kw": { "key": 1234, "dbdir": "/path/to/nssdb", "nickname": [nickname], "error": [error], "msg": "Unable to retrieve cert '[nickname]' from '/path/to/nssdb': [error]" } } Expired certificate: { "source": "ipahealthcheck.ipa.certs", "check": "IPACertfileExpirationCheck", "result": "ERROR", "kw": { "key": 1234, "expiration_date", "20160101001704Z", "msg": "Request id 1234 expired on 20160101001704Z" } } Expiring certificate: { "source": "ipahealthcheck.ipa.certs", "check": "IPACertfileExpirationCheck", "result": "WARNING", "kw": { "key": 1234, "expiration_date", "20160101001704Z", "days": 9, "msg": "Request id 1234 expires in 9 days" } } ### IPACAChainExpirationCheck Load the CA chain from /etc/ipa/ca.crt and test each one for expiration. This test is designed to ensure that the entire CA chain for all certificates is validated. For example, if the web or LDAP certificates have been replaced then the CA chain for those certs will reside in /etc/ipa/ca.crt. This includes an IPA CA signed by an external authority. Expiring certificate: { "source": "ipahealthcheck.ipa.certs", "check": "IPACAChainExpirationCheck", "result": "WARNING", "kw": { "path": "/etc/ipa/ca.crt", "key": "CN=Certificate Authority,O=EXAMPLE.TEST", "days": 2, "msg": "CA '{key}' is expiring in {days} days." } } Expired certificate: { "source": "ipahealthcheck.ipa.certs", "check": "IPACAChainExpirationCheck", "result": "CRITICAL", "kw": { "path": "/etc/ipa/ca.crt", "key": "CN=Certificate Authority,O=EXAMPLE.TEST", "msg": "CA '{key}' is expired." } } ### IPACertTracking Compares the certmonger tracking on the system to the expected values. A query of the expected name/value pairs in certmonger is done to certmonger. On failure the contents of the query are missing. This result would be seen either if the certificate is tracked but there is some slight change in the expected value or if the tracking is missing entirely. Missing certificate tracking: { "source": "ipahealthcheck.ipa.certs", "check": "IPACertTracking", "result": "ERROR", "kw": { "key": "cert-file=/var/lib/ipa/ra-agent.pem, key-file=/var/lib/ipa/ra-agent.key, ca-name=dogtag-ipa-ca-renew-agent, cert-storage=FILE, cert-presave-command=/usr/libexec/ipa/certmonger/renew_ra_cert_pre, cert-postsave-command=/usr/libexec/ipa/certmonger/renew_ra_cert" "msg": "Missing tracking for cert-file=/var/lib/ipa/ra-agent.pem, key-file=/var/lib/ipa/ra-agent.key, ca-name=dogtag-ipa-ca-renew-agent, cert-storage=FILE, cert-presave-command=/usr/libexec/ipa/certmonger/renew_ra_cert_pre, cert-postsave-command=/usr/libexec/ipa/certmonger/renew_ra_cert" } } An unknown certificate is being tracked by certmonger. This may be perfectly legitimate, it is provided for information only: { "source": "ipahealthcheck.ipa.certs", "check": "IPACertTracking", "result": "WARNING", "kw": { "key": 1234, "msg": "Unknown certmonger id 1234' } } ### IPACertNSSTrust The trust for certificates stored in NSS databases is compared against a known good state. { "source": "ipahealthcheck.ipa.certs", "check": "IPACertNSSTrust", "result": "ERROR", "kw": { "key": "auditSigningCert cert-pki-ca", "expected": "u,u,Pu", "got": "u,u,u", "nickname": "auditSigningCert cert-pki-ca", "dbdir": "/etc/pki/pki-tomcat/alias", "msg": "Incorrect NSS trust for auditSigningCert cert-pki-ca. Got u,u,u expected u,u,Pu" } } ### IPACertMatchCheck Ensure CA certificate entries in LDAP and NSS databases match. { "source": "ipahealthcheck.ipa.certs", "check": "IPACertMatchCheck", "result": "ERROR", "kw": { "msg": "CA Certificate from /etc/ipa/nssdb does not match /etc/ipa/ca.crt" } } ### IPADogtagCertsMatchCheck Check if Dogtag certificates present in both NSS DB and LDAP match. { "source": "ipahealthcheck.ipa.certs", "check": "IPADogtagCertsMatchCheck", "result": "ERROR", "kw": { "msg": "'subsystemCert cert-pki-ca' certificate in NSS DB does not match entry in LDAP" } } ### IPANSSChainValidation Validate the certificate chain of the NSS certificates. This executes: certutil -V -u V -e -d [dbdir] -n [nickname]. { "source": "ipahealthcheck.ipa.certs", "check": "IPANSSChainValidation", "result": "ERROR", "kw": { "key": "/etc/dirsrv/slapd-EXAMPLE-TEST:Server-Cert", "nickname": "Server-Cert", "dbdir": [path to NSS database], "reason": "certutil: certificate is invalid: Peer's Certificate issuer is not recognized.\n: ", "msg": ""Validation of Server-Cert in /etc/dirsrv/slapd-EXAMPLE-TEST/ failed: certutil: certificate is invalid: Peer's Certificate issuer is not recognized.\n " } } ### IPAOpenSSLChainValidation Validate the certificate chain of the OpenSSL certificates. This executes: openssl verify -verbose -show_chain -CAfile /etc/ipa/ca.crt /path/to/cert.pem { "source": "ipahealthcheck.ipa.certs", "check": "IPAOpenSSLChainValidation", "result": "ERROR", "kw": { "key": "/var/lib/ipa/ra-agent.pem", "reason": "O = EXAMPLE.TEST, CN = IPA RA\nerror 20 at 0 depth lookup: unable to get local issuer certificate\n", "msg": "Certificate validation for /var/lib/ipa/ra-agent.pem failed: O = EXAMPLE.TEST, CN = IPA RA\nerror 20 at 0 depth lookup: unable to get local issuer certificate\n" } } ### IPARAAgent Verify the description and userCertificate values in uid=ipara,ou=People,o=ipaca. { "source": "ipahealthcheck.ipa.certs", "check": "IPARAAgent", "result": "ERROR", "kw": { "expected": "2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST", "got": "2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST", "msg": "RA agent description does not match. Found 2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST in LDAP and expected 2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST" } } ### IPAKRAAgent Verify the description and userCertificate values in uid=ipakra,ou=people,o=kra,o=ipaca. { "source": "ipahealthcheck.ipa.certs", "check": "IPAKRAAgent", "result": "ERROR", "kw": { "expected": "2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST", "got": "2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST", "msg": "KRA agent description does not match. Found 2;7;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST in LDAP and expected 2;125;CN=Certificate Authority,O=EXAMPLE.TEST;CN=IPA RA,O=EXAMPLE.TEST" } } ### IPACertRevocation Confirm that the IPA certificates are not revoked. This uses the certmonger tracking to determine the list of certificates to validate. { "source": "ipahealthcheck.ipa.certs", "check": "IPACertRevocation", "result": "ERROR", "kw": { "key": 1234, "revocation_reason": "superseded", "msg": "Certificate is revoked, superseded" } } ### IPACertmongerCA Check that the certmonger CA configuration is correct. Evaluates dogtag-ipa-ca-renew-agent and dogtag-ipa-ca-renew-agent-reuse. { "source": "ipahealthcheck.ipa.certs", "check": "IPACertmongerCA", "result": "ERROR", "kw": { "key": "dogtag-ipa-ca-renew-agent", "msg": "Certmonger CA 'dogtag-ipa-ca-renew-agent' missing" } } ## ipahealthcheck.ipa.dna ### IPADNARangeCheck This reports the configured DNA range, if any. It is expected that this is combined elsewhere for further analysis. { "source": "ipahealthcheck.ipa.dna", "check": "IPADNARangeCheck", "result": "SUCCESS", "kw": { "range_start": 1000, "range_max": 199999, "next_start": 0, "next_max": 0, } } ## ipahealthcheck.ipa.files These checks verify the owner and mode of files installed or configured by IPA. There are many permutations of file permissions and ownership that may be valid and continue to work. This reports on the expected values in a fresh IPA installation. Deviations are reported at the WARNING level. This covers the following checks: ### IPAFileNSSDBCheck ### IPAFileCheck ### TomcatFileCheck Examples include: { "source": "ipahealthcheck.ipa.files", "check": "IPAFileCheck", "result": "WARNING", "kw": { "key": "_etc_ipa_ca.crt_mode", "path": "/etc/ipa/ca.crt", "type": "mode", "expected": "0644", "got": "0444", "msg": "Permissions of /etc/ipa/ca.crt are 0444 and should be 0644" } } { "source": "ipahealthcheck.ipa.files", "check": "IPAFileNSSDBCheck", "result": "WARNING", "kw": { "key": "_etc_dirsrv_slapd-EXAMPLE-TEST_pkcs11.txt_mode", "path": "/etc/dirsrv/slapd-EXAMPLE-TEST/pkcs11.txt", "type": "mode", "expected": "0640", "got": "0666", "msg": "Permissions of /etc/dirsrv/slapd-EXAMPLE-TEST/pkcs11.txt are 0666 and should be 0640" } }, ## ipahealthcheck.ipa.host ### IPAHostKeytab Executes: kinit -kt /etc/krb5.keytab to verify that the host keytab is valid. ## ipahealthcheck.ipa.roles A set of information checks to report on whether the current master is the CRL generator and/or the renewal master. ### IPACRLManagerCheck { "source": "ipahealthcheck.ipa.roles", "check": "IPACRLManagerCheck", "result": "SUCCESS", "kw": { "key": "crl_manager", "crlgen_enabled": true } }, ### IPARenewalMasterCheck { "source": "ipahealthcheck.ipa.roles", "check": "IPARenewalMasterCheck", "result": "SUCCESS", "kw": { "key": "renewal_master", "master": true } } ## ipahealthcheck.ipa.topology Topology checks to check both for compliance with recommendations and errors. ### IPATopologyDomainCheck Provide the equivalent of: ipa topologysuffix-verify On failure this will return any errors discovered like connection errors or too many replication agreements. On success it will return the configured domains. { "source": "ipahealthcheck.ipa.topology", "check": "IPATopologyDomainCheck", "result": "SUCCESS", "kw": { "suffix": "domain" } }, { "source": "ipahealthcheck.ipa.topology", "check": "IPATopologyDomainCheck", "result": "SUCCESS", "kw": { "suffix": "ca" } } ## ipahealthcheck.ipa.trust Verify common AD Trust configuration issues. Checks will return SUCCESS if not configured as a trust agent or controller. ### IPATrustAgentCheck Check the sssd configuration when the machine is configured as a trust agent. provider should be ipa and ipa_server_mode should be true. { "source": "ipahealthcheck.ipa.trust", "check": "IPATrustAgentCheck", "severity": ERROR, "kw": { "key": "ipa_server_mode_false", "attr": "ipa_server_mode", "sssd_config": "/etc/sssd/sssd.conf", "domain": "ipa.example.com", "msg": "{attr} is not True in {sssd_config} in the domain {domain}" } } ### IPATrustDomainsCheck Ensure that the IPA domain is in the output of sssctl domain-list and the trust domains matches the sssd domains. If the domain lists don't match: { "source": "ipahealthcheck.ipa.trust", "check": "IPATrustDomainsCheck", "result": "ERROR", "kw": { "key": "domain-list", "sslctl": "/usr/sbin/sssctl", "sssd_domains": "ad.vm", "trust_domains": "", "msg": "{sslctl} {key} reports mismatch: sssd domains {sssd_domains} trust domains {trust_domains}" } } ### IPATrustCatalogCheck This resolves an AD user, Administrator@REALM. This populates the AD Global catalog and AD Domain Controller values in sssctl domain-status output. { "source": "ipahealthcheck.ipa.trust", "check": "IPATrustCatalogCheck", "result": "ERROR", "kw": { "key": "AD Global Catalog", "output": "Active servers:\nAD Domain Controller: root-dc.ad.vm\nIPA: ipa.example.com", "sssctl": "/usr/sbin/sssctl", "domain": "ad.vm", "msg": "{key} not found in {sssctl} 'domain-status' output: {output}" } } ### IPAsidgenpluginCheck Verifies that the sidgen plugin is enabled in the IPA 389-ds instance. { "source": "ipahealthcheck.ipa.trust", "check": "IPAsidgenpluginCheck", "result": "ERROR", "kw": { "key": "IPA SIDGEN", "error": "no such entry", "msg": "Error retrieving 389-ds plugin {key}: {error}" } } ### IPATrustAgentMemberCheck Verify that the current host is a member of cn=adtrust agents,cn=sysaccounts,cn=etc,SUFFIX. { "source": "ipahealthcheck.ipa.trust", "check": "IPATrustAgentMemberCheck", "result": "ERROR", "kw": { "key": "ipa.example.com", "group": "adtrust agents", "msg": "{key} is not a member of {group}" } } ### IPATrustControllerPrincipalCheck Verify that the current host cifs principal is a member of cn=adtrust agents,cn=sysaccounts,cn=etc,SUFFIX. { "source": "ipahealthcheck.ipa.trust", "check": "IPATrustControllerPrincipalCheck", "result": "ERROR", "kw": { "key": "cifs/ipa.example.com@EXAMPLE.COM", "group": "adtrust agents", "msg": "{key} is not a member of {group}" } } ### IPATrustControllerServiceCheck Verify that the current host starts the ADTRUST service in ipactl. { "source": "ipahealthcheck.ipa.trust", "check": "IPATrustControllerServiceCheck", "result": "ERROR", "kw": { "key": "ADTRUST", "msg": "{key} service is not enabled" } } ### IPATrustControllerConfCheck Verify that ldapi is enabled for the passdb backend in the output of net conf list: { "source": "ipahealthcheck.ipa.trust", "check": "IPATrustControllerConfCheck", "result": "ERROR", "kw": { "key": "net conf list", "got": "", "expected": "ipasam:ldapi://%2fvar%2frun%2fslapd-EXAMPLE-COM.socket", "option": "passdb backend", "msg": "{key} option {option} value {got} doesn't match expected value {expected}" } } ### IPATrustControllerGroupSIDCheck Verify that the admins group's SID ends with 512 (Domain Admins RID). { "source": "ipahealthcheck.ipa.trust", "check": "IPATrustControllerGroupSIDCheck", "result": "ERROR", "kw": { "key": "ipantsecurityidentifier", "rid": "S-1-5-21-1078564529-1875285547-1976041503-513", "msg": "{key} is not a Domain Admins RID" } } ### IPATrustPackageCheck If not a trust controller and AD trust is enabled verify that the trust-ad pkg is installed. { "source": "ipahealthcheck.ipa.trust", "check": "IPATrustPackageCheck", "result": "WARNING", "kw": { "key": "adtrustpackage", "msg": "trust-ad sub-package is not installed. Administration will be limited." } } ## ipahealthcheck.meta.services Return the status of required IPA services The following services are monitored: * certmonger * dirsrv * gssproxy * httpd * ipa_custodia * ipa_dnskeysyncd * ipa_otpd * kadmin * krb5kdc * named * pki_tomcatd * sssd The value of check is the name of the IPA service. Note that dashes are replaced with underscores in the service names. An example of a stopped service: { "source": "ipahealthcheck.meta.services", "check": "httpd", "result": "ERROR", "kw": { "status": false, "msg": "httpd: not running" } } ## ipahealthcheck.meta.core Provide basic information about the IPA master itself. ### MetaCheck Output includes the FQDN and the version of IPA. { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "ipa.example.test", "ipa_version": "4.8.0", "ipa_api_version": "2.233" } } ## ipahealthcheck.system.filesystemspace Check on available disk space. Running low can cause issues with logging, execution and backups. ### FileSystemSpaceCheck Both a percentage and raw minimum values are tested. It is possible there is some overlap depending on mount points. The minimum free is 20 percent and is currently hard coded. The following paths are checked: Path free MB /var/lib/dirsrv/ 1024 /var/lib/ipa/backup/ 512 /var/log/ 1024 /var/log/audit/ 512 /var/tmp/ 512 /tmp 512 For example a full /tmp would be reported as: { "source": "ipahealthcheck.system.filesystemspace", "check": "FileSystemSpaceCheck", "result": "ERROR", "kw": { "msg": "/tmp: free space percentage under threshold: 0% < 20%", "store": "/tmp", "percent_free": 0, "threshold": 20 } }, { "source": "ipahealthcheck.system.filesystemspace", "check": "FileSystemSpaceCheck", "result": "ERROR", "kw": { "msg": "/tmp: free space under threshold: 0 MiB < 512 MiB", "store": "/tmp", "free_space": 0, "threshold": 512 } } freeipa-healthcheck-0.10/logrotate/000077500000000000000000000000001420053437700173135ustar00rootroot00000000000000freeipa-healthcheck-0.10/logrotate/ipahealthcheck000066400000000000000000000001521420053437700221710ustar00rootroot00000000000000/var/log/ipa/healthcheck/healthcheck.log { create daily dateext missingok rotate 30 } freeipa-healthcheck-0.10/man/000077500000000000000000000000001420053437700160665ustar00rootroot00000000000000freeipa-healthcheck-0.10/man/man5/000077500000000000000000000000001420053437700167265ustar00rootroot00000000000000freeipa-healthcheck-0.10/man/man5/ipahealthcheck.conf.5000066400000000000000000000105121420053437700226740ustar00rootroot00000000000000.\" A man page for ipahealthcheck.conf .\" Copyright (C) 2019 FreeIPA Contributors see COPYING for license .\" .TH "ipahealthcheckconf" "5" "Apr 5 2019" "FreeIPA" "FreeIPA Manual Pages" .SH "NAME" ipahealthcheck.conf \- ipa-healthcheck configuration file .SH "SYNOPSIS" /etc/ipahealthcheck/ipahealthcheck.conf .SH "DESCRIPTION" The \fIipahealthcheck.conf \fRconfiguration file is used to set the defaults when running ipa\-healthcheck. .SH "SYNTAX" The configuration options are not case sensitive. The values may be case sensitive, depending on the option. Blank lines are ignored. Lines beginning with # are comments and are ignored. Valid lines consist of an option name, an equals sign and a value. Spaces surrounding equals sign are ignored. An option terminates at the end of a line. Values should not be quoted, the quotes will not be stripped. .DS L # Wrong \- don't include quotes verbose = "True" # Right \- Properly formatted options verbose = True verbose=True .DE Options must appear in the section named [default]. There are no other sections defined or used currently. Options may be defined that are not used. Be careful of misspellings, they will not be rejected. .SH "EXCLUDES" There may be reasons that a user will want to suppress some results. One example is a customer certificate that is generating a warning because it is unknown to IPA. Excluding a result key does not prevent it from running, it is filtered from the reported results. Excluding by source or key will prevent it from running at all. Services will not be excluded because other checks may rely on them (ipahealthcheck.meta.services). Each excludes type may be listed multiple times. Invalid sources, checks and/or keys will not be flagged. These configuration options are only processed when found in the EXCLUDES section and are otherwise ignored. Users are encouraged to annotate the reason for excluding the results so the reason is not lost. Results can be suppressed or excluded in three ways: .IP \(bu source, e.g. ipahealthcheck.ipa.certs .IP \(bu check, e.g. IPADNSSystemRecordsCheck .IP \(bu report key, e.g. 20210910141457 (certmonger tracking id) .SH "OPTIONS" The following options are relevant in each section. .TP [default] .TP .B cert_expiration_days\fR The number of days left before a certificate expires to start displaying a warning. The default is 28. .TP .B timeout\fR The time allowed in seconds for each check to run before being considered an error. The default is 10. .TP [excludes] .TP .B source\fR Filter results based on the check source. .TP .B check\fR Filter results based on the check name. .TP .B key\fR Filter results based on the result key in the healthcheck output. .TP All command\-line options may be included in the configuration file. Dashes must be converted to underscore for the configuration file, e.g. \-\-output\-type becomes output_type. All options, including those that don't make sense in a config file, like \-\-list\-sources, are allowed. Let the buyer beware. .TP The purpose of allowing command\-line options to be in the configuration file is for automation without having to tweak the automation script. For example, if you want the default output type to be human for the systemd timer automated runs, settting output_type=human in the configuration file will do this. When loading configuration the first option wins, so if any option is in the configuration file then it cannot be overridden by the command-line unless a different configuration file is specified (see \-\-config). .TP There may be conflicting exceptions. For example, if all=True is set in the configuration file, and the command\-line contains \-\-failures\-only, then only failures will be displayed because of the way the option evaluation is done. .TP Options that don't make sense for the configuration file include \-\-list\-sources and \-\-input\-file. .TP .SH "FILES" .TP .I /etc/ipahealthcheck/ipahealthcheck.conf configuration file .SH "EXAMPLES" .TP 7 days left before a certificate expires to start displaying a warning: [default] cert_expiration_days=7 timeout=5 .RS L [default] cert_expiration_days=7 .RE Exclude all certificate checks. .RS L [excludes] source=ipahealthcheck.ipa.certs .RE Don't warn about a custom certificate being tracked by certmonger: .RS L [excludes] key=20210910141452 .RE .SH "SEE ALSO" .BR ipa\-healthcheck (8) freeipa-healthcheck-0.10/man/man8/000077500000000000000000000000001420053437700167315ustar00rootroot00000000000000freeipa-healthcheck-0.10/man/man8/ipa-healthcheck.8000066400000000000000000000114531420053437700220400ustar00rootroot00000000000000.\" A man page for ipa-healthcheck .\" Copyright (C) 2019 FreeIPA Contributors see COPYING for license .\" .TH "ipa-healthcheck" "8" "Jan 16 2020" "FreeIPA" "FreeIPA Manual Pages" .SH "NAME" ipa\-healthcheck \- Check on the health of an IPA installation .SH "SYNOPSIS" ipa\-healthcheck [\fIOPTION\fR]... .SH "DESCRIPTION" An IPA installation is a complex system and identifying real or potential issues can be difficult and require a lot of analysis. This tool aims to reduce the burden of that and attempts to identify issues in advance so they can be corrected, ideally before the issue is critical. .SS "ORGANIZATION" These areas of the system to check can be logically grouped together. This grouping is called a source. A source consists of one or more checks. A check is as atomic as possible to limit the scope and complexity and provide a yes/no answer on whether that particular configuration is correct. Each check will return a result, either a result of WARNING, ERROR or CRITICAL or SUCCESS. Returning SUCCESS tells you that the check was done and was deemed correct. This should help track when the last time something was examined. Upon failure the output will include the source and check that detected the failure along with a message and name/value pairs indicating the problem. It may very well be that the check can't make a final determination and generally defaults to WARNING if it can't be sure so that it can be examined. .SS "IMPLEMENTATION DETAILS" There is no need for users to authenticate and get a ticket in advance for ipa\-healthcheck to work. Existing tickets will not be used as ipa\-healthcheck will leverage the host keytab and use a temporary credential cache. .SH "OPTIONS" .SS "COMMANDS" .TP \fB\-\-list\-sources\fR Display a list of the available sources and the checks associated with those sources. .SS "OPTIONAL ARGUMENTS" .TP \fB\-\-config\fR=\fIFILE\fR The configuration file to use. If an empty string is passed in then no configuration file is loaded. The default is /etc/ipahealthcheck/ipahealthcheck.conf. .TP \fB\-\-source\fR=\fISOURCE\fR Execute checks within the named source, or all sources in the given namespace. .TP \fB\-\-check\fR=\fICHECK\fR Execute this particular check within a source. The exact source must also be specified via \fB\-\-source\fR. .TP \fB\-\-output\-type\fR=\fITYPE\fR Set the output type. Supported variants are \fBhuman\fR, \fBjson\fR, and \fBprometheus\fR. The default is \fBjson\fR. .TP \fB\-\-failures\-only\fR Exclude SUCCESS results on output. If stdin is a tty then this will default to True. In all other cases it defaults to False. .TP \fB\-\-all\fR Report all results. .TP \fB\-\-severity=\fRSEVERITY\fR Only report errors in the requested severity of SUCCESS, WARNING, ERROR or CRITICAL. This can be provided multiple times to search on multiple levels. .TP \fB\-\-verbose\fR Generate verbose output. .TP \fB\-\-debug\fR Generate additional debugging output. .SS "JSON OUTPUT" The results are displayed as a list of result messages for each check executed in JSON format. This could be input for a monitoring system. .TP \fB\-\-output\-file\fR=\fIFILENAME\fR Write the output to this filename rather than stdout. .TP \fB\-\-input\-file\fR=\fIFILENAME\fR Read the results of a previous run and re-display them. .TP \fB\-\-indent\fR=\fIINDENT\fR Pretty\-print the JSON with this indention level. This can make the output more human\-readable. .SS "HUMAN\-READABLE OUTPUT" The results are displayed in a more human\-readable format. .TP \fB\-\-input\-file\fR=\fIFILENAME\fR Take as input a JSON results output and convert it to a more human\-readable form. .SS "PROMETHEUS OUTPUT" The results are displayed in the Prometheus text metric exposition format. .TP \fB\-\-input\-file\fR=\fIFILENAME\fR Uses the JSON-formatted results output as metrics source. \fB\-\-metric\-prefix\fR=\fIPREFIX\fR Prefix to use for metric names. .SH "EXAMPLES" .PP Execute healthcheck with the default JSON output: .PP .nf 1 \&# ipa\-healthcheck .fi .PP Execute healthcheck with a prettier JSON output: .PP .nf 1 \&# ipa\-healthcheck \-\-indent 2 .fi .PP Execute healthcheck and only display errors: .PP .nf 1 \&# ipa\-healthcheck \-\-failures\-only .fi .PP Display in human\-readable output a previous report: .PP .nf 2 \&# ipa\-healthcheck \-\-output\-type human \-\-input\-file \e \& /var/log/ipa/healthcheck/healthcheck.log .fi .SH "FILES" .TP /etc/ipahealthcheck/ipahealthcheck.conf .SH "EXIT STATUS" 0 if all checks were successful 1 if any one check failed or the command failed to execute properly .SH INTERNET RESOURCES Main website: https://www.freeipa.org/ Git repository for ipa-healthcheck: https://www.github.com/freeipa/freeipa-healthcheck/ .SH OTHER RESOURCES The ipa-healthcheck distribution includes a documentation file named README.md which contains detailed explanations on executed checks. freeipa-healthcheck-0.10/pylint_plugins.py000066400000000000000000000411661420053437700207550ustar00rootroot00000000000000# # Copyright (C) 2015 FreeIPA Contributors see COPYING for license # from __future__ import print_function import copy import os.path import sys import textwrap from astroid import MANAGER, register_module_extender from astroid import scoped_nodes from pylint.checkers import BaseChecker from pylint.checkers.utils import check_messages from pylint.interfaces import IAstroidChecker from astroid.builder import AstroidBuilder def register(linter): linter.register_checker(IPAChecker(linter)) def _warning_already_exists(cls, member): print( "WARNING: member '{member}' in '{cls}' already exists".format( cls="{}.{}".format(cls.root().name, cls.name), member=member), file=sys.stderr ) def fake_class(name_or_class_obj, members=()): if isinstance(name_or_class_obj, scoped_nodes.ClassDef): cl = name_or_class_obj else: cl = scoped_nodes.ClassDef(name_or_class_obj, None) for m in members: if isinstance(m, str): if m in cl.locals: _warning_already_exists(cl, m) else: cl.locals[m] = [scoped_nodes.ClassDef(m, None)] elif isinstance(m, dict): for key, val in m.items(): assert isinstance(key, str), "key must be string" if key in cl.locals: _warning_already_exists(cl, key) fake_class(cl.locals[key], val) else: cl.locals[key] = [fake_class(key, val)] else: # here can be used any astroid type if m.name in cl.locals: _warning_already_exists(cl, m.name) else: cl.locals[m.name] = [copy.copy(m)] return cl # 'class': ['generated', 'properties'] ipa_class_members = { # Python standard library & 3rd party classes 'socket._socketobject': ['sendall'], # IPA classes 'ipalib.base.NameSpace': [ 'add', 'mod', 'del', 'show', 'find' ], 'ipalib.cli.Collector': ['__options'], 'ipalib.config.Env': [ # somehow needed for pylint on Python 2 'debug', 'startup_traceback', 'server', 'validate_api', 'verbose', ], 'ipalib.errors.ACIError': [ 'info', ], 'ipalib.errors.ConversionError': [ 'error', ], 'ipalib.errors.DatabaseError': [ 'desc', ], 'ipalib.errors.NetworkError': [ 'error', ], 'ipalib.errors.NotFound': [ 'reason', ], 'ipalib.errors.PublicError': [ 'msg', 'strerror', 'kw', ], 'ipalib.errors.SingleMatchExpected': [ 'found', ], 'ipalib.errors.SkipPluginModule': [ 'reason', ], 'ipalib.errors.ValidationError': [ 'error', ], 'ipalib.errors.SchemaUpToDate': [ 'fingerprint', 'ttl', ], 'ipalib.messages.PublicMessage': [ 'msg', 'strerror', 'type', 'kw', ], 'ipalib.parameters.Param': [ 'cli_name', 'cli_short_name', 'label', 'default', 'doc', 'required', 'multivalue', 'primary_key', 'normalizer', 'default_from', 'autofill', 'query', 'attribute', 'include', 'exclude', 'flags', 'hint', 'alwaysask', 'sortorder', 'option_group', 'no_convert', 'deprecated', ], 'ipalib.parameters.Bool': [ 'truths', 'falsehoods'], 'ipalib.parameters.Data': [ 'minlength', 'maxlength', 'length', 'pattern', 'pattern_errmsg', ], 'ipalib.parameters.Str': ['noextrawhitespace'], 'ipalib.parameters.Password': ['confirm'], 'ipalib.parameters.File': ['stdin_if_missing'], 'ipalib.parameters.Enum': ['values'], 'ipalib.parameters.Number': [ 'minvalue', 'maxvalue', ], 'ipalib.parameters.Decimal': [ 'precision', 'exponential', 'numberclass', ], 'ipalib.parameters.DNSNameParam': [ 'only_absolute', 'only_relative', ], 'ipalib.parameters.Principal': [ 'require_service', ], 'ipalib.plugable.API': [ 'Advice', ], 'ipalib.util.ForwarderValidationError': [ 'msg', ], 'ipaserver.plugins.dns.DNSRecord': [ 'validatedns', 'normalizedns', ], } def fix_ipa_classes(cls): class_name_with_module = "{}.{}".format(cls.root().name, cls.name) if class_name_with_module in ipa_class_members: fake_class(cls, ipa_class_members[class_name_with_module]) MANAGER.register_transform(scoped_nodes.ClassDef, fix_ipa_classes) def ipaplatform_constants_transform(): return AstroidBuilder(MANAGER).string_build(textwrap.dedent(''' from ipaplatform.base.constants import constants, User, Group __all__ = ('constants', 'User', 'Group') ''')) def ipaplatform_paths_transform(): return AstroidBuilder(MANAGER).string_build(textwrap.dedent(''' from ipaplatform.base.paths import paths __all__ = ('paths',) ''')) def ipaplatform_services_transform(): return AstroidBuilder(MANAGER).string_build(textwrap.dedent(''' from ipaplatform.base.services import knownservices from ipaplatform.base.services import timedate_services from ipaplatform.base.services import service from ipaplatform.base.services import wellknownservices from ipaplatform.base.services import wellknownports __all__ = ('knownservices', 'timedate_services', 'service', 'wellknownservices', 'wellknownports') ''')) def ipaplatform_tasks_transform(): return AstroidBuilder(MANAGER).string_build(textwrap.dedent(''' from ipaplatform.base.tasks import tasks __all__ = ('tasks',) ''')) register_module_extender(MANAGER, 'ipaplatform.constants', ipaplatform_constants_transform) register_module_extender(MANAGER, 'ipaplatform.paths', ipaplatform_paths_transform) register_module_extender(MANAGER, 'ipaplatform.services', ipaplatform_services_transform) register_module_extender(MANAGER, 'ipaplatform.tasks', ipaplatform_tasks_transform) def ipalib_request_transform(): """ipalib.request.context attribute """ return AstroidBuilder(MANAGER).string_build(textwrap.dedent(''' from ipalib.request import context context._pylint_attr = Connection("_pylint", lambda: None) ''')) register_module_extender(MANAGER, 'ipalib.request', ipalib_request_transform) class IPAChecker(BaseChecker): __implements__ = IAstroidChecker name = 'ipa' msgs = { 'W9901': ( 'Forbidden import %s (can\'t import from %s in %s)', 'ipa-forbidden-import', 'Used when an forbidden import is detected.', ), } options = ( ( 'forbidden-imports', { 'default': '', 'type': 'csv', 'metavar': '[:[:...]][,...]', 'help': 'Modules which are forbidden to be imported in the ' 'given paths', }, ), ) priority = -1 def open(self): self._dir = os.path.abspath(os.path.dirname(__file__)) self._forbidden_imports = {self._dir: []} for forbidden_import in self.config.forbidden_imports: forbidden_import = forbidden_import.split(':') path = os.path.join(self._dir, forbidden_import[0]) path = os.path.abspath(path) modules = forbidden_import[1:] self._forbidden_imports[path] = modules self._forbidden_imports_stack = [] def _get_forbidden_import_rule(self, node): path = node.path if path and isinstance(path, list): # In pylint 2.0, path is a list with one element. Namespace # packages may contain more than one element, but we can safely # ignore them, as they don't contain code. path = path[0] if path: path = os.path.abspath(path) while path.startswith(self._dir): if path in self._forbidden_imports: return path path = os.path.dirname(path) return self._dir def visit_module(self, node): self._forbidden_imports_stack.append( self._get_forbidden_import_rule(node)) def leave_module(self, node): self._forbidden_imports_stack.pop() def _check_forbidden_imports(self, node, names): path = self._forbidden_imports_stack[-1] relpath = os.path.relpath(path, self._dir) modules = self._forbidden_imports[path] for module in modules: module_prefix = module + '.' for name in names: if name == module or name.startswith(module_prefix): self.add_message('ipa-forbidden-import', args=(name, module, relpath), node=node) @check_messages('ipa-forbidden-import') def visit_import(self, node): names = [n[0] for n in node.names] self._check_forbidden_imports(node, names) @check_messages('ipa-forbidden-import') def visit_importfrom(self, node): names = ['{}.{}'.format(node.modname, n[0]) for n in node.names] self._check_forbidden_imports(node, names) # # Teach pylint how api object works # # ipalib uses some tricks to create api.env members and api objects. pylint # is not able to infer member names and types from code. The explict # assignments inside the string builder templates are good enough to show # pylint, how the api is created. Additional transformations are not # required. # AstroidBuilder(MANAGER).string_build(textwrap.dedent( """ from ipalib import api from ipalib import cli, plugable, rpc from ipalib.base import NameSpace from ipaclient.plugins import rpcclient try: from ipaserver.plugins import dogtag, ldap2, serverroles except ImportError: HAS_SERVER = False else: HAS_SERVER = True def wildcard(*args, **kwargs): return None # ipalib.api members api.Backend = plugable.APINameSpace(api, None) api.Command = plugable.APINameSpace(api, None) api.Method = plugable.APINameSpace(api, None) api.Object = plugable.APINameSpace(api, None) api.Updater = plugable.APINameSpace(api, None) # ipalib.api.Backend members api.Backend.cli = cli.cli(api) api.Backend.textui = cli.textui(api) api.Backend.jsonclient = rpc.jsonclient(api) api.Backend.rpcclient = rpcclient.rpcclient(api) api.Backend.xmlclient = rpc.xmlclient(api) if HAS_SERVER: api.Backend.kra = dogtag.kra(api) api.Backend.ldap2 = ldap2.ldap2(api) api.Backend.ra = dogtag.ra(api) api.Backend.ra_certprofile = dogtag.ra_certprofile(api) api.Backend.ra_lightweight_ca = dogtag.ra_lightweight_ca(api) api.Backend.serverroles = serverroles.serverroles(api) # ipalib.base.NameSpace NameSpace.find = wildcard """ )) AstroidBuilder(MANAGER).string_build(textwrap.dedent( """ from ipalib import api from ipapython.dn import DN api.env.api_version = '' api.env.bin = '' # object api.env.ca_agent_port = 0 api.env.ca_host = '' api.env.ca_install_port = None api.env.ca_port = 0 api.env.certmonger_wait_timeout = 0 api.env.conf = '' # object api.env.conf_default = '' # object api.env.confdir = '' # object api.env.container_accounts = DN() api.env.container_adtrusts = DN() api.env.container_applications = DN() api.env.container_automember = DN() api.env.container_automount = DN() api.env.container_ca = DN() api.env.container_ca_renewal = DN() api.env.container_caacl = DN() api.env.container_certmap = DN() api.env.container_certmaprules = DN() api.env.container_certprofile = DN() api.env.container_cifsdomains = DN() api.env.container_configs = DN() api.env.container_custodia = DN() api.env.container_deleteuser = DN() api.env.container_dna = DN() api.env.container_dna_posix_ids = DN() api.env.container_dns = DN() api.env.container_dnsservers = DN() api.env.container_group = DN() api.env.container_hbac = DN() api.env.container_hbacservice = DN() api.env.container_hbacservicegroup = DN() api.env.container_host = DN() api.env.container_hostgroup = DN() api.env.container_locations = DN() api.env.container_masters = DN() api.env.container_netgroup = DN() api.env.container_otp = DN() api.env.container_permission = DN() api.env.container_policies = DN() api.env.container_policygroups = DN() api.env.container_policylinks = DN() api.env.container_privilege = DN() api.env.container_radiusproxy = DN() api.env.container_ranges = DN() api.env.container_realm_domains = DN() api.env.container_rolegroup = DN() api.env.container_roles = DN() api.env.container_s4u2proxy = DN() api.env.container_selinux = DN() api.env.container_service = DN() api.env.container_stageuser = DN() api.env.container_sudocmd = DN() api.env.container_sudocmdgroup = DN() api.env.container_sudorule = DN() api.env.container_sysaccounts = DN() api.env.container_topology = DN() api.env.container_trusts = DN() api.env.container_user = DN() api.env.container_vault = DN() api.env.container_views = DN() api.env.container_virtual = DN() api.env.context = '' # object api.env.debug = False api.env.delegate = False api.env.dogtag_version = 0 api.env.dot_ipa = '' # object api.env.enable_ra = False api.env.env_confdir = None api.env.fallback = True api.env.force_schema_check = False api.env.home = '' # object api.env.host = '' api.env.host_princ = '' api.env.http_timeout = 0 api.env.in_server = False # object api.env.in_tree = False # object api.env.interactive = True api.env.ipalib = '' # object api.env.kinit_lifetime = None api.env.lite_pem = '' api.env.lite_profiler = '' api.env.lite_host = '' api.env.lite_port = 0 api.env.log = '' # object api.env.logdir = '' # object api.env.mode = '' api.env.mount_ipa = '' api.env.nss_dir = '' # object api.env.plugins_on_demand = False # object api.env.prompt_all = False api.env.ra_plugin = '' api.env.recommended_max_agmts = 0 api.env.replication_wait_timeout = 0 api.env.rpc_protocol = '' api.env.server = '' api.env.script = '' # object api.env.site_packages = '' # object api.env.skip_version_check = False api.env.smb_princ = '' api.env.startup_timeout = 0 api.env.startup_traceback = False api.env.tls_ca_cert = '' # object api.env.tls_version_max = '' api.env.tls_version_min = '' api.env.validate_api = False api.env.verbose = 0 api.env.version = '' api.env.wait_for_dns = 0 api.env.webui_prod = True """ )) # dnspython 2.x introduces enums and creates module level globals from them # pylint does not understand the trick AstroidBuilder(MANAGER).string_build(textwrap.dedent( """ import dns.flags import dns.rdataclass import dns.rdatatype dns.flags.AD = 0 dns.flags.CD = 0 dns.flags.DO = 0 dns.flags.RD = 0 dns.rdataclass.IN = 0 dns.rdatatype.A = 0 dns.rdatatype.AAAA = 0 dns.rdatatype.CNAME = 0 dns.rdatatype.DNSKEY = 0 dns.rdatatype.MX = 0 dns.rdatatype.NS = 0 dns.rdatatype.PTR = 0 dns.rdatatype.RRSIG = 0 dns.rdatatype.SOA = 0 dns.rdatatype.SRV = 0 dns.rdatatype.TXT = 0 dns.rdatatype.URI = 0 """ )) AstroidBuilder(MANAGER).string_build( textwrap.dedent( """\ from ipatests.test_integration.base import IntegrationTest from ipatests.pytest_ipa.integration.host import Host, WinHost from ipatests.pytest_ipa.integration.config import Config, Domain class PylintIPAHosts: def __getitem__(self, key): return Host() class PylintWinHosts: def __getitem__(self, key): return WinHost() class PylintADDomains: def __getitem__(self, key): return Domain() Host.config = Config() Host.domain = Domain() IntegrationTest.domain = Domain() IntegrationTest.master = Host() IntegrationTest.replicas = PylintIPAHosts() IntegrationTest.clients = PylintIPAHosts() IntegrationTest.ads = PylintWinHosts() IntegrationTest.ad_treedomains = PylintWinHosts() IntegrationTest.ad_subdomains = PylintWinHosts() IntegrationTest.ad_domains = PylintADDomains() """ ) ) freeipa-healthcheck-0.10/pylintrc000066400000000000000000000102731420053437700171050ustar00rootroot00000000000000[MASTER] # Pickle collected data for later comparisons. persistent=no # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. # FIXME: has to be specified on the command line otherwise pylint fails with # DuplicateSectionError for the IPA section #load-plugins=pylint_plugins # Use multiple processes to speed up Pylint. jobs=0 # A list of packages with safe C extensions to load extension-pkg-whitelist= _ldap, cryptography, gssapi, netifaces [CLASSES] # List of valid names for the first argument in a metaclass class method. # This can be removed after upgrading to pylint 2.0 valid-metaclass-classmethod-first-arg=cls [MESSAGES CONTROL] enable= all, python3 disable= I, duplicate-code, interface-not-implemented, no-self-use, redefined-variable-type, too-few-public-methods, too-many-ancestors, too-many-arguments, too-many-boolean-expressions, too-many-branches, too-many-instance-attributes, too-many-locals, too-many-nested-blocks, too-many-public-methods, too-many-return-statements, too-many-statements, abstract-method, anomalous-backslash-in-string, arguments-differ, attribute-defined-outside-init, bad-builtin, bad-indentation, broad-except, consider-using-dict-items, dangerous-default-value, eval-used, exec-used, fixme, global-statement, no-init, pointless-string-statement, protected-access, redefined-builtin, redefined-outer-name, super-init-not-called, undefined-loop-variable, unnecessary-lambda, unused-argument, useless-else-on-loop, bad-continuation, bad-whitespace, blacklisted-name, invalid-name, line-too-long, missing-docstring, multiple-statements, superfluous-parens, too-many-lines, unidiomatic-typecheck, no-absolute-import, wrong-import-order, ungrouped-imports, wrong-import-position, unsubscriptable-object, unsupported-membership-test, not-an-iterable, singleton-comparison, misplaced-comparison-constant, not-a-mapping, singleton-comparison, len-as-condition, # new in pylint 1.7 no-else-return, # new in pylint 1.7 single-string-used-for-slots, # new in pylint 1.7 useless-super-delegation, # new in pylint 1.7 redefined-argument-from-local, # new in pylint 1.7 consider-merging-isinstance, # new in pylint 1.7 bad-option-value, # required to support upgrade to pylint 2.0 assignment-from-no-return, # new in pylint 2.0 keyword-arg-before-vararg, # pylint 2.0, remove after dropping Python 2 consider-using-enumerate, # pylint 2.1, clean up tests later no-else-raise, # python 2.4.0 import-outside-toplevel, # pylint 2.4.2 f-string-without-interpolation, # pylint 2.5.0, bare f-strings are ok super-with-arguments, # pylint 2.6.0, zero-length form is syntactic sugar raise-missing-from, # pylint 2.6.0, implicit exception chaining is ok consider-using-with, # pylint 2.8.0, contextmanager is not mandatory consider-using-max-builtin, # pylint 2.8.0, can be more readable consider-using-min-builtin, # pylint 2.8.0, can be more readable [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html. You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=colorized # Tells whether to display a full report or only the messages reports=no # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details msg-template='{path}:{line}: [{msg_id}({symbol}), {obj}] {msg})' [VARIABLES] dummy-variables-rgx=(_.+|unused) [IPA] forbidden-imports= client/:ipaserver, ipaclient/:ipaclient.install:ipalib.install:ipaserver, ipaclient/install/:ipaserver, ipalib/:ipaclient.install:ipalib.install:ipaserver, ipalib/install/:ipaserver, ipaplatform/:ipaclient:ipalib:ipaserver, ipapython/:ipaclient:ipalib:ipaserver ipatests/pytest_ipa:ipaserver:ipaclient.install:ipalib.install ipatests/test_integration:ipaserver freeipa-healthcheck-0.10/setup.cfg000066400000000000000000000000261420053437700171320ustar00rootroot00000000000000[aliases] test=pytest freeipa-healthcheck-0.10/setup.py000066400000000000000000000065221420053437700170320ustar00rootroot00000000000000from setuptools import find_packages, setup setup( name='ipahealthcheck', version='0.10', namespace_packages=['ipahealthcheck', 'ipaclustercheck'], package_dir={'': 'src'}, # packages=find_packages(where='src'), packages=[ 'ipahealthcheck.core', 'ipahealthcheck.dogtag', 'ipahealthcheck.ds', 'ipahealthcheck.ipa', 'ipahealthcheck.meta', 'ipahealthcheck.system', 'ipaclustercheck.core', 'ipaclustercheck.ipa', ], entry_points={ # creates bin/ipahealthcheck 'console_scripts': [ 'ipa-healthcheck = ipahealthcheck.core.main:main', 'ipa-clustercheck = ipaclustercheck.core.main:main', ], # subsystem registries 'ipahealthcheck.registry': [ 'ipahealthcheck.dogtag = ipahealthcheck.dogtag.plugin:registry', 'ipahealthcheck.ipa = ipahealthcheck.ipa.plugin:registry', 'ipahealthcheck.meta = ipahealthcheck.meta.plugin:registry', 'ipahealthcheck.ds = ipahealthcheck.ds.plugin:registry', 'ipahealthcheck.system = ipahealthcheck.system.plugin:registry' ], # plugin modules for ipahealthcheck.meta registry 'ipahealthcheck.meta': [ 'meta = ipahealthcheck.meta.core', 'services = ipahealthcheck.meta.services', ], # plugin modules for ipahealthcheck.ipa registry 'ipahealthcheck.ipa': [ 'ipacerts = ipahealthcheck.ipa.certs', 'ipadna = ipahealthcheck.ipa.dna', 'ipadns = ipahealthcheck.ipa.idns', 'ipafiles = ipahealthcheck.ipa.files', 'ipahost = ipahealthcheck.ipa.host', 'ipaproxy = ipahealthcheck.ipa.proxy', 'ipameta = ipahealthcheck.ipa.meta', 'ipanss = ipahealthcheck.ipa.nss', 'iparoles = ipahealthcheck.ipa.roles', 'ipatopology = ipahealthcheck.ipa.topology', 'ipatrust = ipahealthcheck.ipa.trust', ], # plugin modules for ipahealthcheck.dogtag registry 'ipahealthcheck.dogtag': [ 'dogtagca = ipahealthcheck.dogtag.ca', ], # plugin modules for ipahealthcheck.ds registry 'ipahealthcheck.ds': [ 'dsbackends = ipahealthcheck.ds.backends', 'dsconfig = ipahealthcheck.ds.config', 'dsdiskspace = ipahealthcheck.ds.disk_space', 'dsdse = ipahealthcheck.ds.dse', 'dsencryption = ipahealthcheck.ds.encryption', 'dsfschecks = ipahealthcheck.ds.fs_checks', 'dsnssssl = ipahealthcheck.ds.nss_ssl', 'dsplugins = ipahealthcheck.ds.ds_plugins', 'dsreplication = ipahealthcheck.ds.replication', 'dsruv = ipahealthcheck.ds.ruv', ], # plugin modules for ipahealthcheck.system registry 'ipahealthcheck.system': [ 'filesystemspace = ipahealthcheck.system.filesystemspace', ], 'ipaclustercheck.registry': [ 'ipaclustercheck.ipa = ipaclustercheck.ipa.plugin:registry', ], 'ipaclustercheck.ipa': [ 'crl = ipaclustercheck.ipa.crlmanager', 'ruv = ipaclustercheck.ipa.ruv', ], }, classifiers=[ 'Programming Language :: Python :: 3.6', ], python_requires='!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', ) freeipa-healthcheck-0.10/src/000077500000000000000000000000001420053437700161025ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipaclustercheck/000077500000000000000000000000001420053437700212535ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipaclustercheck/__init__.py000066400000000000000000000001771420053437700233710ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # __import__('pkg_resources').declare_namespace(__name__) freeipa-healthcheck-0.10/src/ipaclustercheck/core/000077500000000000000000000000001420053437700222035ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipaclustercheck/core/__init__.py000066400000000000000000000000001420053437700243020ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipaclustercheck/core/main.py000066400000000000000000000015511420053437700235030ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # import sys from ipaclustercheck.core.output import output_registry from ipahealthcheck.core.core import RunChecks class ClusterChecks(RunChecks): def add_options(self): parser = self.parser parser.add_argument('--directory', dest='dir', help='Directory holding healthcheck logs') def validate_options(self): super().validate_options() if self.options.dir is None: print("--directory containing logs to check is required") return 1 return None def main(): clusterchecks = ClusterChecks(['ipaclustercheck.registry'], '/etc/ipa/clustercheck.conf', output_registry, 'ansible') sys.exit(clusterchecks.run_healthcheck()) freeipa-healthcheck-0.10/src/ipaclustercheck/core/output.py000066400000000000000000000033201420053437700241130ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import json from ipahealthcheck.core.output import OutputRegistry, Output output_registry = OutputRegistry() class ClusterOutput(Output): """Base class for writing/display output of cluster results severity doesn't apply in this case so exclude those. """ def __init__(self, options): self.filename = options.output_file def strip_output(self, results): """Nothing to strip out""" return list(results.output()) def generate(self, data): raise NotImplementedError @output_registry class Ansible(ClusterOutput): """Output information JSON format for consumption by Ansible Required keywords in a Result: name - unique identifier for the return value One of these is required: value - the return value. Type? I dunno yet error - if an error was returned """ options = ( ('--indent', dict(dest='indent', type=int, default=2, help='Indention level of JSON output')), ) def __init__(self, options): super().__init__(options) self.indent = options.indent def generate(self, data): output = [] for line in data: kw = line.get('kw') name = kw.get('name') value = kw.get('value') error = kw.get('error') if value and error: value = '%s: %s' % (error, value) elif error: value = error rval = {'%s' % name: value} output.append(rval) output = json.dumps(output, indent=self.indent) if self.filename is None: output += '\n' return output freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/000077500000000000000000000000001420053437700220245ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/__init__.py000066400000000000000000000000001420053437700241230ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/crlmanager.py000066400000000000000000000023741420053437700245170ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from ipaclustercheck.ipa.plugin import ClusterPlugin, registry, find_checks from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.core import constants @registry class ClusterCRLManagerCheck(ClusterPlugin): @duration def check(self): data = self.registry.json crlmanagers = [] for fqdn in data.keys(): output = find_checks(data[fqdn], 'ipahealthcheck.ipa.roles', 'IPACRLManagerCheck') enabled = output[0].get('kw').get('crlgen_enabled') if enabled: crlmanagers.append(fqdn) if len(crlmanagers) == 0: yield Result(self, constants.ERROR, name='crlmanager', error='No CRL Manager defined') elif len(crlmanagers) == 1: yield Result(self, constants.SUCCESS, name='crlmanager', value=crlmanagers[0]) else: yield Result(self, constants.ERROR, name='crlmanager', value=','.join(crlmanagers), error='Multiple CRL Managers defined') freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/plugin.py000066400000000000000000000065331420053437700237030ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from copy import deepcopy import json import logging from os import listdir from os.path import isfile, join from ipahealthcheck.core.plugin import Plugin, Registry from ipalib import api logger = logging.getLogger() def find_checks(data, source, check): """Look through the dict for a matching source and check. data: dict of source and check output source: name of source to find check: name of check to find Returns list of contents of source + check or empty list """ rval = [] for d in data: if d.get('source') == source and d.get('check') == check: rval.append(d) return rval def get_masters(data): """ Return the list of known masters This is determined from the list of loaded healthcheck logs. It is possible that mixed versions are used so some may not be reporting the full list of masters, so check them all, and raise an exception if the list cannot be determined. """ test_masters = list(data) masters = None for master in test_masters: output = find_checks(data[master], 'ipahealthcheck.ipa.meta', 'IPAMetaCheck') if len(output) == 0: raise ValueError('Unable to determine full list of masters. ' 'ipahealthcheck.ipa.meta:IPAMetaCheck not ' 'found.') masters = output[0].get('kw').get('masters') if masters: return masters raise ValueError('Unable to determine full list of masters. ' 'None of ipahealthcheck.ipa.meta:IPAMetaCheck ' 'contain masters.') class ClusterPlugin(Plugin): pass class ClusterRegistry(Registry): def __init__(self): super().__init__() self.json = None def initialize(self, framework, config, options=None): super().initialize(framework, config, options) self.json = {} self.load_files(options.dir) if not api.isdone('finalize'): if not api.isdone('bootstrap'): api.bootstrap(in_server=True, context='ipahealthcheck', log=None) if not api.isdone('finalize'): api.finalize() def load_files(self, dir): if self.json: return files = [f for f in listdir(dir) if isfile(join(dir, f))] for file in files: fname = join(dir, file) logger.debug("Reading %s", fname) try: with open(fname, 'r') as fd: data = fd.read() except Exception as e: logger.error("Unable to read %s: %s", fname, e) continue try: data = json.loads(data) except Exception as e: logger.error("Unable to parse JSON in %s: %s", fname, e) continue meta = find_checks(data, 'ipahealthcheck.meta.core', 'MetaCheck') if meta: fqdn = meta[0].get('kw').get('fqdn') self.json[fqdn] = deepcopy(data) else: logger.error("No fqdn defined in JSON in %s", fname) continue registry = ClusterRegistry() freeipa-healthcheck-0.10/src/ipaclustercheck/ipa/ruv.py000066400000000000000000000131711420053437700232150ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import logging from ipaclustercheck.ipa.plugin import ( ClusterPlugin, registry, find_checks, get_masters ) from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.core import constants from ipalib import api from ipapython.dn import DN logger = logging.getLogger() @registry class ClusterRUVCheck(ClusterPlugin): # TODO: confirm that all masters are represented, otherwise the # trustworthiness of dangling RUV is mixed. # # gah, need to provide full list of all masters in a check. @duration def check(self): data = self.registry.json # Start with the list of masters from the file(s) collected # and find a MetaCheck with a full list of masters. For # backwards compatibility. try: masters = get_masters(data) except ValueError as e: yield Result(self, constants.ERROR, name='dangling_ruv', error=str(e)) return if len(data.keys()) < len(masters): yield Result(self, constants.ERROR, name='dangling_ruv', error='Unable to determine list of RUVs, missing ' 'some masters: %s' % ''.join(set(masters) - set(data.keys()))) return # collect the full set of known RUVs for each master info = {} for master in masters: info[master] = { 'ca': False, # does the host have ca configured? 'ruvs': set(), # ruvs on the host 'csruvs': set(), # csruvs on the host 'clean_ruv': set(), # ruvs to be cleaned from the host 'clean_csruv': set() # csruvs to be cleaned from the host } for fqdn in data.keys(): outputs = find_checks(data[fqdn], 'ipahealthcheck.ds.ruv', 'KnownRUVCheck') for output in outputs: if not 'suffix' in output.get('kw'): continue basedn = DN(output.get('kw').get('suffix')) ruvset = set() ruvtmp = output.get('kw').get('ruvs') for ruv in ruvtmp: ruvset.add(tuple(ruv)) if basedn == DN('o=ipaca'): info[fqdn]['ca'] = True info[fqdn]['csruvs'] = ruvset elif basedn == api.env.basedn: info[fqdn]['ruvs'] = ruvset else: yield Result(self, constants.WARNING, name='dangling_ruv', error='Unknown suffix found %s expected %s' % (basedn, api.env.basedn)) # Collect the nsDS5ReplicaID for each master ruvs = set() csruvs = set() for fqdn in data.keys(): outputs = find_checks(data[fqdn], 'ipahealthcheck.ds.ruv', 'RUVCheck') for output in outputs: if not 'key' in output.get('kw'): continue basedn = DN(output.get('kw').get('key')) ruv = (fqdn, (output.get('kw').get('ruv'))) if basedn == DN('o=ipaca'): csruvs.add(ruv) elif basedn == api.env.basedn: ruvs.add(ruv) else: yield Result(self, constants.WARNING, name='dangling_ruv', error='Unknown suffix found %s expected %s' % (basedn, api.env.basedn)) dangles = False # get the dangling RUVs for master_info in info.values(): for ruv in master_info['ruvs']: if ruv not in ruvs: master_info['clean_ruv'].add(ruv) dangles = True # if ca is not configured, there will be no csruvs in master_info for csruv in master_info['csruvs']: if csruv not in csruvs: master_info['clean_csruv'].add(csruv) dangles = True clean_csruvs = set() clean_ruvs = set() if dangles: for _unused, master_info in info.items(): for ruv in master_info['clean_ruv']: logger.debug( "Dangling RUV id: %s, hostname: %s", ruv[1], ruv[0] ) clean_ruvs.add(ruv[1]) for csruv in master_info['clean_csruv']: logger.debug( "Dangling CS RUV id: %s, hostname: %s", csruv[1], csruv[0] ) clean_csruvs.add(csruv[1]) if clean_ruvs: yield Result(self, constants.ERROR, name='dangling_ruv', value=', '.join(clean_ruvs)) else: yield Result(self, constants.SUCCESS, name='dangling_ruv', value='No dangling RUVs found') if clean_csruvs: yield Result(self, constants.ERROR, name='dangling_csruv', value=', '.join(clean_csruvs)) else: yield Result(self, constants.SUCCESS, name='dangling_csruv', value='No dangling CS RUVs found') freeipa-healthcheck-0.10/src/ipahealthcheck/000077500000000000000000000000001420053437700210375ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/__init__.py000066400000000000000000000001771420053437700231550ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # __import__('pkg_resources').declare_namespace(__name__) freeipa-healthcheck-0.10/src/ipahealthcheck/core/000077500000000000000000000000001420053437700217675ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/core/__init__.py000066400000000000000000000000001420053437700240660ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/core/config.py000066400000000000000000000066301420053437700236130ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import logging import os from configparser import ConfigParser, ParsingError from collections import OrderedDict from ipahealthcheck.core.constants import CONFIG_SECTION, EXCLUDE_SECTION from ipahealthcheck.core.constants import DEFAULT_CONFIG logger = logging.getLogger() class DuplicateOrderedDict(OrderedDict): def __setitem__(self, key, value): """Duplicate keys will be concatenated strings separated by new-line""" if isinstance(value, list) and key in self: self[key].extend(value) else: super().__setitem__(key, value) class Config: """Helper class to manage configuration Let one treat config items as properties instead of using a dict. It just allows for an easier-to-read shorthand. >>> config = Config() >>> config.foo = 'bar' >>> config.foo 'bar' Return a list of the configuration option keys. >>> list(config) ['foo'] """ def __init__(self): object.__setattr__(self, '_Config__d', {}) def __setattr__(self, key, value): """ Set the attribute named ``name`` to ``value``. """ self[key] = value def __setitem__(self, key, value): """ Set ``key`` to ``value``. """ object.__setattr__(self, key, value) self.__d[key] = value def __getattr__(self, key): """ Return the value corresponding to ``key``. """ return self.__d[key] def __getitem__(self, key): """ Return the value corresponding to ``key``. """ return self.__d[key] def __contains__(self, key): """ Return True if instance contains ``key``; otherwise return False. """ return key in self.__d def __iter__(self): """ Iterate through keys in ascending order. """ for key in sorted(self.__d): yield key def merge(self, d): """ Merge variables from dict ``d`` into the configuration The first one wins. :param d: dict containing configuration """ for key in d: self.__d[key] = d[key] def read_config(config_file): """ Simple configuration file reader Read and return the configuration for only the default section. Returns a dict on success, None on failure """ config = Config() config.merge(DEFAULT_CONFIG) if not os.path.exists(config_file): logging.warning( "config file %s does not exist, using defaults", config_file ) return config parser = ConfigParser(dict_type=DuplicateOrderedDict, strict=False) try: parser.read(config_file) except ParsingError as e: logging.error("Unable to parse %s: %s", config_file, e) return None if not parser.has_section(CONFIG_SECTION): logging.error( "Config file %s missing %s section", config_file, CONFIG_SECTION ) return None items = parser.items(CONFIG_SECTION) for (key, value) in items: if not key.startswith('excludes_'): config[key] = value if parser.has_section(EXCLUDE_SECTION): items = parser.items(EXCLUDE_SECTION) for (key, value) in items: config[EXCLUDE_SECTION + '_' + key] = value.split(os.linesep) return config freeipa-healthcheck-0.10/src/ipahealthcheck/core/constants.py000066400000000000000000000025541420053437700243630ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # DEFAULT_OUTPUT = 'json' # Error reporting result SUCCESS = 0 WARNING = 10 ERROR = 20 CRITICAL = 30 _levelToName = { SUCCESS: 'SUCCESS', WARNING: 'WARNING', ERROR: 'ERROR', CRITICAL: 'CRITICAL', } _nameToLevel = { 'SUCCESS': SUCCESS, 'WARNING': WARNING, 'ERROR': ERROR, 'CRITICAL': CRITICAL, } def getLevelName(level): """ Translate between level constants and their textual mappings. If the level is one of the predefined levels then returns the corresponding string. If a numeric value corresponding to one of the defined levels is passed in instead the corresponding string representation is returned. """ name = _levelToName.get(level) or _nameToLevel.get(level) if name is not None: return name return level def getLevel(name): """ Translate between level text and their numeric constants If the level is one of the predefined levels then returns the corresponding number. """ level = _nameToLevel.get(name) if level is not None: return level return name CONFIG_FILE = '/etc/ipahealthcheck/ipahealthcheck.conf' CONFIG_SECTION = 'default' EXCLUDE_SECTION = 'excludes' DEFAULT_TIMEOUT = 10 DEFAULT_CONFIG = { 'cert_expiration_days': 28, 'timeout': DEFAULT_TIMEOUT, } freeipa-healthcheck-0.10/src/ipahealthcheck/core/core.py000066400000000000000000000401361420053437700232750ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import argparse import json import logging import pkg_resources import signal import warnings import traceback from datetime import datetime from ipahealthcheck.core.config import read_config from ipahealthcheck.core.exceptions import TimeoutError from ipahealthcheck.core.plugin import Result, Results, json_to_results from ipahealthcheck.core.output import output_registry from ipahealthcheck.core import constants from ipahealthcheck.core.service import ServiceCheck logging.basicConfig(format='%(message)s') logger = logging.getLogger() def find_registries(entry_points): # Loading the resources may reset the log level, save it. log_level = logger.level registries = {} for entry_point in entry_points: registries.update({ ep.name: ep.resolve() for ep in pkg_resources.iter_entry_points(entry_point) }) logger.setLevel(log_level) return registries def find_plugins(name, registry): for ep in pkg_resources.iter_entry_points(name): # load module ep.load() return registry.get_plugins() def run_plugin(plugin, available=(), timeout=constants.DEFAULT_TIMEOUT): def signal_handler(signum, frame): raise TimeoutError('Request timed out') # manually calculate duration when we create results of our own start = datetime.utcnow() signal.signal(signal.SIGALRM, signal_handler) signal.alarm(timeout) try: for result in plugin.check(): if result is None: # Treat no result as success, fudge start time result = Result(plugin, constants.SUCCESS, start=start) yield result except TimeoutError as e: yield Result(plugin, constants.ERROR, exception=str(e), start=start) except Exception as e: logger.debug('Exception raised: %s', e) logger.debug(traceback.format_exc()) yield Result(plugin, constants.CRITICAL, exception=str(e), traceback=traceback.format_exc(), start=start) finally: signal.alarm(0) def source_or_check_matches(plugin, source, check): """Determine whether a given a plugin matches if a source and optional check are provided. """ if ( source is not None and not _is_prefix_of_source(source, plugin.__module__) ): return False if check and plugin.__class__.__name__ != check: return False return True def exclude_source_or_check(source, check, config): """Return True if a source or check should be excluded, otherwise False""" exclude_source = [] exclude_check = [] if 'excludes_source' in config: exclude_source = config.excludes_source if 'excludes_check' in config: exclude_check = config.excludes_check for exclude in exclude_source: if _is_prefix_of_source(exclude, source): return True for exclude in exclude_check: if exclude == check: return True return False def run_service_plugins(plugins, source, check): """Execute plugins with the base class of ServiceCheck This is a specialized check to use systemd to determine if a service is running or not. """ results = Results() available = [] for plugin in plugins: if not isinstance(plugin, ServiceCheck): continue # Try to save some time to not check dependent services if the # parent is down. if not set(plugin.requires).issubset(available): # A required service is not available. Either it hasn't been # checked yet or it isn't running. If not running break. running = True for result in results.results: if result.check in plugin.requires: # if not in available but in results the service failed running = False break if not running: logger.debug( 'Skipping %s:%s because %s service(s) not running', plugin.__class__.__module__, plugin.__class__.__name__, ', '.join(set(plugin.requires) - set(available)) ) continue logger.debug('Calling check %s', plugin) for result in plugin.check(): # always run the service checks so dependencies work if result is not None and result.result == constants.SUCCESS: available.append(plugin.service.service_name) if not source_or_check_matches(plugin, source, check): continue if result is not None: results.add(result) return results, set(available) def run_plugins(plugins, available, source, check, config, timeout=constants.DEFAULT_TIMEOUT): """Execute plugins without the base class of ServiceCheck These are the remaining, non-service checking checks that do validation for various parts of a system. """ results = Results() for plugin in plugins: if isinstance(plugin, ServiceCheck): continue if exclude_source_or_check( plugin.__module__, plugin.__class__.__name__, config ): logger.debug("Excluding %s::%s per config", plugin.__module__, plugin.__class__.__name__) continue if not source_or_check_matches(plugin, source, check): continue logger.debug("Calling check %s", plugin) if not set(plugin.requires).issubset(available): logger.debug('Skipping %s:%s because %s service(s) not running', plugin.__class__.__module__, plugin.__class__.__name__, ', '.join(set(plugin.requires) - available)) # Not providing a Result in this case because if a required # service isn't available then this could generate a lot of # false positives. else: for result in run_plugin(plugin, available, timeout): results.add(result) return results def list_sources(plugins): """Print list of all sources and checks""" source = None for plugin in plugins: if source != plugin.__class__.__module__: print(plugin.__class__.__module__) source = plugin.__class__.__module__ print(" ", plugin.__class__.__name__) return 0 def add_default_options(parser, output_registry, default_output): output_names = [plugin.__name__.lower() for plugin in output_registry.plugins] parser.add_argument('--config', dest='config', default=None, help='Config file to load') parser.add_argument('--verbose', dest='verbose', action='store_true', default=False, help='Run in verbose mode') parser.add_argument('--debug', dest='debug', action='store_true', default=False, help='Include debug output') parser.add_argument('--list-sources', dest='list_sources', action='store_true', default=False, help='List all available sources') parser.add_argument('--source', dest='source', default=None, help='Source of checks, e.g. foo.bar.baz') parser.add_argument('--check', dest='check', default=None, help='Check to execute, e.g. BazCheck') parser.add_argument('--output-type', dest='output_type', choices=output_names, default=default_output, help='Output method') parser.add_argument('--output-file', dest='output_file', default=None, help='File to store output') parser.add_argument('--version', dest='version', action='store_true', help='Report the version number and exit') def add_output_options(parser, output_registry): for plugin in output_registry.plugins: onelinedoc = plugin.__doc__.split('\n\n', 1)[0].strip() group = parser.add_argument_group(plugin.__name__.lower(), onelinedoc) for option in plugin.options: group.add_argument(option[0], **option[1]) def parse_options(parser): options = parser.parse_args() # Validation if options.check and not options.source: print("--source is required when --check is used") return 1 return options def limit_results(results, source, check): """Return ony those results which match source and/or check""" new_results = Results() for result in results.results: if check is None: # treat 'source' as prefix if _is_prefix_of_source(source, result.source): new_results.add(result) else: # when 'check' is given, match source fully if result.source == source and result.check == check: new_results.add(result) return new_results def exclude_keys(config, results): """Generate a new result, excluding unwanted keys""" new_results = Results() for result in results.results: if ( 'excludes_key' in config and result.kw.get("key") in config.excludes_key ): logger.debug("Excluding %s::%s::%s per config", result.source, result.check, result.kw.get('key')) else: new_results.add(result) return new_results def _is_prefix_of_source(prefix, source): prefix_parts = prefix.split('.') source_parts = source.split('.') return source_parts[:len(prefix_parts)] == prefix_parts class RunChecks: def __init__(self, entry_points, configfile, output_registry=output_registry, default_output='json'): """Initialize class variables entry_points: A list of entry points to find plugins configfile: full path to the config file output_registry: registry containing the set of output plugins to register. default_output: default output class """ self.entry_points = entry_points self.configfile = configfile self.output_registry = output_registry self.default_output = default_output self.parser = argparse.ArgumentParser() self.options = None def pre_check(self): return None def add_options(self): """Add custom options for this check program""" def validate_options(self): """Validate options other than source and check""" return None def run_healthcheck(self): framework = object() plugins = [] output = constants.DEFAULT_OUTPUT logger.setLevel(logging.WARNING) add_default_options(self.parser, self.output_registry, self.default_output) add_output_options(self.parser, self.output_registry) self.add_options() options = parse_options(self.parser) if options.version: for registry in self.entry_points: name = registry.split('.')[0] try: version = pkg_resources.get_distribution(name).version except pkg_resources.DistributionNotFound: continue print('%s: %s' % (name, version)) return 0 # pylint: disable=assignment-from-none rval = self.validate_options() # pylint: enable=assignment-from-none if rval is not None: return rval if options.verbose: logger.setLevel(logging.INFO) if options.debug: logger.setLevel(logging.DEBUG) if options.config is not None: config = read_config(options.config) else: config = read_config(self.configfile) if config is None: return 1 # Unify config and options. One of these variables will be # eventually deprecated in the future. This way all cli # options can be set in config instead. config.merge(vars(options)) self.options = config options = config # pylint: disable=assignment-from-none rval = self.pre_check() # pylint: enable=assignment-from-none if rval is not None: return rval # If we have IPA configured without a CA then we want to skip # the pkihealthcheck plugins otherwise they will generated a # lot of false positives. The IPA plugins are loaded first so # which should set ca_configured in its registry to True or # False. We will skip the pkihealthcheck plugins only if # ca_configured is False which means that it was set by IPA. ca_configured = False for name, registry in find_registries(self.entry_points).items(): try: registry.initialize(framework, config, options) except Exception as e: warnings.warn("Trying deprecated initialization API: %s" % e, DeprecationWarning) try: registry.initialize(framework, config) except Exception as e: logger.error("Unable to initialize %s: %s", name, e) continue if hasattr(registry, 'ca_configured'): ca_configured = registry.ca_configured for name, registry in find_registries(self.entry_points).items(): if 'pkihealthcheck' in name and ca_configured is False: logger.debug('IPA CA is not configured, skipping %s', name) continue for plugin in find_plugins(name, registry): plugins.append(plugin) for out in self.output_registry.plugins: if out.__name__.lower() == options.output_type: output = out(options) break if options.list_sources: return list_sources(plugins) if 'infile' in options and options.infile: try: with open(options.infile, 'r') as f: raw_data = f.read() json_data = json.loads(raw_data) results = json_to_results(json_data) available = () except Exception as e: print("Unable to import '%s': %s" % (options.infile, e)) return 1 if options.source: results = limit_results(results, options.source, options.check) else: results, available = run_service_plugins(plugins, options.source, options.check) results.extend(run_plugins(plugins, available, options.source, options.check, config, int(config.timeout))) if options.source and len(results.results) == 0: for plugin in plugins: if not source_or_check_matches(plugin, options.source, options.check): continue if not set(plugin.requires).issubset(available): print("Source '%s' is missing one or more requirements " "'%s'" % (options.source, ', '.join(plugin.requires))) return 1 if options.check: print("Check '%s' not found in Source '%s'" % (options.check, options.source)) else: print("Source '%s' not found" % options.source) return 1 results = exclude_keys(config, results) try: output.render(results) except Exception as e: logger.error('Output raised %s: %s', e.__class__.__name__, e) return_value = 0 for result in results.results: if result.result != constants.SUCCESS: return_value = 1 break return return_value freeipa-healthcheck-0.10/src/ipahealthcheck/core/exceptions.py000066400000000000000000000001571420053437700245250ustar00rootroot00000000000000# # Copyright (C) 2021 FreeIPA Contributors see COPYING for license # class TimeoutError(Exception): pass freeipa-healthcheck-0.10/src/ipahealthcheck/core/files.py000066400000000000000000000111421420053437700234420ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import grp import os import pwd from ipahealthcheck.core import constants from ipahealthcheck.core.plugin import Result, duration class FileCheck: """Generic check to validate permission and ownership of files files is a tuple of tuples. Each tuple consists of: (path, expected_perm, expected_owner, expected_group) perm is in the form of a POSIX ACL: e.g. 0440, 0770. owner and group are names or a tuple of names, not uid/gid. If owner and/or group are tuples then all names are checked. If a match is found that that is the one reported in SUCCESS. If it fails then all values are reported. """ def __init__(self): self.files = [] @duration def check(self): for (path, owner, group, mode) in self.files: if not isinstance(owner, tuple): owner = tuple((owner,)) if not isinstance(group, tuple): group = tuple((group,)) if not os.path.exists(path): for type in ('mode', 'owner', 'group'): key = '%s_%s' % (path.replace('/', '_'), type) yield Result(self, constants.SUCCESS, key=key, type=type, path=path, msg='File does not exist') continue stat = os.stat(path) fmode = str(oct(stat.st_mode)[-4:]) key = '%s_mode' % path.replace('/', '_') if mode != fmode: if mode < fmode: yield Result(self, constants.WARNING, key=key, path=path, type='mode', expected=mode, got=fmode, msg='Permissions of %s are too permissive: ' '%s and should be %s' % (path, fmode, mode)) if mode > fmode: yield Result(self, constants.ERROR, key=key, path=path, type='mode', expected=mode, got=fmode, msg='Permissions of %s are too restrictive: ' '%s and should be %s' % (path, fmode, mode)) else: yield Result(self, constants.SUCCESS, key=key, type='mode', path=path) found = False for o in owner: fowner = pwd.getpwnam(o) if fowner.pw_uid == stat.st_uid: found = True break if not found: actual = pwd.getpwuid(stat.st_uid) key = '%s_owner' % path.replace('/', '_') if len(owner) == 1: msg = 'Ownership of %s is %s and should ' \ 'be %s' % \ (path, actual.pw_name, owner[0]) else: msg = 'Ownership of %s is %s and should ' \ 'be one of %s' % \ (path, actual.pw_name, ','.join(owner)) owner = ','.join(owner) yield Result(self, constants.WARNING, key=key, path=path, type='owner', expected=owner, got=actual.pw_name, msg=msg) else: yield Result(self, constants.SUCCESS, key=key, type='owner', path=path) found = False for g in group: fgroup = grp.getgrnam(g) if fgroup.gr_gid == stat.st_gid: found = True break if not found: key = '%s_group' % path.replace('/', '_') actual = grp.getgrgid(stat.st_gid) if len(group) == 1: msg = 'Group of %s is %s and should ' \ 'be %s' % \ (path, actual.gr_name, group[0]) else: msg = 'Group of %s is %s and should ' \ 'be one of %s' % \ (path, actual.gr_name, ','.join(group)) group = ','.join(group) yield Result(self, constants.WARNING, key=key, path=path, type='group', expected=group, got=actual.gr_name, msg=msg) else: yield Result(self, constants.SUCCESS, key=key, type='group', path=path) freeipa-healthcheck-0.10/src/ipahealthcheck/core/main.py000066400000000000000000000033021420053437700232630ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from os import environ import sys from ipahealthcheck.core import constants from ipahealthcheck.core.core import RunChecks try: from ipalib.facts import is_ipa_configured except ImportError: is_ipa_configured = None class IPAChecks(RunChecks): def pre_check(self): if is_ipa_configured is None: print("IPA server is not installed") return 1 if not is_ipa_configured(): print("IPA server is not configured") return 1 return None def add_options(self): parser = self.parser parser.add_argument('--input-file', dest='infile', help='File to read as input') parser.add_argument('--failures-only', dest='failures_only', action='store_true', default=False, help='Exclude SUCCESS results on output (see' 'man page for more details)') parser.add_argument('--all', dest='all', action='store_true', default=False, help='Report all results on output') parser.add_argument('--severity', dest='severity', action="append", help='Include only the selected severity(s)', choices=list(constants._nameToLevel)) def main(): environ["KRB5_CLIENT_KTNAME"] = "/etc/krb5.keytab" environ["KRB5CCNAME"] = "MEMORY:" ipachecks = IPAChecks(['ipahealthcheck.registry', 'pkihealthcheck.registry'], constants.CONFIG_FILE) sys.exit(ipachecks.run_healthcheck()) freeipa-healthcheck-0.10/src/ipahealthcheck/core/output.py000066400000000000000000000172211420053437700237040ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from datetime import datetime import json import sys from ipahealthcheck.core.constants import _nameToLevel, SUCCESS from ipahealthcheck.core.plugin import Registry class OutputRegistry(Registry): pass output_registry = OutputRegistry() class Output: """Base class for writing/displaying the output of results options is a tuple of argparse options that can add class-specific options for output. Output will be typically generated like: >>> output = JSON(options) >>> output.render(results) render() will: 1. Strip out any SUCCESS if requested (strip_output) 2. Generate a string to be written (generate) 3. Write to the requested file or stdout (write_file) stdout == sys.stdout by default. An Output class only needs to implement the generate() method which will render the results into a string for writing. """ def __init__(self, options): self.filename = options.output_file # Non-required options in the framework, set logical defaults to # pre 0.6 behavior with everything reported. self.severity = None self.failures_only = False self.all = True if 'failures_only' in options: self.failures_only = options.failures_only if 'all' in options: self.all = options.all if 'severity' in options: self.severity = options.severity def render(self, results): """Process the results into output""" output = self.strip_output(results) output = self.generate(output) self.write_file(output) def write_file(self, output): """Write the output to a file or sys.stdout""" if self.filename: with open(self.filename, 'w') as fd: fd.write(output) else: sys.stdout.write(output) def strip_output(self, results): """Strip out SUCCESS results if --failures-only or --severity was used Returns a list of result values. """ output = [] for line in results.output(): result = line.get('result') if _nameToLevel.get(result) == SUCCESS: if self.failures_only: continue if (not self.all and self.filename is None and not (self.failures_only is False and not sys.stdin.isatty())): continue if self.severity is not None and result not in self.severity: continue output.append(line) return output def generate(self, data): """Convert the output to the desired format, ready for writing This is the only method an output plugin is required to provide. The return value should be in ready-to-write format. Returns a string. """ raise NotImplementedError @output_registry class JSON(Output): """Output information in JSON format""" options = ( ('--indent', dict(dest='indent', type=int, default=2, help='Indention level of JSON output')), ) def __init__(self, options): super().__init__(options) self.indent = int(options.indent) def generate(self, data): output = json.dumps(data, indent=self.indent) if self.filename is None: output += '\n' return output @output_registry class Human(Output): """Display output in a more human-friendly way""" options = () def generate(self, data): if not data: return "No issues found.\n" output = '' for line in data: kw = line.get('kw') result = line.get('result') source = line.get('source') check = line.get('check') outline = '%s: %s.%s' % (result, source, check) if 'key' in kw: outline += '.%s' % kw.get('key') if 'msg' in kw: msg = kw.get('msg') err = msg.format(**kw) outline += ': %s' % err elif 'exception' in kw: outline += ': %s' % kw.get('exception') output += outline + '\n' return output @output_registry class Prometheus(Output): """Render results as Prometheus text metric exposition format""" options = ( ('--metric-prefix', dict(dest='metric_prefix', default='ipa', help='Metric name prefix')), ) def __init__(self, options): super().__init__(options) self.metric_prefix = options.metric_prefix def generate(self, data): if not data: return '\n' crt = {} svc = {} chk = {} for line in data: kw = line.get('kw') result = line.get('result') source = line.get('source') check = line.get('check') if result in chk: chk[result] += 1 else: chk[result] = 1 if source == 'ipahealthcheck.meta.services': state = 1.0 if _nameToLevel.get(result) == SUCCESS else 0.0 svc[check] = state elif (source == 'ipahealthcheck.ipa.certs' and check == "IPACertmongerExpirationCheck"): # only unsuccessful checks carry the expiration information if 'key' in kw and 'expiration_date' in kw: expiration = datetime.strptime(kw['expiration_date'], '%Y%m%d%H%M%SZ') crt[kw['key']] = expiration.timestamp() metrics = [] self.generate_check_metrics(metrics, chk) self.generate_service_metrics(metrics, svc) self.generate_certificate_metrics(metrics, crt) metrics.append('') return "\n".join(metrics) def generate_check_metrics(self, out, data): if not data: return metric_name = 'healthcheck' out.append('HELP %s_%s %s' % (self.metric_prefix, metric_name, 'Number of healthchecks with a certain result')) out.append('TYPE %s_%s gauge' % (self.metric_prefix, metric_name)) for check, quantity in data.items(): out.append('%s_%s{result="%s"} %.1f' % (self.metric_prefix, metric_name, check, quantity)) def generate_service_metrics(self, out, data): if not data: return metric_name = 'service_state' out.append('HELP %s_%s %s' % (self.metric_prefix, metric_name, 'State of the services monitored by IPA healthcheck')) out.append('TYPE %s_%s gauge' % (self.metric_prefix, metric_name)) for service, state in data.items(): out.append('%s_%s{service="%s"} %.1f' % (self.metric_prefix, metric_name, service, state)) def generate_certificate_metrics(self, out, data): if not data: return metric_name = 'cert_expiration' out.append('HELP %s_%s %s' % (self.metric_prefix, metric_name, 'Expiration date of certificates in warning/error state')) out.append('TYPE %s_%s gauge' % (self.metric_prefix, metric_name)) for certificate, timestamp in data.items(): out.append('%s_%s{certificate_request_id="%s"} %.9e' % (self.metric_prefix, metric_name, certificate, timestamp)) freeipa-healthcheck-0.10/src/ipahealthcheck/core/plugin.py000066400000000000000000000147451420053437700236520ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import uuid from datetime import datetime from functools import wraps from ipahealthcheck.core.constants import getLevelName, getLevel def duration(f): """Compute the duration of execution""" @wraps(f) def wrapper(*args, **kwds): start = datetime.utcnow() end = None for result in f(*args, **kwds): end = datetime.utcnow() dur = end - start result.duration = '%6.6f' % dur.total_seconds() yield result if end is None: # no results, yield None so a SUCCESS result will be created yield None return wrapper class Registry: """ A decorator that makes plugins available to the API Usage:: register = Registry() @register() class some_plugin(...): ... """ def __init__(self): self.plugins = [] self.framework = None self.config = dict() self.options = None def initialize(self, framework, config, options=None): self.framework = framework self.config = config self.options = options def __call__(self, cls): if not callable(cls): raise TypeError('plugin must be callable; got %r' % cls) self.plugins.append(cls) return cls def get_plugins(self): for plugincls in self.plugins: yield plugincls(self) class Plugin: """ Base class for all plugins. registry defines where the plugin was registered, normally via a pkg_resource. requires is a tuple of strings that define pre-requisites for execution. Some output formats allow plugins that do not have these requirements met to skip them (JSON does NOT, all plugins are always executed and reported). Each Plugin should define a check() method that contains as simple a test as possible on the status a unique potential issue. A Plugin may return either Result for a single result or Results if multiple issues are discovered. It is strongly recommended to keep each Plugin as discrete as possible. This is not always possible or practical, for example to avoid hundreds of plugins that test nearly the same thing. Usage:: register = Registry() @register() tmp_exists_check(Plugin) def check(self): if os.path.exists('/tmp'): result = Result(self, SUCCESS) else: result = Result(self, CRITICAL, path='/tmp', msg='Temporary directory is missing') return result """ requires = () def __init__(self, registry): self.registry = registry self.config = registry.config class Result: """ The result of a check. :param plugin: The plugin which generated the result. :param result: A result constant representing the level of error. :param source: If no plugin is passed then the name of the source can be provided directly. :param check: If no plugin is passed then the name of the check can be provided directly. :param kw: A dictionary of items providing insight in the error. Either both check and source need to be provided or plugin needs to be provided. kw is meant to provide some level of flexibility to check authors but the following is a set of pre-defined keys that may be present: key: some checks can have multiple tests. This provides for uniqueuess. msg: A message that can take other keywords as input exception: used when a check raises an exception """ def __init__(self, plugin, result, source=None, check=None, start=None, duration=None, when=None, **kw): self.result = result self.kw = kw self.when = when or generalized_time(datetime.utcnow()) self.duration = duration self.uuid = str(uuid.uuid4()) if None not in (check, source): self.check = check self.source = source else: if plugin is None: raise TypeError('source and check or plugin must be provided') self.check = plugin.__class__.__name__ self.source = plugin.__class__.__module__ if start is not None: dur = datetime.utcnow() - start self.duration = '%6.6f' % dur.total_seconds() assert getLevelName(result) is not None def __repr__(self): return "%s.%s(%s): %s" % (self.source, self.check, self.kw, self.result) class Results: """ A list-like collection of Result values. Provides a very limited subset of list operations. Is intended for internal-use only and not by check functions. Usage:: results = Results() result = Result(plugin, SUCCESS, **kw) results.add(result) """ def __init__(self): self.results = [] def __len__(self): return len(self.results) def add(self, result): assert isinstance(result, Result) self.results.append(result) def extend(self, results): assert isinstance(results, Results) self.results.extend(results.results) def output(self): for result in self.results: yield dict(source=result.source, check=result.check, result=getLevelName(result.result), uuid=result.uuid, when=result.when, duration=result.duration, kw=result.kw) def json_to_results(data): """ Convert JSON data into a Results object. :param data: valid JSON input :returns: a Results object representing the JSON input """ results = Results() for line in data: result = getLevel(line.pop('result')) source = line.pop('source') check = line.pop('check') duration = line.pop('duration') when = line.pop('when') kw = line.pop('kw') result = Result(None, result, source, check, duration=duration, when=when, **kw) results.add(result) return results def generalized_time(intime): """Convert a datetime.datetime object to LDAP generalized time format :param intime: a datetime.datetime object """ assert isinstance(intime, datetime) return intime.strftime('%Y%m%d%H%M%SZ') freeipa-healthcheck-0.10/src/ipahealthcheck/core/service.py000066400000000000000000000005321420053437700240010ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from ipahealthcheck.core.plugin import Plugin class ServiceCheck(Plugin): def __init__(self, registry): super().__init__(registry) self.service = None self.service_name = None def check(self, instance=''): raise NotImplementedError freeipa-healthcheck-0.10/src/ipahealthcheck/dogtag/000077500000000000000000000000001420053437700223045ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/dogtag/__init__.py000066400000000000000000000000001420053437700244030ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/dogtag/ca.py000066400000000000000000000111761420053437700232470ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import logging from ipahealthcheck.dogtag.plugin import DogtagPlugin, registry from ipahealthcheck.core.plugin import Result from ipahealthcheck.core.plugin import duration from ipahealthcheck.core import constants from ipalib import api, errors from ipaplatform.paths import paths from ipaserver.install import certs from ipaserver.install import krainstance from ipapython.directivesetter import get_directive from cryptography.hazmat.primitives.serialization import Encoding logger = logging.getLogger() @registry class DogtagCertsConfigCheck(DogtagPlugin): """ Compare the cert blob in the NSS database to that stored in CS.cfg """ @duration def check(self): if not self.ca.is_configured(): logger.debug("No CA configured, skipping dogtag config check") return kra = krainstance.KRAInstance(api.env.realm) blobs = {'auditSigningCert cert-pki-ca': 'ca.audit_signing.cert', 'ocspSigningCert cert-pki-ca': 'ca.ocsp_signing.cert', 'caSigningCert cert-pki-ca': 'ca.signing.cert', 'subsystemCert cert-pki-ca': 'ca.subsystem.cert', 'Server-Cert cert-pki-ca': 'ca.sslserver.cert'} # Nicknames to skip because their certs are not in CS.cfg skip = [] if kra.is_installed(): kra_blobs = { 'transportCert cert-pki-kra': 'ca.connector.KRA.transportCert', } blobs.update(kra_blobs) skip.append('storageCert cert-pki-kra') skip.append('auditSigningCert cert-pki-kra') db = certs.CertDB(api.env.realm, paths.PKI_TOMCAT_ALIAS_DIR) for nickname, _trust_flags in db.list_certs(): if nickname in skip: logger.debug('Skipping nickname %s because it isn\'t in ' 'the configuration file') continue try: val = get_directive(paths.CA_CS_CFG_PATH, blobs[nickname], '=') except KeyError: logger.debug("%s not found, assuming 3rd party", nickname) continue if val is None: yield Result(self, constants.ERROR, key=nickname, configfile=paths.CA_CS_CFG_PATH, msg='Certificate %s not found in %s' % (blobs[nickname], paths.CA_CS_CFG_PATH)) continue cert = db.get_cert_from_db(nickname) pem = cert.public_bytes(Encoding.PEM).decode() pem = pem.replace('\n', '') pem = pem.replace('-----BEGIN CERTIFICATE-----', '') pem = pem.replace('-----END CERTIFICATE-----', '') if pem.strip() != val: yield Result(self, constants.ERROR, key=nickname, directive=blobs[nickname], configfile=paths.CA_CS_CFG_PATH, msg='Certificate \'%s\' does not match the value ' 'of %s in %s' % (nickname, blobs[nickname], paths.CA_CS_CFG_PATH)) else: yield Result(self, constants.SUCCESS, key=nickname, configfile=paths.CA_CS_CFG_PATH) @registry class DogtagCertsConnectivityCheck(DogtagPlugin): """ Test basic connectivity by using cert-show to fetch a cert """ requires = ('dirsrv',) @duration def check(self): if not self.ca.is_configured(): logger.debug('CA is not configured, skipping connectivity check') return # There is nothing special about cert 1. Even if there is no cert # serial number 1 but the connection is ok it is considered passing. try: api.Command.cert_show(1, all=True) except errors.CertificateOperationError as e: if 'not found' not in str(e): yield Result(self, constants.ERROR, key='cert_show_1', msg='Request for certificate failed, %s' % e) else: yield Result(self, constants.SUCCESS) except Exception as e: yield Result(self, constants.ERROR, key='cert_show_1', msg='Request for certificate failed, %s' % e) else: yield Result(self, constants.SUCCESS) freeipa-healthcheck-0.10/src/ipahealthcheck/dogtag/plugin.py000066400000000000000000000021101420053437700241460ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from ipahealthcheck.core.plugin import Plugin, Registry from ipaserver.install import cainstance from ipaserver.install import installutils from ipalib import api, errors class DogtagPlugin(Plugin): def __init__(self, reg): super().__init__(reg) self.ca = cainstance.CAInstance(api.env.realm, host_name=api.env.host) class DogtagRegistry(Registry): def initialize(self, framework, config, options=None): super().initialize(framework, config) installutils.check_server_configuration() if not api.isdone('bootstrap'): api.bootstrap(in_server=True, context='ipahealthcheck', log=None) if not api.isdone('finalize'): api.finalize() if not api.Backend.ldap2.isconnected(): try: api.Backend.ldap2.connect() except errors.CCacheError: pass except errors.NetworkError: pass registry = DogtagRegistry() freeipa-healthcheck-0.10/src/ipahealthcheck/ds/000077500000000000000000000000001420053437700214455ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/ds/__init__.py000066400000000000000000000000001420053437700235440ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/ds/backends.py000066400000000000000000000004721420053437700235740ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipahealthcheck.ds.plugin import DSPlugin, registry from lib389.backend import Backends @registry class BackendsCheck(DSPlugin): """ Check all the backends for misconfigurations """ check_class = Backends many = True freeipa-healthcheck-0.10/src/ipahealthcheck/ds/config.py000066400000000000000000000004351420053437700232660ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipahealthcheck.ds.plugin import DSPlugin, registry from lib389.config import Config @registry class ConfigCheck(DSPlugin): """ Check the DS config for obvious errors """ check_class = Config freeipa-healthcheck-0.10/src/ipahealthcheck/ds/disk_space.py000066400000000000000000000004671420053437700241330ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipahealthcheck.ds.plugin import DSPlugin, registry from lib389.monitor import MonitorDiskSpace @registry class DiskSpaceCheck(DSPlugin): """ Check the all the disks that the DS uses """ check_class = MonitorDiskSpace freeipa-healthcheck-0.10/src/ipahealthcheck/ds/ds_plugins.py000066400000000000000000000005461420053437700241730ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipahealthcheck.ds.plugin import DSPlugin, registry from lib389.plugins import ReferentialIntegrityPlugin @registry class RIPluginCheck(DSPlugin): """ Check that the RI plugin configuration is valid and properly indexed """ check_class = ReferentialIntegrityPlugin freeipa-healthcheck-0.10/src/ipahealthcheck/ds/dse.py000066400000000000000000000004461420053437700225760ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipahealthcheck.ds.plugin import DSPlugin, registry from lib389.dseldif import DSEldif @registry class DSECheck(DSPlugin): """ Check the dse.ldif/cn=config for obvious issues """ check_class = DSEldif freeipa-healthcheck-0.10/src/ipahealthcheck/ds/encryption.py000066400000000000000000000004711420053437700242130ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipahealthcheck.ds.plugin import DSPlugin, registry from lib389.config import Encryption @registry class EncryptionCheck(DSPlugin): """ Check the DS security configuration for obvious errors """ check_class = Encryption freeipa-healthcheck-0.10/src/ipahealthcheck/ds/fs_checks.py000066400000000000000000000004501420053437700237460ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipahealthcheck.ds.plugin import DSPlugin, registry from lib389.dseldif import FSChecks @registry class FSCheck(DSPlugin): """ Check the FS for permissions issues impacting DS """ check_class = FSChecks freeipa-healthcheck-0.10/src/ipahealthcheck/ds/nss_ssl.py000066400000000000000000000004541420053437700235060ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipahealthcheck.ds.plugin import DSPlugin, registry from lib389.nss_ssl import NssSsl @registry class NssCheck(DSPlugin): """ Check the NSS database certificates for expiring issues """ check_class = NssSsl freeipa-healthcheck-0.10/src/ipahealthcheck/ds/plugin.py000066400000000000000000000104301420053437700233130ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipalib import api from ipahealthcheck.core import constants from ipahealthcheck.core.plugin import Plugin, Result, Registry, duration from ipaserver.install import dsinstance, installutils try: from ipapython.ipaldap import realm_to_serverid except ImportError: from ipaserver.install.installutils import realm_to_serverid from lib389.cli_base import disconnect_instance, connect_instance from lib389.properties import SER_LDAP_URL, SER_ROOT_DN class DSArgs(dict): """ Prepare the args to make a dirsrv connection that is compatible with lib389's Dirsrv object. """ def __init__(self, inst): self.pwdfile = None self.bindpw = None self.prompt = False self.instance = inst class DSPlugin(Plugin): requires = ('dirsrv',) check_class = None many = False def __init__(self, registry): super().__init__(registry) self.ds = self.ds = dsinstance.DsInstance() self.conn = api.Backend.ldap2 self.serverid = realm_to_serverid(api.env.realm) def convertSev(self, ds_severity): """Convert lib389 HC severity level to IDM's HC level""" sev = ds_severity.lower() if sev == 'high': return constants.CRITICAL if sev == 'medium': return constants.ERROR return constants.WARNING def doCheck(self, DSObj, many=False): """Perform a healthcheck on a specific DS/lib389 class. First we need to set up the proper args and dicts to properly connect to the LDAP server via lib389. Then run the classes' lint functions. :param DSObj: a class from lib389 that has built-in lint functions like: Backends, Replica, Encryption, NssSsl, Config, etc :returns: a list of Result objects """ args = DSArgs(self.serverid) dsrc_inst = { 'uri': args.instance, 'basedn': None, 'binddn': None, 'bindpw': None, 'saslmech': None, 'tls_cacertdir': None, 'tls_cert': None, 'tls_key': None, 'tls_reqcert': 1, 'starttls': False, 'prompt': False, 'pwdfile': None, 'args': {} } dsrc_inst['args'][SER_LDAP_URL] = dsrc_inst['uri'] dsrc_inst['args'][SER_ROOT_DN] = dsrc_inst['binddn'] inst = connect_instance(dsrc_inst=dsrc_inst, verbose=False, args=args) ds_obj = DSObj(inst) results = [] if many: # DS class that has many instances of itself (e.g. Backends) for clo in ds_obj.list(): result = clo.lint() if result is not None: # DS result could be a single or multiple results if isinstance(result, list): for single_result in result: results += single_result else: results += result else: # Single object always returns a list of results results = ds_obj.lint() hc_results = [] if results is not None: for result in results: hc_results.append(Result(self, self.convertSev(result['severity']), key=result['dsle'], items=result['items'], msg=result['detail'])) disconnect_instance(inst) return hc_results @duration def check(self): results = self.doCheck(self.check_class, self.many) if len(results) > 0: for result in results: yield result else: yield Result(self, constants.SUCCESS) class DSRegistry(Registry): def initialize(self, framework, config, options=None): super().initialize(framework, config) installutils.check_server_configuration() if not api.isdone('bootstrap'): api.bootstrap(in_server=True, context='ipahealthcheck', log=None) if not api.isdone('finalize'): api.finalize() registry = DSRegistry() freeipa-healthcheck-0.10/src/ipahealthcheck/ds/replication.py000066400000000000000000000007761420053437700243420ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from ipahealthcheck.ds.plugin import DSPlugin, registry from lib389.replica import Replica, Changelog5 @registry class ReplicationCheck(DSPlugin): """ Check the agreement status for various states, and check for conflicts """ check_class = Replica @registry class ReplicationChangelogCheck(DSPlugin): """ Check the replication changelog has some sort of trimming configured """ check_class = Changelog5 freeipa-healthcheck-0.10/src/ipahealthcheck/ds/ruv.py000066400000000000000000000071431420053437700226400ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # import logging import re from urllib.parse import urlparse from ipahealthcheck.ds.plugin import DSPlugin, registry from ipahealthcheck.core.plugin import Result from ipahealthcheck.core.plugin import duration from ipahealthcheck.core import constants from ipalib import api, errors from ipapython.dn import DN logger = logging.getLogger() @registry class RUVCheck(DSPlugin): """ Provide the main and dogtag RUV. Local analysis is not possible since it requires collecting the RUV from all masters and healthcheck is limited to only talking to itself. """ requires = ('dirsrv',) def get_ruv(self, dn): """Identify the RUV for a suffix on this master""" try: entry = self.conn.get_entry(dn) except Exception: return None else: return entry.single_value.get('nsDS5ReplicaID') @duration def check(self): ruv = self.get_ruv(DN(('cn', 'replica'), ('cn', api.env.basedn), ('cn', 'mapping tree'), ('cn', 'config'))) csruv = self.get_ruv(DN(('cn', 'replica'), ('cn', 'o=ipaca'), ('cn', 'mapping tree'), ('cn', 'config'))) if ruv is not None: yield Result(self, constants.SUCCESS, key=str(api.env.basedn), ruv=ruv) if csruv is not None: yield Result(self, constants.SUCCESS, key='o=ipaca', ruv=csruv) @registry class KnownRUVCheck(DSPlugin): """Return all known RUVs. This can be used to identify "dangling" RUVs, or left-overs from previous replication agreements. """ requires = ('dirsrv',) def get_all_ruvs(self, suffix): """Get all known RUVs on this master Return the RUV entries as a list of tuples: (hostname, rid) """ search_filter = '(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)' \ '(objectclass=nstombstone))' try: entries = self.conn.get_entries( suffix, self.conn.SCOPE_SUBTREE, search_filter, ['nsds50ruv']) except errors.NotFound: logger.debug("No RUV records found.") return [] # raise NoRUVsFound("No RUV records found.") servers = [] for e in entries: for ruv in e['nsds50ruv']: if ruv.startswith('{replicageneration'): continue data = re.match( r'\{replica (\d+) (ldap://.*:\d+)\}(\s+\w+\s+\w*){0,1}', ruv ) if data: rid = data.group(1) ( _scheme, netloc, _path, _params, _query, _fragment ) = urlparse(data.group(2)) servers.append((re.sub(r':\d+', '', netloc), rid)) else: logger.debug("Unable to decode RUV: %s", ruv) return servers @duration def check(self): ruvs = self.get_all_ruvs(api.env.basedn) csruvs = self.get_all_ruvs(DN('o=ipaca')) if ruvs: yield Result(self, constants.SUCCESS, key='ruvs_' + str(api.env.basedn), suffix=str(api.env.basedn), ruvs=ruvs) if csruvs: yield Result(self, constants.SUCCESS, key='ruvs_o=ipaca', suffix='o=ipaca', ruvs=csruvs) freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/000077500000000000000000000000001420053437700216105ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/__init__.py000066400000000000000000000000001420053437700237070ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/certs.py000066400000000000000000001443131420053437700233100ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from __future__ import division from datetime import datetime, timezone, timedelta import itertools import logging import os import socket import tempfile from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipahealthcheck.core.plugin import Result, generalized_time from ipahealthcheck.core.plugin import duration from ipahealthcheck.core import constants from ipalib import api from ipalib import errors from ipalib import x509 from ipalib.install import certmonger from ipalib.constants import RENEWAL_CA_NAME, IPA_CA_RECORD from ipaplatform.paths import paths from ipaserver.install import certs from ipaserver.install import dsinstance from ipaserver.install import krainstance from ipaserver.install import krbinstance from ipaserver.plugins import ldap2 from ipapython import certdb from ipapython import ipautil from ipapython.dn import DN from ipapython.ipaldap import realm_to_serverid logger = logging.getLogger() DAY = 60 * 60 * 24 def is_ipa_issued_cert(myapi, cert): """Thin wrapper around certs.is_ipa_issued to test for LDAP""" if not myapi.Backend.ldap2.isconnected(): return None return certs.is_ipa_issued_cert(myapi, cert) def get_expected_requests(ca, ds, serverid): """Provide the expected certmonger tracking request data This list is based in part on certificate_renewal_update() in ipaserver/install/server/upgrade.py and various start_tracking_certificates() methods in *instance.py. The list is filtered depending on whether a CA is running and the certificates have been issued by IPA. :param ca: the CAInstance :param ds: the DSInstance :param serverid: the DS serverid name """ template = paths.CERTMONGER_COMMAND_TEMPLATE if api.Command.ca_is_enabled()['result']: requests = [ { 'cert-file': paths.RA_AGENT_PEM, 'key-file': paths.RA_AGENT_KEY, 'ca-name': RENEWAL_CA_NAME, 'cert-presave-command': template % 'renew_ra_cert_pre', 'cert-postsave-command': template % 'renew_ra_cert', }, ] else: requests = [] if ca.is_configured(): dogtag_reqs = ca.tracking_reqs.items() kra = krainstance.KRAInstance(api.env.realm) if kra.is_installed(): dogtag_reqs = itertools.chain(dogtag_reqs, kra.tracking_reqs.items()) for nick, profile in dogtag_reqs: req = { 'cert-database': paths.PKI_TOMCAT_ALIAS_DIR, 'cert-nickname': nick, 'ca-name': RENEWAL_CA_NAME, 'cert-presave-command': template % 'stop_pkicad', 'cert-postsave-command': (template % 'renew_ca_cert "{}"'.format(nick)), 'template-profile': profile, } requests.append(req) else: logger.debug('CA is not configured, skipping CA tracking') cert = x509.load_certificate_from_file(paths.HTTPD_CERT_FILE) issued = is_ipa_issued_cert(api, cert) if issued is None: logger.debug('Unable to determine if \'%s\' was issued by IPA ' 'because no LDAP connection, assuming yes.') if issued or issued is None: requests.append( { 'cert-file': paths.HTTPD_CERT_FILE, 'key-file': paths.HTTPD_KEY_FILE, 'ca-name': 'IPA', 'cert-postsave-command': template % 'restart_httpd', } ) else: logger.debug('HTTP cert not issued by IPA, \'%s\', skip tracking ' 'check', DN(cert.issuer)) # Check the ldap server cert if issued by IPA ds_nickname = ds.get_server_cert_nickname(serverid) ds_db_dirname = dsinstance.config_dirname(serverid) ds_db = certs.CertDB(api.env.realm, nssdir=ds_db_dirname) connected = api.Backend.ldap2.isconnected() if not connected: logger.debug('Unable to determine if \'%s\' was issued by IPA ' 'because no LDAP connection, assuming yes.') if not connected or ds_db.is_ipa_issued_cert(api, ds_nickname): requests.append( { 'cert-database': ds_db_dirname[:-1], 'cert-nickname': ds_nickname, 'ca-name': 'IPA', 'cert-postsave-command': '%s %s' % (template % 'restart_dirsrv', serverid), } ) else: logger.debug('DS cert is not issued by IPA, skip tracking check') # Check if pkinit is enabled if os.path.exists(paths.KDC_CERT): pkinit_request_ca = krbinstance.get_pkinit_request_ca() requests.append( { 'cert-file': paths.KDC_CERT, 'key-file': paths.KDC_KEY, 'ca-name': pkinit_request_ca, 'cert-postsave-command': template % 'renew_kdc_cert', } ) else: logger.debug('No KDC pkinit certificate') # See if a host certificate was issued. This is only to # prevent a false-positive if one is indeed installed. local = { paths.IPA_NSSDB_DIR: 'Local IPA host', paths.NSS_DB_DIR: 'IPA Machine Certificate - %s' % socket.getfqdn(), } for db, nickname in local.items(): nssdb = certdb.NSSDatabase(db) if nssdb.has_nickname(nickname): requests.append( { 'cert-database': db, 'cert-nickname': nickname, 'ca-name': 'IPA', } ) return requests def get_dogtag_cert_password(): """Return the NSSDB token password Will raise IOError if there is a problem reading the file. """ ca_passwd = None token = 'internal' with open(paths.PKI_TOMCAT_PASSWORD_CONF, 'r') as f: for line in f: (tok, pin) = line.split('=', 1) if token == tok: ca_passwd = pin.strip() break return ca_passwd @registry class IPACertmongerExpirationCheck(IPAPlugin): """ Collect the known/tracked certificates and check the validity This verifies only the information that certmonger has and uses to schedule renewal. This is to ensure something hasn't changed certmonger's view of the world. """ @duration def check(self): cm = certmonger._certmonger() all_requests = cm.obj_if.get_requests() for req in all_requests: request = certmonger._cm_dbus_object(cm.bus, cm, req, certmonger.DBUS_CM_REQUEST_IF, certmonger.DBUS_CM_IF, True) id = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF, 'nickname') notafter = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF, 'not-valid-after') if notafter == 0: yield Result(self, constants.ERROR, key=id, msg='certmonger request id {key} does not have ' 'a not-valid-after date, assuming it ' 'has not been issued yet.') continue nafter = datetime.fromtimestamp(notafter, timezone.utc) now = datetime.now(timezone.utc) if now > nafter: yield Result(self, constants.ERROR, key=id, expiration_date=generalized_time(nafter), msg='Request id {key} expired on ' '{expiration_date}') else: delta = nafter - now diff = int(delta.total_seconds() / DAY) if diff < int(self.config.cert_expiration_days): yield Result(self, constants.WARNING, key=id, expiration_date=generalized_time(nafter), days=diff, msg='Request id {key} expires in {days} ' 'days. certmonger should renew this ' 'automatically. Watch the status with ' 'getcert list -i {key}.') else: yield Result(self, constants.SUCCESS, key=id) @registry class IPACertfileExpirationCheck(IPAPlugin): """ Collect the known/tracked certificates and check file validity Look into the certificate file or NSS database to check the validity of the on-disk certificate. This is to ensure a certificate wasn't replaced without certmonger being notified. """ @duration def check(self): cm = certmonger._certmonger() all_requests = cm.obj_if.get_requests() for req in all_requests: request = certmonger._cm_dbus_object(cm.bus, cm, req, certmonger.DBUS_CM_REQUEST_IF, certmonger.DBUS_CM_IF, True) id = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF, 'nickname') store = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF, 'cert-storage') if store == 'FILE': certfile = str(request.prop_if.Get( certmonger.DBUS_CM_REQUEST_IF, 'cert-file')) try: cert = x509.load_certificate_from_file(certfile) except Exception as e: yield Result(self, constants.ERROR, key=id, certfile=certfile, error=str(e), msg='Request id {key}: Unable to open cert ' 'file \'{certfile}\': {error}') continue elif store == 'NSSDB': nickname = str(request.prop_if.Get( certmonger.DBUS_CM_REQUEST_IF, 'key_nickname')) dbdir = str(request.prop_if.Get( certmonger.DBUS_CM_REQUEST_IF, 'cert_database')) try: db = certdb.NSSDatabase(dbdir) except Exception as e: yield Result(self, constants.ERROR, key=id, dbdir=dbdir, error=str(e), msg='Request id {key}: Unable to open NSS ' 'database \'{dbdir}\': {error}') continue try: cert = db.get_cert(nickname) except Exception as e: yield Result(self, constants.ERROR, key=id, dbdir=dbdir, nickname=nickname, error=str(e), msg='Request id {key}: Unable to retrieve ' 'cert \'{nickname}\' from \'{dbdir}\': ' '{error}') continue else: yield Result(self, constants.ERROR, key=id, store=store, msg='Request id {key}: Unknown certmonger ' 'storage type: {store}') continue now = datetime.utcnow() notafter = cert.not_valid_after if now > notafter: yield Result(self, constants.ERROR, key=id, expiration_date=generalized_time(notafter), msg='Request id {key} expired on ' '{expiration_date}') continue delta = notafter - now diff = int(delta.total_seconds() / DAY) if diff < int(self.config.cert_expiration_days): yield Result(self, constants.WARNING, key=id, expiration_date=generalized_time(notafter), days=diff, msg='Request id {key} expires in {days} ' 'days. certmonger should renew this ' 'automatically. Watch the status with' 'getcert list -i {key}.') else: yield Result(self, constants.SUCCESS, key=id) @registry class IPACertTracking(IPAPlugin): """Compare the certificates tracked by certmonger to those that are configured by default. Steps: 1. Collect all expected certificates into `requests` 2. Get the ids of all the certificates that certmonger is tracking 3. Iterate over `requests` to retrieve the request id of the expected tracking. 4. If the id is found we remove it from the ids list and move on 5. In the unlikely event that the request_id is not in the ids list of all tracked certs report it. 6. Report on all tracked certs that IPA didn't setup itself as potential issues. """ requires = ('dirsrv',) @duration def check(self): requests = get_expected_requests(self.ca, self.ds, self.serverid) cm = certmonger._certmonger() ids = [] all_requests = cm.obj_if.get_requests() for req in all_requests: request = certmonger._cm_dbus_object(cm.bus, cm, req, certmonger.DBUS_CM_REQUEST_IF, certmonger.DBUS_CM_IF, True) id = request.prop_if.Get(certmonger.DBUS_CM_REQUEST_IF, 'nickname') ids.append(str(id)) for request in requests: request_id = certmonger.get_request_id(request) try: if request_id is not None: # Tracking found, move onto the next ids.remove(request_id) yield Result(self, constants.SUCCESS, key=request_id) continue except ValueError as e: # A request was found but the id isn't in the # list from certmonger!? yield Result(self, constants.ERROR, key=request_id, error=str(e), msg='Found request id {key} but it is not tracked' ' by certmonger!?: {error}') continue # The criteria was not met if request_id is None: flatten = ', '.join("{!s}={!s}".format(key, val) for (key, val) in request.items()) yield Result(self, constants.ERROR, key=flatten, msg='Expected certmonger tracking is missing for ' '{key}. Automated renewal will not happen ' 'for this certificate') continue # Report any unknown certmonger requests as warnings if ids: for id in ids: yield Result(self, constants.WARNING, key=id, msg='certmonger tracking request {key} found and ' 'is not expected on an IPA master.') @registry class IPACertDNSSAN(IPAPlugin): """Check whether a IPA-issued certificates have a SAN configured Steps: 1. Collect all expected certificates into `requests` 2. Iterate over the list of certificates 3. If issued by IPA and a caIPAserviceCert then verify that the host FQDN is in the list of SAN 4. If a CA is configured on this host then also verify that ipa-ca.$DOMAIN is in the SAN. """ requires = ('dirsrv',) @duration def check(self): fqdn = socket.getfqdn() requests = get_expected_requests(self.ca, self.ds, self.serverid) for request in requests: request_id = certmonger.get_request_id(request) if request_id is None: # log and skip. Missed tracking is reported by IPACertTracking flatten = ', '.join("{!s}={!s}".format(key, val) for (key, val) in request.items()) logger.debug( "Skipping %s since it is handled by IPACertTracking", flatten ) continue ca_name = certmonger.get_request_value(request_id, 'ca-name') if ca_name != 'IPA': logger.debug('Skipping request %s with CA %s', request_id, ca_name) continue profile = certmonger.get_request_value(request_id, 'template_profile') if profile != 'caIPAserviceCert': logger.debug('Skipping request %s with profile %s', request_id, profile) continue certfile = None if request.get('cert-file') is not None: certfile = request.get('cert-file') try: cert = x509.load_certificate_from_file(certfile) except Exception as e: yield Result(self, constants.ERROR, key=request_id, certfile=certfile, error=str(e), msg='Unable to open cert file {certfile}: ' '{error}') continue elif request.get('cert-database') is not None: nickname = request.get('cert-nickname') dbdir = request.get('cert-database') try: db = certdb.NSSDatabase(dbdir) except Exception as e: yield Result(self, constants.ERROR, key=request_id, dbdir=dbdir, error=str(e), msg='Unable to open NSS database {dbdir}: ' '{error}') continue try: cert = db.get_cert(nickname) except Exception as e: yield Result(self, constants.ERROR, key=id, dbdir=dbdir, nickname=nickname, error=str(e), msg='Unable to retrieve certificate ' '\'{nickname}\' from {dbdir}: {error}') continue hostlist = [fqdn] if self.ca.is_configured() and certfile == paths.HTTPD_CERT_FILE: hostlist.append(f'{IPA_CA_RECORD}.{api.env.domain}') error = False for host in hostlist: if host not in cert.san_a_label_dns_names: error = True yield Result(self, constants.ERROR, key=request_id, hostname=host, san=cert.san_a_label_dns_names, ca=ca_name, profile=profile, msg='Certificate request id {key} with ' 'profile {profile} for CA {ca} does not ' 'have a DNS SAN {san} matching name ' '{hostname}') if not error: yield Result(self, constants.SUCCESS, key=request_id, hostname=hostlist, san=cert.san_a_label_dns_names, ca=ca_name, profile=profile) @registry class IPACertNSSTrust(IPAPlugin): """Compare the NSS trust for the CA certs to a known good value""" @duration def check(self): expected_trust = { 'ocspSigningCert cert-pki-ca': 'u,u,u', 'subsystemCert cert-pki-ca': 'u,u,u', 'auditSigningCert cert-pki-ca': 'u,u,Pu', 'Server-Cert cert-pki-ca': 'u,u,u', } kra = krainstance.KRAInstance(api.env.realm) if kra.is_installed(): kra_trust = { 'transportCert cert-pki-kra': 'u,u,u', 'storageCert cert-pki-kra': 'u,u,u', 'auditSigningCert cert-pki-kra': 'u,u,Pu', } expected_trust.update(kra_trust) if not self.ca.is_configured(): logger.debug('CA is not configured, skipping NSS trust check') return db = certs.CertDB(api.env.realm, paths.PKI_TOMCAT_ALIAS_DIR) for nickname, _trust_flags in db.list_certs(): flags = certdb.unparse_trust_flags(_trust_flags) if nickname.startswith('caSigningCert cert-pki-ca'): expected = 'CTu,Cu,Cu' else: try: expected = expected_trust[nickname] except KeyError: logger.debug( "%s not found in %s, assuming 3rd party", nickname, paths.PKI_TOMCAT_ALIAS_DIR, ) continue try: expected_trust.pop(nickname) except KeyError: pass if flags != expected: yield Result( self, constants.ERROR, key=nickname, expected=expected, got=flags, nickname=nickname, dbdir=paths.PKI_TOMCAT_ALIAS_DIR, msg='Incorrect NSS trust for {nickname} in {dbdir}. ' 'Got {got} expected {expected}.') continue yield Result(self, constants.SUCCESS, key=nickname) for nickname in expected_trust: yield Result( self, constants.ERROR, key=nickname, nickname=nickname, dbdir=paths.PKI_TOMCAT_ALIAS_DIR, msg='Certificate {nickname} missing from {dbdir} while ' 'verifying trust') @registry class IPACertMatchCheck(IPAPlugin): """ Ensure certificates match between LDAP and NSS databases """ requires = ('dirsrv',) def get_cert_list_from_db(self, nssdb, nickname): """ Retrieve all certificates from an NSS database for nickname. """ try: args = ["-L", "-n", nickname, "-a"] result = nssdb.run_certutil(args, capture_output=True) return x509.load_certificate_list(result.raw_output) except ipautil.CalledProcessError: return [] @duration def check(self): if not self.ca.is_configured(): logger.debug("No CA configured, skipping certificate match check") return # Ensure /etc/ipa/ca.crt matches the NSS DB CA certificates def match_cacert_and_db(plugin, cacerts, dbpath): db = certs.CertDB(api.env.realm, dbpath) nickname = '%s IPA CA' % api.env.realm try: dbcacerts = self.get_cert_list_from_db(db, nickname) except Exception as e: yield Result(plugin, constants.ERROR, key=nickname, error=str(e), msg='Unable to load CA cert: {error}') return False ok = True for cert in dbcacerts: if cert not in cacerts: ok = False yield Result(plugin, constants.ERROR, key=nickname, nickname=nickname, serial_number=cert.serial_number, dbdir=dbpath, certdir=paths.IPA_CA_CRT, msg=('CA Certificate nickname {nickname} ' 'with serial number {serial} ' 'is in {dbdir} but is not in' '%s' % paths.IPA_CA_CRT)) return ok try: cacerts = x509.load_certificate_list_from_file(paths.IPA_CA_CRT) except Exception: yield Result(self, constants.ERROR, key=paths.IPA_CA_CRT.replace(os.path.sep, '_'), path=paths.IPA_CA_CRT, msg='Unable to load CA cert file {path}: {error}') return # Ensure CA cert entry from LDAP matches /etc/ipa/ca.crt dn = DN('cn=%s IPA CA' % api.env.realm, 'cn=certificates,cn=ipa,cn=etc', api.env.basedn) try: entry = self.conn.get_entry(dn) except errors.NotFound: yield Result(self, constants.ERROR, key=str(dn), dn=str(dn), msg='CA Certificate entry \'{dn}\' ' 'not found in LDAP') return cacerts_ok = True # Are all the certs in LDAP for the IPA CA in /etc/ipa/ca.crt for cert in entry['CACertificate']: if cert not in cacerts: cacerts_ok = False yield Result(self, constants.ERROR, key=str(dn), dn=str(dn), serial_number=cert.serial_number, msg=('CA Certificate serial number {serial} is ' 'in LDAP \'{dn}\' but is not in ' '%s' % paths.IPA_CA_CRT)) # Ensure NSS DBs have matching CA certs for /etc/ipa/ca.crt serverid = realm_to_serverid(api.env.realm) dspath = paths.ETC_DIRSRV_SLAPD_INSTANCE_TEMPLATE % serverid cacertds_ok = yield from match_cacert_and_db(self, cacerts, dspath) cacertnss_ok = yield from match_cacert_and_db(self, cacerts, paths.IPA_NSSDB_DIR) if cacerts_ok: yield Result(self, constants.SUCCESS, key=paths.IPA_CA_CRT) if cacertds_ok: yield Result(self, constants.SUCCESS, key=dspath) if cacertnss_ok: yield Result(self, constants.SUCCESS, key=paths.IPA_NSSDB_DIR) @registry class IPADogtagCertsMatchCheck(IPAPlugin): """ Check if dogtag certs present in both NSS DB and LDAP match """ requires = ('dirsrv',) @duration def check(self): if not self.ca.is_configured(): logger.debug('CA is not configured, skipping connectivity check') return def match_ldap_nss_cert(plugin, ldap, db, cert_dn, attr, cert_nick): try: entry = ldap.get_entry(cert_dn) except errors.NotFound: yield Result(plugin, constants.ERROR, key=cert_dn, msg='%s entry not found in LDAP' % cert_dn) return False try: nsscert = db.get_cert_from_db(cert_nick) except Exception as e: yield Result(plugin, constants.ERROR, key=cert_nick, error=str(e), msg=('Unable to load %s certificate:' '{error}' % cert_nick)) return False cert_matched = any(cert == nsscert for cert in entry[attr]) if not cert_matched: yield Result(plugin, constants.ERROR, key=cert_nick, nickname=cert_nick, dbdir=db.secdir, msg=('{nickname} certificate in NSS DB {dbdir} ' 'does not match entry in LDAP')) return False return True def match_ldap_nss_certs_by_subject(plugin, ldap, db, dn, expected_nicks_subjects): entries = ldap.get_entries(dn) all_ok = True for nick, subject in expected_nicks_subjects.items(): cert = db.get_cert_from_db(nick) ok = any( cert in entry["userCertificate"] and subject == entry["subjectName"][0] for entry in entries if "userCertificate" in entry ) if not ok: all_ok = False yield Result(plugin, constants.ERROR, key=nick, nickname=nick, dbdir=db.secdir, msg=('{nickname} certificate in NSS DB ' '{dbdir} does not match entry in LDAP')) return all_ok db = certs.CertDB(api.env.realm, paths.PKI_TOMCAT_ALIAS_DIR) dn = DN('uid=pkidbuser,ou=people,o=ipaca') subsystem_nick = 'subsystemCert cert-pki-ca' subsystem_ok = yield from match_ldap_nss_cert(self, self.conn, db, dn, 'userCertificate', subsystem_nick) dn = DN('cn=%s IPA CA' % api.env.realm, 'cn=certificates,cn=ipa,cn=etc', api.env.basedn) casigning_nick = 'caSigningCert cert-pki-ca' casigning_ok = yield from match_ldap_nss_cert(self, self.conn, db, dn, 'CACertificate', casigning_nick) expected_nicks_subjects = { 'ocspSigningCert cert-pki-ca': 'CN=OCSP Subsystem,O=%s' % api.env.realm, 'subsystemCert cert-pki-ca': 'CN=CA Subsystem,O=%s' % api.env.realm, 'auditSigningCert cert-pki-ca': 'CN=CA Audit,O=%s' % api.env.realm, 'Server-Cert cert-pki-ca': 'CN=%s,O=%s' % (api.env.host, api.env.realm), } kra = krainstance.KRAInstance(api.env.realm) if kra.is_installed(): kra_expected_nicks_subjects = { 'transportCert cert-pki-kra': 'CN=KRA Transport Certificate,O=%s' % api.env.realm, 'storageCert cert-pki-kra': 'CN=KRA Storage Certificate,O=%s' % api.env.realm, 'auditSigningCert cert-pki-kra': 'CN=KRA Audit,O=%s' % api.env.realm, } expected_nicks_subjects.update(kra_expected_nicks_subjects) ipaca_basedn = DN('ou=certificateRepository,ou=ca,o=ipaca') ipaca_certs_ok = yield from match_ldap_nss_certs_by_subject( self, self.conn, db, ipaca_basedn, expected_nicks_subjects ) if subsystem_ok: yield Result(self, constants.SUCCESS, key=subsystem_nick) if casigning_ok: yield Result(self, constants.SUCCESS, key=casigning_nick) if ipaca_certs_ok: yield Result(self, constants.SUCCESS, key=str(ipaca_basedn)) @registry class IPANSSChainValidation(IPAPlugin): """Validate the certificate chain of the certs.""" def validate_nss(self, dbdir, dbtype, pinfile, nickname): """Call out to certutil to verify a certificate. The caller must handle the exceptions """ args = [paths.CERTUTIL, '-V', '-u', 'V', '-e'] args.extend(['-d', dbtype + ':' + dbdir]) args.extend(['-n', nickname]) args.extend(['-f', pinfile]) return ipautil.run(args, raiseonerr=False) @duration def check(self): validate = [] ca_pw_fname = None if self.ca.is_configured(): try: ca_passwd = get_dogtag_cert_password() except IOError as e: yield Result( self, constants.ERROR, key='db_authenticate', error=str(e), msg='Unable to read CA NSSDB token password: {error}') return else: with tempfile.NamedTemporaryFile(mode='w', delete=False) as ca_pw_file: ca_pw_file.write(ca_passwd) ca_pw_fname = ca_pw_file.name validate.append( ( paths.PKI_TOMCAT_ALIAS_DIR, 'Server-Cert cert-pki-ca', ca_pw_fname, ), ) validate.append( ( dsinstance.config_dirname(self.serverid), self.ds.get_server_cert_nickname(self.serverid), os.path.join(dsinstance.config_dirname(self.serverid), 'pwdfile.txt'), ) ) # Wrap in try/except to ensure the temporary password file is # removed try: for (dbdir, nickname, pinfile) in validate: # detect the database type so we have the right prefix db = certdb.NSSDatabase(dbdir) key = os.path.normpath(dbdir) + ':' + nickname try: response = self.validate_nss(dbdir, db.dbtype, pinfile, nickname) except ipautil.CalledProcessError as e: logger.debug('Validation of NSS certificate failed %s', e) yield Result( self, constants.ERROR, key=key, dbdir=dbdir, nickname=nickname, reason=response.output_error, msg='Validation of {nickname} in {dbdir} failed: ' '{reason}') else: if 'certificate is valid' not in \ response.raw_output.decode('utf-8'): yield Result( self, constants.ERROR, key=key, dbdir=dbdir, nickname=nickname, reason="%s: %s" % (response.raw_output.decode('utf-8'), response.error_log), msg='Validation of {nickname} in {dbdir} failed: ' '{reason}') else: yield Result(self, constants.SUCCESS, dbdir=dbdir, nickname=nickname, key=key) finally: if ca_pw_fname: ipautil.remove_file(ca_pw_fname) @registry class IPAOpenSSLChainValidation(IPAPlugin): """Validate the certificate chain of the certs.""" def validate_openssl(self, file): """Call out to openssl to verify a certificate against global chain The caller must handle the exceptions """ args = [paths.OPENSSL, 'verify', '-verbose', '-show_chain', '-CAfile', paths.IPA_CA_CRT, file] return ipautil.run(args, raiseonerr=False) @duration def check(self): certlist = [paths.HTTPD_CERT_FILE] if self.ca.is_configured(): certlist.append(paths.RA_AGENT_PEM) for cert in certlist: try: response = self.validate_openssl(cert) except Exception as e: yield Result( self, constants.ERROR, key=cert, error=str(e), msg='Certificate validation for {key} failed: {error}') continue else: if ': OK' not in response.raw_output.decode('utf-8'): yield Result( self, constants.ERROR, key=cert, reason=response.raw_error_output.decode('utf-8'), msg='Certificate validation for {key} failed: ' '{reason}') else: yield Result( self, constants.SUCCESS, key=cert) def check_agent(plugin, base_dn, agent_type): """Check RA/KRA Agent""" try: cert = x509.load_certificate_from_file(paths.RA_AGENT_PEM) except Exception as e: yield Result(plugin, constants.ERROR, key=paths.RA_AGENT_PEM.replace(os.path.sep, '_'), error=str(e), msg='Unable to load RA cert: {error}') return serial_number = cert.serial_number subject = DN(cert.subject) issuer = DN(cert.issuer) description = '2;%d;%s;%s' % (serial_number, issuer, subject) logger.debug('%s agent description should be %s', agent_type, description) db_filter = ldap2.ldap2.combine_filters( [ ldap2.ldap2.make_filter({'objectClass': 'inetOrgPerson'}), ldap2.ldap2.make_filter( {'description': ';%s;%s' % (issuer, subject)}, exact=False, trailing_wildcard=False), ], ldap2.ldap2.MATCH_ALL) try: entries = plugin.conn.get_entries(base_dn, plugin.conn.SCOPE_SUBTREE, db_filter) except errors.NotFound: yield Result(plugin, constants.ERROR, key=agent_type, description=description, msg='%s agent not found in LDAP' % agent_type) return except Exception as e: yield Result(plugin, constants.ERROR, key=agent_type, error=str(e), msg='Retrieving %s agent from LDAP failed {error}' % agent_type) return else: logger.debug('%s agent description is %s', agent_type, description) if len(entries) != 1: yield Result(plugin, constants.ERROR, key='too_many_agents', found=len(entries), msg='Too many %s agent entries found, {found}' % agent_type) return entry = entries[0] raw_desc = entry.get('description') if raw_desc is None: yield Result(plugin, constants.ERROR, key='agent_missing_description', msg='%s agent is missing the description ' 'attribute or it is not readable' % agent_type) return ra_desc = raw_desc[0] ra_certs = entry.get('usercertificate') if ra_desc != description: yield Result(plugin, constants.ERROR, key='description_mismatch', expected=description, got=ra_desc, msg='%s agent description does not match. Found ' '{got} in LDAP and expected {expected}' % agent_type) return found = False for candidate in ra_certs: if candidate == cert: found = True break if not found: yield Result(plugin, constants.ERROR, key='ldap_mismatch', certfile=paths.RA_AGENT_PEM, dn=str(entry.dn), msg='%s agent certificate in {certfile} not ' 'found in LDAP userCertificate attribute ' 'for the entry {dn}' % agent_type) yield Result(plugin, constants.SUCCESS) @registry class IPARAAgent(IPAPlugin): """Validate the RA Agent used to talk to the CA Compare the description and usercertificate values. """ requires = ('dirsrv',) @duration def check(self): if not self.ca.is_configured(): logger.debug('CA is not configured, skipping RA Agent check') return base_dn = DN('uid=ipara,ou=people,o=ipaca') yield from check_agent(self, base_dn, 'RA') @registry class IPAKRAAgent(IPAPlugin): """Validate the KRA Agent Compare the description and usercertificate values. """ requires = ('dirsrv',) @duration def check(self): if not self.ca.is_configured(): logger.debug('CA is not configured, skipping KRA Agent check') return kra = krainstance.KRAInstance(api.env.realm) if not kra.is_installed(): logger.debug('KRA is not installed, skipping KRA Agent check') return base_dn = DN('uid=ipakra,ou=people,o=kra,o=ipaca') yield from check_agent(self, base_dn, 'KRA') @registry class IPACertRevocation(IPAPlugin): """Confirm that the IPA certificates are not revoked This uses the certmonger expected tracking list to know which one(s) to consider. """ revocation_reason = [ "unspecified", "key compromise", "CA compromise", "affiliation changed", "superseded", "cessation of operation", "certificate hold", "", # unused "remove from CRL", "privilege withdrawn", "AA compromise", ] requires = ('dirsrv',) @duration def check(self): # For simplicity use the expected certmonger tracking for the # list of certificates to check because it already filters out # based on whether the CA system is configure and whether the # certificates were issued by IPA. if not self.ca.is_configured(): logger.debug('CA is not configured, skipping revocation check') return requests = get_expected_requests(self.ca, self.ds, self.serverid) for request in requests: id = certmonger.get_request_id(request) if request.get('cert-file') is not None: certfile = request.get('cert-file') try: cert = x509.load_certificate_from_file(certfile) except Exception as e: yield Result(self, constants.ERROR, key=id, certfile=certfile, error=str(e), msg='Unable to open cert file {certfile}: ' '{error}') continue elif request.get('cert-database') is not None: nickname = request.get('cert-nickname') dbdir = request.get('cert-database') try: db = certdb.NSSDatabase(dbdir) except Exception as e: yield Result(self, constants.ERROR, key=id, dbdir=dbdir, error=str(e), msg='Unable to open NSS database {dbdir}: ' '{error}') continue try: cert = db.get_cert(nickname) except Exception as e: yield Result(self, constants.ERROR, key=id, dbdir=dbdir, nickname=nickname, error=str(e), msg='Unable to retrieve certificate ' '\'{nickname}\' from {dbdir}: {error}') continue else: yield Result(self, constants.ERROR, key=id, msg='Unable to to identify certificate storage ' 'type for request {key}') continue issued = is_ipa_issued_cert(api, cert) if issued is False: logger.debug('\'%s\' was not issued by IPA, skipping', DN(cert.subject)) continue if issued is None: logger.debug('LDAP is down, skipping \'%s\'', DN(cert.subject)) continue # Now we have the cert either way, check the recovation try: result = api.Command.cert_show(cert.serial_number, all=True) except Exception as e: yield Result(self, constants.ERROR, key=id, serial=cert.serial_number, error=str(e), msg='Request for certificate serial number ' '{serial} in request {key} failed: {error}') continue try: if result['result']['revoked']: reason = result['result']['revocation_reason'] reason_txt = self.revocation_reason[reason] yield Result(self, constants.ERROR, revocation_reason=reason_txt, key=id, msg='Certificate tracked by {key} is revoked ' '{revocation_reason}') else: yield Result(self, constants.SUCCESS, key=id) except Exception as e: yield Result(self, constants.ERROR, key=id, error=str(e), msg='Unable to determine revocation ' 'status for {key}: {error}') @registry class IPACertmongerCA(IPAPlugin): """Ensure that the required CAs are available in certmonger Addresses symptom of https://pagure.io/freeipa/issue/7870 """ def find_ca(self, name): cm = certmonger._certmonger() ca_path = cm.obj_if.find_ca_by_nickname(name) return certmonger._cm_dbus_object(cm.bus, cm, ca_path, certmonger.DBUS_CM_CA_IF, certmonger.DBUS_CM_IF, True) @duration def check(self): ca_list = ['IPA'] if self.ca.is_configured(): ca_list.extend([ 'dogtag-ipa-ca-renew-agent', 'dogtag-ipa-ca-renew-agent-reuse' ]) for ca in ca_list: logger.debug('Checking for existence of certmonger CA \'%s\'', ca) try: self.find_ca(ca) except Exception as e: logger.debug('Search for certmonger CA %s failed: %s', ca, e) yield Result(self, constants.ERROR, key=ca, msg='Certmonger CA \'{key}\' missing') else: yield Result(self, constants.SUCCESS, key=ca) @registry class IPACAChainExpirationCheck(IPAPlugin): """Verify that the certs in the CA chain in /etc/ipa/ca.crt are valid """ @duration def check(self): try: ca_certs = x509.load_certificate_list_from_file(paths.IPA_CA_CRT) except IOError as e: logger.debug("Could not open %s: %s", paths.IPA_CA_CRT, e) yield Result(self, constants.ERROR, key=paths.IPA_CA_CRT, error=str(e), msg='Error opening IPA CA chain at {key}: {error}') return except ValueError as e: logger.debug( "% contains an invalid certificate", paths.IPA_CA_CRT ) yield Result(self, constants.ERROR, key=paths.IPA_CA_CRT, error=str(e), msg='IPA CA chain {key} contains an invalid ' 'certificate: {error}') return now = datetime.now(timezone.utc) soon = now + timedelta(days=int(self.config.cert_expiration_days)) for cert in ca_certs: subject = DN(cert.subject) subject = str(subject).replace('\\;', '\\3b') dt = cert.not_valid_after.replace(tzinfo=timezone.utc) if dt < now: logger.debug("%s is expired", subject) yield Result(self, constants.CRITICAL, path=paths.IPA_CA_CRT, key=subject, msg='CA \'{key}\' in {path} is expired.') elif dt <= soon: logger.debug("%s is expiring soon", subject) yield Result(self, constants.WARNING, path=paths.IPA_CA_CRT, key=subject, days=(dt - now).days, msg='CA \'{key}\' in {path} is expiring in ' '{days} days.') else: yield Result(self, constants.SUCCESS, path=paths.IPA_CA_CRT, key=subject, days=(dt - now).days) freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/dna.py000066400000000000000000000032351420053437700227270ustar00rootroot00000000000000 # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import logging from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.core import constants from ipalib import api from ipaserver.install import replication logger = logging.getLogger() @registry class IPADNARangeCheck(IPAPlugin): """ Report the configured DNA range, if any. This expects some external system to analyze and determine if any or all masters have a DNA range configured. It is not an error if a master does not have a range. It IS an error if no masters have a range. """ requires = ('dirsrv',) @duration def check(self): agmt = replication.ReplicationManager(api.env.realm, api.env.host) (range_start, range_max) = agmt.get_DNA_range(api.env.host) (next_start, next_max) = agmt.get_DNA_next_range(api.env.host) if range_start is not None: yield Result(self, constants.SUCCESS, range_start=range_start, range_max=range_max, next_start=next_start or 0, next_max=next_max or 0) else: yield Result(self, constants.WARNING, key='no_dna_range_defined', range_start=0, range_max=0, next_start=0, next_max=0, msg='No DNA range defined. If no masters define a ' 'range then users and groups cannot be ' 'created.') freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/files.py000066400000000000000000000150471420053437700232730ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import glob import logging import os from ipahealthcheck.core.files import FileCheck from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipalib import api, errors from ipaplatform.paths import paths from ipaplatform.constants import constants from ipapython.certdb import NSS_SQL_FILES from ipapython.dn import DN from ipaserver.install import dsinstance from ipaserver.install import krbinstance logger = logging.getLogger() @registry class IPAFileNSSDBCheck(IPAPlugin, FileCheck): def collect_files(self, basedir, filelist, owner, group, perms): for file in filelist: self.files.append((os.path.join(basedir, file), owner, group, perms)) def check(self): self.files = [] self.collect_files(dsinstance.config_dirname(self.serverid), NSS_SQL_FILES, 'dirsrv', 'root', '0640') # There always has to be a special one. pkcs11.txt has a different # group so pop off the auto-generated one and add a replacement. old = (os.path.join(dsinstance.config_dirname(self.serverid), 'pkcs11.txt'), 'dirsrv', 'root', '0640') self.files.remove(old) new = (os.path.join(dsinstance.config_dirname(self.serverid), 'pkcs11.txt'), 'dirsrv', 'dirsrv', '0640') self.files.append(new) if self.ca.is_configured(): self.collect_files(paths.PKI_TOMCAT_ALIAS_DIR, NSS_SQL_FILES, 'pkiuser', 'pkiuser', '0600') return FileCheck.check(self) @registry class IPAFileCheck(IPAPlugin, FileCheck): def dns_container_exists(self): try: self.conn.get_entry(DN(api.env.container_dns, api.env.basedn), []) except errors.NotFound: return False except AttributeError: logger.debug("LDAP is down, can't tell whether DNS is available." " Skipping those file checks.") return False return True def check(self): self.files = [] if self.ca.is_configured(): self.files.append((paths.RA_AGENT_PEM, 'root', 'ipaapi', '0440')) self.files.append((paths.RA_AGENT_KEY, 'root', 'ipaapi', '0440')) if krbinstance.is_pkinit_enabled(): self.files.append((paths.KDC_CERT, 'root', 'root', '0644')) self.files.append((paths.KDC_KEY, 'root', 'root', '0600')) if self.dns_container_exists(): self.files.append((paths.NAMED_KEYTAB, constants.NAMED_USER, constants.NAMED_GROUP, '0400')) if os.path.exists(paths.IPA_DNSKEYSYNCD_KEYTAB): self.files.append((paths.IPA_DNSKEYSYNCD_KEYTAB, 'root', constants.ODS_GROUP, '0440')) self.files.append((paths.GSSAPI_SESSION_KEY, 'root', 'root', '0600')) self.files.append((paths.DS_KEYTAB, constants.DS_USER, constants.DS_GROUP, '0600')) self.files.append((paths.IPA_CA_CRT, 'root', 'root', '0644')) self.files.append((paths.IPA_CUSTODIA_KEYS, 'root', 'root', '0600')) self.files.append((paths.RESOLV_CONF, ('root', 'systemd-resolve'), ('root', 'systemd-resolve'), '0644')) self.files.append((paths.HOSTS, 'root', 'root', '0644')) # IPA log files that may vary by installation. Only verify # those that exist for filename in ( paths.IPABACKUP_LOG, paths.IPARESTORE_LOG, paths.IPACLIENT_INSTALL_LOG, paths.IPACLIENT_UNINSTALL_LOG, paths.IPAREPLICA_CA_INSTALL_LOG, paths.IPAREPLICA_CONNCHECK_LOG, paths.IPAREPLICA_INSTALL_LOG, paths.IPASERVER_INSTALL_LOG, paths.IPASERVER_KRA_INSTALL_LOG, paths.IPASERVER_UNINSTALL_LOG, paths.IPAUPGRADE_LOG, paths.IPATRUSTENABLEAGENT_LOG, ): if os.path.exists(filename): self.files.append((filename, 'root', 'root', '0600')) self.files.append((paths.IPA_CUSTODIA_AUDIT_LOG, 'root', 'root', '0644')) self.files.append((paths.KADMIND_LOG, 'root', 'root', '0600')) self.files.append((paths.KRB5KDC_LOG, 'root', 'root', '0640')) inst = api.env.realm.replace('.', '-') self.files.append((paths.SLAPD_INSTANCE_ACCESS_LOG_TEMPLATE % inst, 'dirsrv', 'dirsrv', '0600')) self.files.append((paths.SLAPD_INSTANCE_ERROR_LOG_TEMPLATE % inst, 'dirsrv', 'dirsrv', '0600')) self.files.append((paths.VAR_LOG_HTTPD_ERROR, 'root', 'root', '0644')) for globpath in glob.glob("%s/debug*.log" % paths.TOMCAT_CA_DIR): self.files.append((globpath, "pkiuser", "pkiuser", "0644")) for globpath in glob.glob( "%s/ca_audit*" % paths.TOMCAT_SIGNEDAUDIT_DIR ): self.files.append((globpath, 'pkiuser', 'pkiuser', '0640')) for filename in ('selftests.log', 'system', 'transactions'): self.files.append(( os.path.join(paths.TOMCAT_CA_DIR, filename), 'pkiuser', 'pkiuser', '0640' )) for globpath in glob.glob("%s/debug*.log" % paths.TOMCAT_KRA_DIR): self.files.append((globpath, "pkiuser", "pkiuser", "0644")) for globpath in glob.glob( "%s/ca_audit*" % paths.TOMCAT_KRA_SIGNEDAUDIT_DIR ): self.files.append((globpath, 'pkiuser', 'pkiuser', '0640')) for filename in ('selftests.log', 'system', 'transactions'): self.files.append(( os.path.join(paths.TOMCAT_KRA_DIR, filename), 'pkiuser', 'pkiuser', '0640' )) return FileCheck.check(self) @registry class TomcatFileCheck(IPAPlugin, FileCheck): def check(self): if not self.ca.is_configured(): logger.debug('CA is not configured, skipping') self.files = [] else: self.files = [ (paths.PKI_TOMCAT_PASSWORD_CONF, constants.PKI_USER, constants.PKI_GROUP, '0660'), (paths.CA_CS_CFG_PATH, constants.PKI_USER, constants.PKI_GROUP, '0660'), (os.path.join(paths.PKI_TOMCAT, 'server.xml'), constants.PKI_USER, constants.PKI_GROUP, '0660'), ] return FileCheck.check(self) freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/host.py000066400000000000000000000022121420053437700231340ustar00rootroot00000000000000 # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import gssapi import logging import os import tempfile from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.core import constants from ipalib import api from ipalib.install.kinit import kinit_keytab from ipaplatform.paths import paths from ipapython import ipautil logger = logging.getLogger() @registry class IPAHostKeytab(IPAPlugin): """Ensure the host keytab can get a TGT""" requires = ('krb5kdc', 'dirsrv') @duration def check(self): ccache_dir = tempfile.mkdtemp() ccache_name = os.path.join(ccache_dir, 'ccache') try: try: host_princ = str('host/%s@%s' % (api.env.host, api.env.realm)) kinit_keytab(host_princ, paths.KRB5_KEYTAB, ccache_name) except gssapi.exceptions.GSSError as e: yield Result(self, constants.ERROR, msg='Failed to obtain host TGT: %s' % e) finally: ipautil.remove_file(ccache_name) os.rmdir(ccache_dir) freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/idns.py000066400000000000000000000223221420053437700231200ustar00rootroot00000000000000 # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from dns import rdatatype from dns.exception import DNSException import logging from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.core import constants from ipalib import api try: from dns.resolver import resolve except ImportError: from dns.resolver import query as resolve logger = logging.getLogger() def query_uri(uri): try: answers = resolve(uri, rdatatype.URI) except DNSException as e: logger.debug("DNS record not found: %s", e.__class__.__name__) answers = [] return answers @registry class IPADNSSystemRecordsCheck(IPAPlugin): """ Verify that the expected DNS service records are resolvable. IPA will already provide the values we need to validate with the IPASystemRecords class. We just need to pull that and do the equivalent DNS lookups. """ requires = ('dirsrv',) def srv_to_name(self, srv, target): """Combine the SRV record and target into a unique name.""" return srv + ":" + target def uri_to_name(self, uri, target): """Combine the SRV record and target into a unique name.""" return uri + ":" + target @duration def check(self): # pylint: disable=import-outside-toplevel from ipapython.dnsutil import query_srv from ipaserver.dns_data_management import IPASystemRecords # pylint: enable=import-outside-toplevel system_records = IPASystemRecords(api) base_records = system_records.get_base_records() # collect the list of expected values txt_rec = dict() srv_rec = dict() uri_rec = dict() a_rec = list() aaaa_rec = list() for name, node in base_records.items(): for rdataset in node: for rd in rdataset: if rd.rdtype == rdatatype.SRV: if name.ToASCII() in srv_rec: srv_rec[name.ToASCII()].append(rd.target.to_text()) else: srv_rec[name.ToASCII()] = [rd.target.to_text()] elif rd.rdtype == rdatatype.TXT: if name.ToASCII() in txt_rec: txt_rec[name.ToASCII()].append(rd.to_text()) else: txt_rec[name.ToASCII()] = [rd.to_text()] elif rd.rdtype == rdatatype.A: a_rec.append(rd.to_text()) elif rd.rdtype == rdatatype.AAAA: aaaa_rec.append(rd.to_text()) elif rd.rdtype == rdatatype.URI: if name.ToASCII() in uri_rec: uri_rec[name.ToASCII()].append( rd.target.decode('utf-8') ) else: uri_rec[name.ToASCII()] = [ rd.target.decode('utf-8') ] else: logger.error("Unhandled rdtype %d", rd.rdtype) # For each SRV record that IPA thinks it should have, do a DNS # lookup of it and ensure that DNS has the same set of values # that IPA thinks it should. for srv in srv_rec: logger.debug("Search DNS for SRV record of %s", srv) try: answers = query_srv(srv) except DNSException as e: logger.debug("DNS record not found: %s", e.__class__.__name__) answers = [] hosts = srv_rec[srv] for answer in answers: logger.debug("DNS record found: %s", answer) try: hosts.remove(answer.target.to_text()) yield Result( self, constants.SUCCESS, key=self.srv_to_name(srv, answer.target.to_text())) except ValueError: yield Result( self, constants.WARNING, msg='Unexpected SRV entry in DNS', key=self.srv_to_name(srv, answer.target.to_text())) for host in hosts: yield Result( self, constants.WARNING, msg='Expected SRV record missing', key=self.srv_to_name(srv, host)) for uri in uri_rec: logger.debug("Search DNS for URI record of %s", uri) answers = query_uri(uri) hosts = uri_rec[uri] for answer in answers: logger.debug("DNS record found: %s", answer) try: hosts.remove(answer.target.decode('utf-8')) yield Result( self, constants.SUCCESS, key=self.uri_to_name( uri, answer.target.decode('utf-8') ) ) except ValueError: yield Result( self, constants.WARNING, msg='Unexpected URI entry in DNS', key=self.uri_to_name( uri, answer.target.decode('utf-8') ) ) for host in hosts: yield Result( self, constants.WARNING, msg='Expected URI record missing', key=self.uri_to_name(uri, host) ) for txt in txt_rec: logger.debug("Search DNS for TXT record of %s", txt) try: answers = resolve(txt, rdatatype.TXT) except DNSException as e: logger.debug("DNS record not found: %s", e.__class__.__name__) answers = [] realms = txt_rec[txt] for answer in answers: logger.debug("DNS record found: %s", answer) realm = answer.to_text() try: realms.remove(realm) yield Result(self, constants.SUCCESS, key=realm) except ValueError: yield Result(self, constants.WARNING, key=realm, msg='expected realm missing') if a_rec: # Look up the ipa-ca records qname = "ipa-ca." + api.env.domain + "." logger.debug("Search DNS for A record of %s", qname) try: answers = resolve(qname, rdatatype.A) except DNSException as e: logger.debug("DNS record not found: %s", e.__class__.__name__) answers = [] for answer in answers: logger.debug("DNS record found: %s", answer) ipaddr = answer.to_text() try: yield Result(self, constants.SUCCESS, key=ipaddr) except ValueError: yield Result(self, constants.WARNING, key=ipaddr, msg='expected ipa-ca IPv4 address missing') ca_count = 0 for server in system_records.servers_data: master = system_records.servers_data.get(server) if 'CA server' in master.get('roles'): ca_count += 1 if len(answers) != ca_count: yield Result( self, constants.WARNING, key='ca_count_a_rec', msg='Got {count} ipa-ca A records, expected {expected}', count=len(answers), expected=ca_count) if aaaa_rec: # Look up the ipa-ca records qname = "ipa-ca." + api.env.domain + "." logger.debug("Search DNS for AAAA record of %s", qname) try: answers = resolve(qname, rdatatype.AAAA) except DNSException as e: logger.debug("DNS record not found: %s", e.__class__.__name__) answers = [] for answer in answers: logger.debug("DNS record found: %s", answer) ipaddr = answer.to_text() try: yield Result(self, constants.SUCCESS, key=ipaddr) except ValueError: yield Result(self, constants.WARNING, key=ipaddr, msg='expected ipa-ca IPv6 address missing') ca_count = 0 for server in system_records.servers_data: master = system_records.servers_data.get(server) if 'CA server' in master.get('roles'): ca_count += 1 if len(answers) != ca_count: yield Result( self, constants.WARNING, key='ca_count_aaaa_rec', msg='Got {count} ipa-ca AAAA records, expected {expected}', count=len(answers), expected=ca_count) freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/meta.py000066400000000000000000000015131420053437700231100ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from ipahealthcheck.core import constants from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipalib import api @registry class IPAMetaCheck(IPAPlugin): """Return meta data for the IPA installation""" requires = ('dirsrv',) @duration def check(self): try: result = api.Command.server_find(pkey_only=True) except Exception as e: yield Result(self, constants.ERROR, msg='server-show failed, %s' % e) else: masters = [] for server in result['result']: masters.append(server['cn'][0]) yield Result(self, constants.SUCCESS, masters=masters) freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/nss.py000066400000000000000000000035021420053437700227650ustar00rootroot00000000000000# # Copyright (C) 2021 FreeIPA Contributors see COPYING for license # import grp import logging from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipahealthcheck.core.plugin import Result from ipahealthcheck.core.plugin import duration from ipahealthcheck.core import constants logger = logging.getLogger() # A tuple of groups and a tuple of expected members # # For example the apache user needs to be in the ipaapi group so # the tuple would look like: 'ipaapi', ('apache',). # # The second value is a tuple so that we can more easily extend if # multiple users need to be a member of a group. # # (group_name, (members,)) GROUP_MEMBERS = ( ('ipaapi', ('apache',)), ) @registry class IPAGroupMemberCheck(IPAPlugin): """ Ensure that nss/POSIX group membership is as expected. This can be critical for security and/or proper access control and is primarily being checked for privilege separation. The ipaapi user needs to be able to read ccaches created by Apache. """ @duration def check(self): for (group, members) in GROUP_MEMBERS: try: grp_group = grp.getgrnam(group) except KeyError: yield Result(self, constants.ERROR, key=group, msg='group {key} does not exist') continue for member in members: if member not in grp_group.gr_mem: yield Result(self, constants.ERROR, key=group, member=member, msg='{member} is not a member of group {key}') else: yield Result(self, constants.SUCCESS, key=group, member=member) freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/plugin.py000066400000000000000000000061001420053437700234550ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import logging from ipalib import api, errors try: from ipapython.ipaldap import realm_to_serverid except ImportError: from ipaserver.install.installutils import realm_to_serverid from ipaserver.install import cainstance from ipaserver.install import dsinstance from ipaserver.install import httpinstance from ipaserver.install import installutils from ipahealthcheck.core.plugin import Plugin, Registry logger = logging.getLogger() class IPAPlugin(Plugin): def __init__(self, reg): super().__init__(reg) self.ca = cainstance.CAInstance(api.env.realm, host_name=api.env.host) self.http = httpinstance.HTTPInstance() self.ds = dsinstance.DsInstance() self.serverid = realm_to_serverid(api.env.realm) self.conn = api.Backend.ldap2 class IPARegistry(Registry): def __init__(self): super().__init__() self.trust_agent = False self.trust_controller = False self.ca_configured = False def initialize(self, framework, config, options=None): super().initialize(framework, config) # deferred import for mock # pylint: disable=import-outside-toplevel from ipaserver.servroles import ADtrustBasedRole, ServiceBasedRole # pylint: enable=import-outside-toplevel installutils.check_server_configuration() if not api.isdone('finalize'): if not api.isdone('bootstrap'): api.bootstrap(in_server=True, context='ipahealthcheck', log=None) if not api.isdone('finalize'): api.finalize() if not api.Backend.ldap2.isconnected(): try: api.Backend.ldap2.connect() except (errors.CCacheError, errors.NetworkError) as e: logger.debug('Failed to connect to LDAP: %s', e) return ca = cainstance.CAInstance(api.env.realm, host_name=api.env.host) self.ca_configured = ca.is_configured() # This package is pulled in when the trust package is installed # and is required to lookup trust users. If this is not installed # then it can be inferred that trust is not enabled. try: # pylint: disable=unused-import,import-outside-toplevel import pysss_nss_idmap # noqa: F401 # pylint: enable=unused-import,import-outside-toplevel except ImportError: return roles = ( ADtrustBasedRole(u"ad_trust_agent_server", u"AD trust agent"), ServiceBasedRole( u"ad_trust_controller_server", u"AD trust controller", component_services=['ADTRUST'] ), ) role = roles[0].status(api)[0] if role.get('status') == 'enabled': self.trust_agent = True role = roles[1].status(api)[0] if role.get('status') == 'enabled': self.trust_controller = True registry = IPARegistry() freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/proxy.py000066400000000000000000000100561420053437700233450ustar00rootroot00000000000000# # Copyright (C) 2021 FreeIPA Contributors see COPYING for license # import logging import lxml.etree import re from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipahealthcheck.core.plugin import Result from ipahealthcheck.core.plugin import duration from ipahealthcheck.core import constants from ipaplatform.paths import paths logger = logging.getLogger() def read_ipa_pki_proxy(): """Read the IPA Proxy configuration file Split out to make it easier to mock """ with open(paths.HTTPD_IPA_PKI_PROXY_CONF, "r") as fd: lines = fd.readlines() return lines @registry class IPAProxySecretCheck(IPAPlugin): """ Ensure that the proxy secrets match between tomcat and Apache Also report if tomcat has both secret and requiredSecret defined and whether all three secrets match. """ @duration def check(self): if not self.ca.is_configured(): logger.debug("CA is not configured, skipping IPAProxySecretCheck") return PROXY_SECRETS = 'proxy_secrets' # so many things can go wrong just keep one big global to # determine if we can eventually return SUCCESS failures = False server_xml = lxml.etree.parse(paths.PKI_TOMCAT_SERVER_XML) doc = server_xml.getroot() # no AJP connector means nothing to check connectors = doc.xpath('//Connector[@protocol="AJP/1.3"]') if len(connectors) == 0: yield Result(self, constants.CRITICAL, key=PROXY_SECRETS, server_xml=paths.PKI_TOMCAT_SERVER_XML, msg='No AJP/1.3 Connectors defined in {server_xml}') return # IPA only deals with the first connect so that's all we'll check connector = connectors[0] ajp_secret = [] if 'secret' in connector.attrib: ajp_secret.append(connector.attrib['secret']) if 'requiredSecret' in connector.attrib: ajp_secret.append(connector.attrib['requiredSecret']) if len(ajp_secret) > 1: if ajp_secret[0] != ajp_secret[1]: failures = True yield Result( self, constants.WARNING, key=PROXY_SECRETS, server_xml=paths.PKI_TOMCAT_SERVER_XML, msg='The AJP secrets in {server_xml} do not match' ) # We could warn that both secret and requiredSecret are defined # but the presence of both with the same password doesn't # break anything so we will not warn for now. lines = read_ipa_pki_proxy() proxy_secrets = [] PROXY_RE = r'\s+ProxyPassMatch ajp://localhost:8009 secret=(\w+)$' # Collect all the ipa-pki-proxy.conf secrets and ensure they all match for line in lines: m = re.match(PROXY_RE, line) if m: proxy_secrets.extend(m.groups(1)) if not proxy_secrets: failures = True yield Result( self, constants.CRITICAL, key=PROXY_SECRETS, proxy_conf=paths.HTTPD_IPA_PKI_PROXY_CONF, msg='No ProxyPassMatch secrets found in {proxy_conf}' ) return if len(set(proxy_secrets)) != 1: failures = True yield Result( self, constants.CRITICAL, key=PROXY_SECRETS, proxy_conf=paths.HTTPD_IPA_PKI_PROXY_CONF, msg='Not all ProxyPassMatch secrets match in {proxy_conf}' ) for secret in proxy_secrets: if secret not in ajp_secret: failures = True yield Result( self, constants.CRITICAL, key=PROXY_SECRETS, server_xml=paths.PKI_TOMCAT_SERVER_XML, msg='A ProxyPassMatch secret not found in {server_xml}' ) if not failures: yield Result(self, constants.SUCCESS, key=PROXY_SECRETS) freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/roles.py000066400000000000000000000037561420053437700233210ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import logging from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipahealthcheck.core.plugin import Result from ipahealthcheck.core.plugin import duration from ipahealthcheck.core import constants from ipalib import api logger = logging.getLogger() @registry class IPACRLManagerCheck(IPAPlugin): """ Determine if this master is the CRL manager This check in itself will always return SUCCESS and is only useful in the context of the ohter masters. Some external service is expected to aggregate this. """ @duration def check(self): if not self.ca.is_configured(): return try: enabled = self.ca.is_crlgen_enabled() except AttributeError: yield Result(self, constants.SUCCESS, key='crl_manager', crlgen_enabled=None, msg='Not available in this version of IPA') else: yield Result(self, constants.SUCCESS, key='crl_manager', crlgen_enabled=enabled) @registry class IPARenewalMasterCheck(IPAPlugin): """ Determine if this master is the CA renewal master. This check in itself will always return SUCCESS and is only useful in the context of the ohter masters. Some external service is expected to aggregate this. """ requires = ('dirsrv',) @duration def check(self): try: result = api.Command.config_show() except Exception as e: yield Result(self, constants.ERROR, key='renewal_master', msg='Request for configuration failed, %s' % e) else: server = result['result'].get('ca_renewal_master_server', None) yield Result(self, constants.SUCCESS, key='renewal_master', master=server == api.env.host) freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/topology.py000066400000000000000000000052761420053437700240500ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import logging from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipahealthcheck.core.plugin import Result from ipahealthcheck.core.plugin import duration from ipahealthcheck.core import constants from ipalib import api logger = logging.getLogger() @registry class IPATopologyDomainCheck(IPAPlugin): """ Execute the equivalant of ipa topologysuffix-verify domain Return any errors discovered. This can include: * too many agreements * connection errors """ def report_errors(self, suffix, result): if result['result']['in_order']: yield Result(self, constants.SUCCESS, suffix=suffix) else: max_agmts = result['result']['max_agmts'] connect_errors = result['result']['connect_errors'] max_agmts_errors = result['result']['max_agmts_errors'] cmsg = 'Server %(srv)s can\'t contact servers: %(replicas)s' mmsg = 'Server "%(srv)s" has %(n)d agreements, recommended ' \ 'max %(m)d' if connect_errors: for error in connect_errors: msg = cmsg % {'srv': error[0], 'replicas': ', '.join(error[1])} yield Result(self, constants.ERROR, key=error[0], replicas=error[2], suffix=suffix, type='connect', msg=msg) if max_agmts_errors: for error in max_agmts_errors: msg = mmsg % {'srv': error[0], 'n': len(error[1]), 'm': max_agmts} yield Result(self, constants.ERROR, key=error[0], replicas=error[1], suffix=suffix, type='max', msg=msg) def run_check(self, suffix): try: result = api.Command.topologysuffix_verify(suffix) except Exception as e: yield Result(self, constants.ERROR, msg='topologysuffix-verify domain failed, %s' % e) else: for r in self.report_errors(suffix, result): yield r requires = ('dirsrv',) @duration def check(self): for y in self.run_check(u'domain'): yield y if api.Command.ca_is_enabled()['result']: for y in self.run_check(u'ca'): yield y freeipa-healthcheck-0.10/src/ipahealthcheck/ipa/trust.py000066400000000000000000000577661420053437700233700ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import configparser import logging import SSSDConfig from ipahealthcheck.ipa.plugin import IPAPlugin, registry from ipahealthcheck.core.plugin import Result from ipahealthcheck.core.plugin import duration from ipahealthcheck.core import constants from ipalib import api from ipaplatform.paths import paths from ipapython import ipautil from ipapython.dn import DN try: import pysss_nss_idmap except ImportError: # agent and controller will be set to False in init, all tests will # be skipped pass try: from ipaserver.masters import ENABLED_SERVICE, HIDDEN_SERVICE except ImportError: from ipaserver.install.service import ENABLED_SERVICE, HIDDEN_SERVICE try: from ipapython.ipaldap import realm_to_serverid except ImportError: from ipaserver.install.installutils import realm_to_serverid logger = logging.getLogger() def get_trust_domains(): """ Get the list of AD trust domains from IPA The caller is expected to catch any exceptions. Each entry is a dictionary representating an AD domain. """ trust_domains = [] trusts = api.Command.trust_find(pkey_only=True, raw=True) for trust in trusts['result']: for cn in trust.get('cn'): trustdomains = api.Command.trustdomain_find(cn, raw=True) for trustdomain in trustdomains['result']: domain = dict() domain['domain'] = trustdomain.get('cn')[0] domain['domainsid'] = trustdomain.get( 'ipanttrusteddomainsid')[0] domain['netbios'] = trustdomain.get('ipantflatname')[0] trust_domains.append(domain) return trust_domains @registry class IPATrustAgentCheck(IPAPlugin): """ Check the values that should be set when configures as a trust agent. """ @duration def check(self): if not self.registry.trust_agent: logger.debug('Not a trust agent, skipping') return try: sssdconfig = SSSDConfig.SSSDConfig() sssdconfig.import_config() except Exception as e: logger.debug('Failed to parse sssd.conf: %s', e) yield Result(self, constants.CRITICAL, error=str(e), msg='Unable to parse sssd.conf: {error}') return else: domains = sssdconfig.list_active_domains() errors = False for name in domains: domain = sssdconfig.get_domain(name) try: provider = domain.get_option('id_provider') except SSSDConfig.NoOptionError: continue if provider == "ipa": try: mode = domain.get_option('ipa_server_mode') except SSSDConfig.NoOptionError: yield Result(self, constants.ERROR, key='ipa_server_mode_missing', attr='ipa_server_mode', domain=name, sssd_config=paths.SSSD_CONF, msg='{sssd_config} is missing {attr} ' 'in the domain {domain}') errors = True else: if not mode: yield Result(self, constants.ERROR, key='ipa_server_mode_false', attr='ipa_server_mode', domain=name, sssd_config=paths.SSSD_CONF, msg='{attr} is not True in {sssd_config} ' 'in the domain {domain}') errors = True if not errors: yield Result(self, constants.SUCCESS) @registry class IPATrustDomainsCheck(IPAPlugin): """ Check the trust domains """ @duration def check(self): if not self.registry.trust_agent: logger.debug('Not a trust agent, skipping') return result = ipautil.run([paths.SSSCTL, "domain-list"], raiseonerr=False, capture_output=True) if result.returncode != 0: yield Result(self, constants.ERROR, key='domain_list_error', sssctl=paths.SSSCTL, error=result.error_log, msg='Execution of {sssctl} failed: {error}') return sssd_domains = result.output.strip().split('\n') if 'implicit_files' in sssd_domains: sssd_domains.remove('implicit_files') trust_domains = [] try: domains = get_trust_domains() except Exception as e: yield Result(self, constants.WARNING, key='trust-find', error=str(e), msg='Execution of {key} failed: {error}') else: for entry in domains: trust_domains.append(entry.get('domain')) if api.env.domain in sssd_domains: sssd_domains.remove(api.env.domain) else: yield Result(self, constants.ERROR, key=api.env.domain, sssctl=paths.SSSCTL, msg='{key} not in {sssctl} domain-list') trust_domains_out = ', '.join(trust_domains) sssd_domains_out = ', '.join(sssd_domains) if set(trust_domains).symmetric_difference(set(sssd_domains)): yield Result(self, constants.ERROR, key='domain-list', sssctl=paths.SSSCTL, sssd_domains=sssd_domains_out, trust_domains=trust_domains_out, msg='{sssctl} {key} reports mismatch: ' 'sssd domains {sssd_domains} ' 'trust domains {trust_domains}') else: yield Result(self, constants.SUCCESS, key='domain-list', sssd_domains=sssd_domains_out, trust_domains=trust_domains_out) for domain in sssd_domains: args = [paths.SSSCTL, "domain-status", domain, "--online"] try: result = ipautil.run(args, capture_output=True) except Exception as e: yield Result(self, constants.WARNING, key='domain-status', error=str(e), msg='Execution of {key} failed: {error}') continue else: if result.output.strip() != 'Online status: Online': yield Result(self, constants.WARNING, key='domain-status', domain=domain, msg='Domain {domain} is not online') else: yield Result(self, constants.SUCCESS, key='domain-status', domain=domain) @registry class IPADomainCheck(IPAPlugin): """ Check that the IPA domain provider is configured to use ipa """ @duration def check(self): try: sssdconfig = SSSDConfig.SSSDConfig() sssdconfig.import_config() except Exception as e: logger.debug('Failed to parse sssd.conf: %s', e) yield Result(self, constants.CRITICAL, error=str(e), key='domain-check', msg='Unable to parse sssd.conf: {error}') return try: domain = sssdconfig.get_domain(api.env.domain) except SSSDConfig.NoDomainError: yield Result(self, constants.ERROR, key='domain-check', domain=api.env.domain, msg='IPA domain {domain} not found in sssd.conf') return error = False for option in ('id_provider', 'auth_provider', 'chpass_provider', 'access_provider'): try: provider = domain.get_option(option) except SSSDConfig.NoOptionError: yield Result(self, constants.ERROR, key='domain-check', domain=api.env.domain, option=option, msg='Option {option} in domain {domain} not ' 'found in sssd.conf') error = True continue if provider != "ipa": yield Result(self, constants.ERROR, key='domain-check', option=option, provider=provider, domain=api.env.domain, msg='Option {option} in domain {domain} is ' '{provider} not ipa') error = True if not error: yield Result(self, constants.SUCCESS, key='domain-check') @registry class IPATrustCatalogCheck(IPAPlugin): """ Resolve an AD user This should populate the 'AD Global catalog' and 'AD Domain Controller' fields in 'sssctl domain-status' output (means SSSD actually talks to AD DCs) """ @duration def check(self): if not self.registry.trust_agent: logger.debug('Not a trust agent, skipping') return try: trust_domains = get_trust_domains() except Exception as e: yield Result(self, constants.WARNING, key='trust-find', error=str(e), msg='Execution of {key} failed: {error}') trust_domains = [] for trust_domain in trust_domains: sid = trust_domain.get('domainsid') try: id = pysss_nss_idmap.getnamebysid(sid + '-500') except Exception as e: yield Result(self, constants.ERROR, key=sid, error=str(e), msg='Look up of{key} failed: {error}') continue if not id: yield Result(self, constants.WARNING, key=sid, error='returned nothing', msg='Look up of {key} {error}') else: yield Result(self, constants.SUCCESS, key='Domain Security Identifier', sid=sid) domain = trust_domain.get('domain') args = [paths.SSSCTL, "domain-status", domain, "--active-server"] try: result = ipautil.run(args, capture_output=True) except Exception as e: yield Result(self, constants.ERROR, key='domain-status', error=str(e), msg='Execution of {key} failed: {error}') continue else: for txt in ['AD Global Catalog', 'AD Domain Controller']: if txt not in result.output: yield Result(self, constants.ERROR, key=txt, output=result.output.strip(), sssctl=paths.SSSCTL, domain=domain, msg='{key} not found in {sssctl} ' '\'domain-status\' output: {output}') else: yield Result(self, constants.SUCCESS, key=txt, domain=domain) @registry class IPAsidgenpluginCheck(IPAPlugin): """ Verify that the sidgen 389-ds plugins are enabled """ @duration def check(self): if not self.registry.trust_agent: logger.debug('Not a trust agent, skipping') return for plugin in ['IPA SIDGEN', 'ipa-sidgen-task']: sidgen_dn = DN(('cn', plugin), "cn=plugins,cn=config") try: entry = self.conn.get_entry( sidgen_dn, attrs_list=['nsslapd-pluginEnabled']) except Exception as e: yield Result(self, constants.ERROR, key=plugin, error=str(e), msg='Error retrieving 389-ds plugin {key}: ' '{error}') else: enabled = entry.get('nsslapd-pluginEnabled', []) if len(enabled) != 1: yield Result(self, constants.ERROR, key=plugin, dn=str(sidgen_dn), attr=enabled, msg='{key}: unexpected value in ' 'nsslapd-pluginEnabled in entry {dn}' '{attr}') continue if entry.get('nsslapd-pluginEnabled', [])[0].lower() != 'on': yield Result(self, constants.ERROR, key=plugin, msg='389-ds plugin {key} is not enabled') else: yield Result(self, constants.SUCCESS, key=plugin) @registry class IPATrustAgentMemberCheck(IPAPlugin): """ Verify that the current host is a member of adtrust agents """ @duration def check(self): if not self.registry.trust_agent: logger.debug('Not a trust agent, skipping') return agent_dn = DN(('fqdn', api.env.host), api.env.container_host, api.env.basedn) group_dn = DN(('cn', 'adtrust agents'), api.env.container_sysaccounts, api.env.basedn) try: entry = self.conn.get_entry( agent_dn, attrs_list=['memberOf']) except Exception as e: yield Result(self, constants.ERROR, key=str(agent_dn), error=str(e), msg='Error retrieving ldap entry {key}: ' '{error}') else: memberof = entry.get('memberof', []) for member in memberof: if DN(member) == group_dn: yield Result(self, constants.SUCCESS, key=api.env.host) return yield Result(self, constants.ERROR, key=api.env.host, group='adtrust agents', msg='{key} is not a member of {group}') @registry class IPATrustControllerPrincipalCheck(IPAPlugin): """ Verify that the current host cifs principal is a member of adtrust agents """ @duration def check(self): if not self.registry.trust_controller: logger.debug('Not a trust controller, skipping') return agent_dn = DN(('krbprincipalname', 'cifs/%s@%s' % (api.env.host, api.env.realm)), api.env.container_service, api.env.basedn) group_dn = DN(('cn', 'adtrust agents'), api.env.container_sysaccounts, api.env.basedn) try: entry = self.conn.get_entry( agent_dn, attrs_list=['memberOf']) except Exception as e: yield Result(self, constants.ERROR, key=str(agent_dn), error=str(e), msg='Error retrieving ldap entry {key}: ' '{error}') else: memberof = entry.get('memberof', []) for member in memberof: if DN(member) == group_dn: yield Result(self, constants.SUCCESS, key='cifs/%s@%s' % (api.env.host, api.env.realm)) return yield Result(self, constants.ERROR, key='cifs/%s@%s' % (api.env.host, api.env.realm), group='adtrust agents', msg='{key} is not a member of {group}') @registry class IPATrustControllerServiceCheck(IPAPlugin): """ Verify that the current host starts the ADTRUST service. """ @duration def check(self): if not self.registry.trust_controller: logger.debug('Not a trust controller, skipping') return service_dn = DN(('cn', 'ADTRUST'), ('cn', api.env.host), api.env.container_masters, api.env.basedn) try: entry = self.conn.get_entry( service_dn, attrs_list=['ipaconfigstring']) except Exception as e: yield Result(self, constants.ERROR, key=str(service_dn), error=str(e), msg='Error retrieving ldap entry {key}: ' '{error}') else: configs = entry.get('ipaconfigstring', []) enabled = False for config in configs: if config in [ENABLED_SERVICE, HIDDEN_SERVICE]: enabled = True break if enabled: yield Result(self, constants.SUCCESS, key='ADTRUST') else: yield Result(self, constants.ERROR, key='ADTRUST', msg='{key} service is not enabled') @registry class IPATrustControllerConfCheck(IPAPlugin): """ Verify that certain elements of the configuration are unchanged This is expected to be expanded over time. """ @duration def check(self): if not self.registry.trust_controller: logger.debug('Not a trust controller, skipping') return ldapi_socket = "ipasam:ldapi://%%2fvar%%2frun%%2fslapd-%s.socket" % \ realm_to_serverid(api.env.realm) try: result = ipautil.run(['net', 'conf', 'list'], capture_output=True) except Exception as e: yield Result(self, constants.ERROR, key='net conf list', error=str(e), msg='Execution of {key} failed: {error}') return conf = result.output.replace('\t', '') config = configparser.ConfigParser(delimiters=('='), interpolation=None) try: config.read_string(conf) except Exception as e: yield Result(self, constants.ERROR, key='net conf list', error=str(e), msg='Unable to parse {key} output: {error}') return try: net_ldapi = config.get('global', 'passdb backend') except Exception as e: yield Result(self, constants.ERROR, key='net conf list', error=str(e), section='global', option='passdb backend', msg='Unable to read \'{option}\' in section ' '{section} in {key} output: {error}') return if net_ldapi != ldapi_socket: yield Result(self, constants.ERROR, key='net conf list', got=net_ldapi, expected=ldapi_socket, option='passdb backend', msg='{key} option {option} value {got} ' 'doesn\'t match expected value {expected}') else: yield Result(self, constants.SUCCESS, key='net conf list') @registry class IPATrustControllerGroupSIDCheck(IPAPlugin): """ Verify that the admins group's SID ends with 512 (Domain Admins RID) """ @duration def check(self): if not self.registry.trust_controller: logger.debug('Not a trust controller, skipping') return admins_dn = DN(('cn', 'admins'), api.env.container_group, api.env.basedn) try: entry = self.conn.get_entry( admins_dn, attrs_list=['ipantsecurityidentifier']) except Exception as e: yield Result(self, constants.ERROR, key=str(admins_dn), error=str(e), msg='Error retrieving ldap entry {key}: ' '{error}') return identifier = entry.get('ipantsecurityidentifier', [None])[0] if not identifier or not identifier.endswith('512'): yield Result(self, constants.ERROR, key='ipantsecurityidentifier', rid=identifier, msg='{key} is not a Domain Admins RID') else: yield Result(self, constants.SUCCESS, rid=identifier, key='ipantsecurityidentifier') @registry class IPATrustControllerAdminSIDCheck(IPAPlugin): """ Verify that the admin user's SID ends with 500 """ @duration def check(self): if not self.registry.trust_controller: logger.debug('Not a trust controller, skipping') return admin_dn = DN(('uid', 'admin'), api.env.container_user, api.env.basedn) try: entry = self.conn.get_entry( admin_dn, attrs_list=['ipantsecurityidentifier']) except Exception as e: yield Result(self, constants.ERROR, key=str(admin_dn), error=str(e), msg='Error retrieving the admin user at {key}: ' '{error}') return identifier = entry.get('ipantsecurityidentifier', [None])[0] if not identifier or not identifier.endswith('500'): yield Result(self, constants.ERROR, key='ipantsecurityidentifier', rid=identifier, msg='{key} is not a Domain Admin RID') else: yield Result(self, constants.SUCCESS, rid=identifier, key='ipantsecurityidentifier') @registry class IPATrustPackageCheck(IPAPlugin): """ If AD trust is enabled verify that the trust-ad pkg is installed If AD trust is enabled and the master does not have the freeipa-server-trust-ad package installed then the master will able to resolve users/groups via extdom plugin and sssd but won't be able to do framework-specific operations. """ @duration def check(self): if self.registry.trust_controller: logger.debug('Trust controller, skipping') return if not self.registry.trust_agent: logger.debug('Not a trust agent, skipping') return # The trust-ad package provides this import try: # pylint: disable=unused-import,import-outside-toplevel from ipaserver.install import adtrustinstance # noqa: F401 # pylint: enable=unused-import,import-outside-toplevel yield Result(self, constants.SUCCESS, key='adtrustpackage') except ImportError: yield Result(self, constants.WARNING, key='adtrustpackage', msg='trust-ad sub-package is not installed. ' 'Administration will be limited.') freeipa-healthcheck-0.10/src/ipahealthcheck/meta/000077500000000000000000000000001420053437700217655ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/meta/__init__.py000066400000000000000000000000001420053437700240640ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/meta/core.py000066400000000000000000000063221420053437700232720ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import logging import os import socket from ipahealthcheck.core import constants from ipahealthcheck.core.exceptions import TimeoutError from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.meta.plugin import Plugin, registry from ipapython import ipautil from ipapython.version import VERSION, API_VERSION from ipaplatform.paths import paths if 'FIPS_MODE_SETUP' not in dir(paths): paths.FIPS_MODE_SETUP = '/usr/bin/fips-mode-setup' logger = logging.getLogger() @registry class MetaCheck(Plugin): @duration def check(self): rval = constants.SUCCESS if not os.path.exists(paths.FIPS_MODE_SETUP): fips = "missing {}".format(paths.FIPS_MODE_SETUP) logger.debug('%s is not installed, skipping', paths.FIPS_MODE_SETUP) else: try: result = ipautil.run([paths.FIPS_MODE_SETUP, '--is-enabled'], capture_output=True, raiseonerr=False,) except TimeoutError: logger.debug('fips-mode-setup timed out') fips = "check timed out" rval = constants.ERROR except Exception as e: logger.debug('fips-mode-setup failed: %s', e) fips = "failed to check" rval = constants.ERROR else: logger.debug(result.raw_output.decode('utf-8')) if result.returncode == 0: fips = "enabled" elif result.returncode == 1: fips = "inconsistent" elif result.returncode == 2: fips = "disabled" else: fips = "unknown" if not os.path.exists('/usr/sbin/ipa-acme-manage'): acme = "missing {}".format('/usr/sbin/ipa-acme-manage') logger.debug('%s is not installed, skipping', '/usr/sbin/ipa-acme-manage') else: try: result = ipautil.run(['ipa-acme-manage', 'status'], capture_output=True, raiseonerr=False,) except TimeoutError: logger.debug('ipa-acme-manage timed out') acme = "check timed out" rval = constants.ERROR except Exception as e: logger.debug('ipa-acme-manage failed: %s', e) acme = "failed to check" rval = constants.ERROR else: logger.debug(result.raw_output.decode('utf-8')) if "disabled" in result.output_log: acme = "disabled" elif "enabled" in result.output_log: acme = "enabled" else: acme = "unknown" yield Result(self, rval, key='meta', fqdn=socket.getfqdn(), fips=fips, acme=acme, ipa_version=VERSION, ipa_api_version=API_VERSION,) freeipa-healthcheck-0.10/src/ipahealthcheck/meta/plugin.py000066400000000000000000000003511420053437700236340ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from ipahealthcheck.core.plugin import Plugin, Registry class MetaPlugin(Plugin): pass class MetaRegistry(Registry): pass registry = MetaRegistry() freeipa-healthcheck-0.10/src/ipahealthcheck/meta/services.py000066400000000000000000000070461420053437700241710ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import logging from ipahealthcheck.core import constants from ipahealthcheck.core.plugin import Result, duration from ipahealthcheck.core.service import ServiceCheck from ipahealthcheck.meta.plugin import registry try: from ipapython.ipaldap import realm_to_serverid except ImportError: from ipaserver.install.installutils import realm_to_serverid from ipalib import api from ipaplatform import services from ipaserver.install import bindinstance from ipaserver.install import cainstance logger = logging.getLogger() class IPAServiceCheck(ServiceCheck): @duration def check(self, instance=''): try: # services named with a hyphen cannot be addressed # as knownservices.name # so use knownservices['name'] instead self.service = services.knownservices[self.service_name] except KeyError: logger.debug( "Service '%s' is unknown to ipaplatform, skipping check", self.service_name ) return () status = self.service.is_running(instance) if status is False: yield Result(self, constants.ERROR, status=status, msg='%s: not running' % self.service.service_name) else: yield Result(self, constants.SUCCESS, status=status) return None @registry class certmonger(IPAServiceCheck): def check(self, instance=''): self.service_name = 'certmonger' return super().check() @registry class dirsrv(IPAServiceCheck): def check(self, instance=''): self.service_name = 'dirsrv' return super().check(realm_to_serverid(api.env.realm)) @registry class gssproxy(IPAServiceCheck): def check(self, instance=''): self.service_name = 'gssproxy' return super().check() @registry class httpd(IPAServiceCheck): def check(self, instance=''): self.service_name = 'httpd' return super().check() @registry class ipa_custodia(IPAServiceCheck): def check(self, instance=''): self.service_name = 'ipa-custodia' return super().check() @registry class ipa_dnskeysyncd(IPAServiceCheck): requires = ('dirsrv',) def check(self, instance=''): self.service_name = 'ipa-dnskeysyncd' if not bindinstance.named_conf_exists(): return () return super().check() @registry class ipa_otpd(IPAServiceCheck): def check(self, instance=''): self.service_name = 'ipa-otpd' return super().check() @registry class kadmin(IPAServiceCheck): def check(self, instance=''): self.service_name = 'kadmin' return super().check() @registry class krb5kdc(IPAServiceCheck): def check(self, instance=''): self.service_name = 'krb5kdc' return super().check() @registry class named(IPAServiceCheck): def check(self, instance=''): self.service_name = 'named' if not bindinstance.named_conf_exists(): return () return super().check() @registry class pki_tomcatd(IPAServiceCheck): def check(self, instance=''): self.service_name = 'pki_tomcatd' ca = cainstance.CAInstance(api.env.realm, host_name=api.env.host) if not ca.is_configured(): return () return super().check() @registry class sssd(IPAServiceCheck): def check(self, instance=''): self.service_name = 'sssd' return super().check() freeipa-healthcheck-0.10/src/ipahealthcheck/system/000077500000000000000000000000001420053437700223635ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/system/__init__.py000066400000000000000000000000001420053437700244620ustar00rootroot00000000000000freeipa-healthcheck-0.10/src/ipahealthcheck/system/filesystemspace.py000066400000000000000000000076511420053437700261460ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from __future__ import division import os import shutil from ipahealthcheck.system.plugin import SystemPlugin, registry from ipahealthcheck.core.plugin import duration, Result from ipahealthcheck.core import constants def in_container(): """Determine if we're running in a container.""" with open('/proc/1/sched', 'r') as sched: data_sched = sched.readline() with open('/proc/self/cgroup', 'r') as cgroup: data_cgroup = cgroup.readline() checks = [ data_sched.split()[0] not in ('systemd', 'init',), data_cgroup.split()[0] not in ('libpod'), os.path.exists('/.dockerenv'), os.path.exists('/.dockerinit'), os.getenv('container', None) is not None ] return any(checks) @registry class FileSystemSpaceCheck(SystemPlugin): """Check for filesystem available space.""" # watch important directories for FreeIPA _pathchecks = { '/var/lib/dirsrv/': 1024, '/var/lib/ipa/backup/': 512, '/var/log/': 1024, '/var/tmp/': 512, '/tmp': 512 } if not in_container(): _pathchecks['/var/log/audit/'] = 512 # File systems reaching 90% capacity risk fragmentation. # Defragmentation is never desirable and not available # on ext4 anyway. So error out at 20% free space. min_free_percent = 20 def get_fs_free_space(self, pathname): stat = shutil.disk_usage(pathname) return int(stat.free / 2**20) def get_fs_free_space_percentage(self, pathname): stat = shutil.disk_usage(pathname) return int(stat.free * 100 / stat.total) @duration def check(self): for store in self._pathchecks: try: percent_free = self.get_fs_free_space_percentage(store) except FileNotFoundError: yield Result( self, constants.WARNING, key=store, msg='File system {store} is not mounted', store=store ) continue if percent_free < self.min_free_percent: yield Result( self, constants.ERROR, key=store, msg='%s: %s %s%% < %s%%' % ( store, 'free space percentage under threshold:', percent_free, self.min_free_percent ), store=store, percent_free=percent_free, threshold=self.min_free_percent ) else: yield Result( self, constants.SUCCESS, key=store, msg='%s: %s %s%% >= %s%%' % ( store, 'free space percentage within limits:', percent_free, self.min_free_percent ), store=store, percent_free=percent_free, threshold=self.min_free_percent ) free_space = self.get_fs_free_space(store) threshold = self._pathchecks[store] if free_space < threshold: yield Result( self, constants.ERROR, key=store, msg='%s: %s %s MiB < %s MiB' % ( store, 'free space under threshold:', free_space, threshold ), store=store, free_space=free_space, threshold=threshold ) else: yield Result( self, constants.SUCCESS, key=store, msg='%s: %s %s MiB >= %s MiB' % ( store, 'free space within limits:', free_space, threshold ), store=store, free_space=free_space, threshold=threshold ) freeipa-healthcheck-0.10/src/ipahealthcheck/system/plugin.py000066400000000000000000000004561420053437700242400ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from ipahealthcheck.core.plugin import Plugin, Registry class SystemPlugin(Plugin): pass class SystemRegistry(Registry): def initialize(self, framework, config, options=None): pass registry = SystemRegistry() freeipa-healthcheck-0.10/systemd/000077500000000000000000000000001420053437700170035ustar00rootroot00000000000000freeipa-healthcheck-0.10/systemd/ipa-healthcheck.service000066400000000000000000000002261420053437700233770ustar00rootroot00000000000000[Unit] Description=Execute IPA Healthcheck [Service] Type=simple ExecStart=/usr/libexec/ipa/ipa-healthcheck.sh [Install] WantedBy=multi-user.target freeipa-healthcheck-0.10/systemd/ipa-healthcheck.sh000077500000000000000000000001531420053437700223530ustar00rootroot00000000000000#!/bin/sh LOGDIR=/var/log/ipa/healthcheck /usr/bin/ipa-healthcheck --output-file $LOGDIR/healthcheck.log freeipa-healthcheck-0.10/systemd/ipa-healthcheck.timer000066400000000000000000000002421420053437700230550ustar00rootroot00000000000000[Unit] Description=Execute IPA Healthcheck every day at 4AM [Timer] OnCalendar=*-*-* 04:00:00 Unit=ipa-healthcheck.service [Install] WantedBy=multi-user.target freeipa-healthcheck-0.10/tests/000077500000000000000000000000001420053437700164555ustar00rootroot00000000000000freeipa-healthcheck-0.10/tests/base.py000066400000000000000000000033771420053437700177530ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from unittest import mock, TestCase from util import no_exceptions from util import ADtrustBasedRole, ServiceBasedRole class BaseTest(TestCase): """ Base class for tests. Most tests use the same set of mocks so centralize and apply them once when the class of tests is created. A child class defines self.patches as a dictionary of functions and Mock values. These are applied once when the class starts up. If a test needs a particular value then it will need to use @patch individually. A default set of Mock patches is set because they apply to all or nearly all test cases. """ default_patches = { 'ipaserver.install.installutils.check_server_configuration': mock.Mock(return_value=None), 'ipaserver.servroles.ServiceBasedRole': mock.Mock(return_value=ServiceBasedRole()), 'ipaserver.servroles.ADtrustBasedRole': mock.Mock(return_value=ADtrustBasedRole()), } patches = {} results = None applied_patches = None def setup_class(self): # collect the list of patches to be applied for this class of # tests self.default_patches.update(self.patches) self.applied_patches = [ mock.patch(patch, data) for patch, data in self.default_patches.items() ] for patch in self.applied_patches: patch.start() def teardown_class(self): mock.patch.stopall() def tearDown(self): """ Ensure that no exceptions snuck into the results which might not be noticed because an exception may have the same result as the expected result. """ no_exceptions(self.results) freeipa-healthcheck-0.10/tests/clusterdata.py000066400000000000000000000537161420053437700213560ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # ONE_MASTER = { 'ipa.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "ipa.ipa.example", "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", ] } }, # No RUV's on a freshly installed master { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { } }, ] } THREE_MASTERS_OK = { 'ipa.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "ipa.ipa.example", "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example" ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "dc=ipa,dc=example", "ruv": "4" } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "o=ipaca", "ruv": "6" } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_dc=ipa,dc=example", "suffix": "dc=ipa,dc=example", "ruvs": [ [ "ipa.ipa.example", "4" ], [ "replica2.ipa.example", "7" ], [ "replica1.ipa.example", "3" ] ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_o=ipaca", "suffix": "o=ipaca", "ruvs": [ [ "ipa.ipa.example", "6" ], [ "replica2.ipa.example", "8" ], [ "replica1.ipa.example", "5" ] ] } } ], 'replica1.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "replica1.ipa.example", "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example", ], "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example" ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "dc=ipa,dc=example", "ruv": "3" } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "o=ipaca", "ruv": "5" } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_dc=ipa,dc=example", "suffix": "dc=ipa,dc=example", "ruvs": [ [ "replica1.ipa.example", "3" ], [ "ipa.ipa.example", "4" ], [ "replica2.ipa.example", "7" ] ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_o=ipaca", "suffix": "o=ipaca", "ruvs": [ [ "replica1.ipa.example", "5" ], [ "ipa.ipa.example", "6" ], [ "replica2.ipa.example", "8" ] ] } } ], 'replica2.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "replica2.ipa.example", "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example", ], "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example" ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "dc=ipa,dc=example", "ruv": "7" } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "o=ipaca", "ruv": "8" } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_dc=ipa,dc=example", "suffix": "dc=ipa,dc=example", "ruvs": [ [ "replica2.ipa.example", "7" ], [ "ipa.ipa.example", "4" ], [ "replica1.ipa.example", "3" ] ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_o=ipaca", "suffix": "o=ipaca", "ruvs": [ [ "replica2.ipa.example", "8" ], [ "ipa.ipa.example", "6" ], [ "replica1.ipa.example", "5" ] ] } } ] } # # Same three masters but replica1 has an extra RUV value # THREE_MASTERS_BAD_IPA_RUV = { 'ipa.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "ipa.ipa.example", "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example", ], "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example" ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "dc=ipa,dc=example", "ruv": "4" } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "o=ipaca", "ruv": "6" } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_dc=ipa,dc=example", "suffix": "dc=ipa,dc=example", "ruvs": [ [ "ipa.ipa.example", "4" ], [ "replica2.ipa.example", "7" ], [ "replica1.ipa.example", "3" ], [ "replica1.ipa.example", "9" ] ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_o=ipaca", "suffix": "o=ipaca", "ruvs": [ [ "ipa.ipa.example", "6" ], [ "replica2.ipa.example", "8" ], [ "replica1.ipa.example", "5" ] ] } } ], 'replica1.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "replica1.ipa.example", "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example", ], "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example" ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "dc=ipa,dc=example", "ruv": "3" } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "o=ipaca", "ruv": "5" } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_dc=ipa,dc=example", "suffix": "dc=ipa,dc=example", "ruvs": [ [ "replica1.ipa.example", "3" ], [ "ipa.ipa.example", "4" ], [ "replica2.ipa.example", "7" ] ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_o=ipaca", "suffix": "o=ipaca", "ruvs": [ [ "replica1.ipa.example", "5" ], [ "ipa.ipa.example", "6" ], [ "replica2.ipa.example", "8" ] ] } } ], 'replica2.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "replica2.ipa.example", "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example", ], "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example" ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "dc=ipa,dc=example", "ruv": "7" } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "o=ipaca", "ruv": "8" } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_dc=ipa,dc=example", "suffix": "dc=ipa,dc=example", "ruvs": [ [ "replica2.ipa.example", "7" ], [ "ipa.ipa.example", "4" ], [ "replica1.ipa.example", "3" ] ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_o=ipaca", "suffix": "o=ipaca", "ruvs": [ [ "replica2.ipa.example", "8" ], [ "ipa.ipa.example", "6" ], [ "replica1.ipa.example", "5" ] ] } } ] } # # Same three masters but replica2 CA has an extra RUV value # THREE_MASTERS_BAD_CS_RUV = { 'ipa.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "ipa.ipa.example", "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example", ], "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example" ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "dc=ipa,dc=example", "ruv": "4" } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "o=ipaca", "ruv": "6" } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_dc=ipa,dc=example", "suffix": "dc=ipa,dc=example", "ruvs": [ [ "ipa.ipa.example", "4" ], [ "replica2.ipa.example", "7" ], [ "replica1.ipa.example", "3" ], ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_o=ipaca", "suffix": "o=ipaca", "ruvs": [ [ "ipa.ipa.example", "6" ], [ "replica2.ipa.example", "8" ], [ "replica1.ipa.example", "5" ] ] } } ], 'replica1.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "replica1.ipa.example", "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example", ], "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example" ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "dc=ipa,dc=example", "ruv": "3" } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "o=ipaca", "ruv": "5" } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_dc=ipa,dc=example", "suffix": "dc=ipa,dc=example", "ruvs": [ [ "replica1.ipa.example", "3" ], [ "ipa.ipa.example", "4" ], [ "replica2.ipa.example", "7" ] ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_o=ipaca", "suffix": "o=ipaca", "ruvs": [ [ "replica1.ipa.example", "5" ], [ "ipa.ipa.example", "6" ], [ "replica2.ipa.example", "8" ] ] } } ], 'replica2.ipa.example': [ { "source": "ipahealthcheck.meta.core", "check": "MetaCheck", "result": "SUCCESS", "kw": { "fqdn": "replica2.ipa.example", "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example", ], "ipa_version": "4.8.4", "ipa_api_version": "2.235" } }, { "source": "ipahealthcheck.ipa.meta", "check": "IPAMetaCheck", "result": "SUCCESS", "kw": { "masters": [ "ipa.ipa.example", "replica1.ipa.example", "replica2.ipa.example" ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "dc=ipa,dc=example", "ruv": "7" } }, { "source": "ipahealthcheck.ds.ruv", "check": "RUVCheck", "result": "SUCCESS", "kw": { "key": "o=ipaca", "ruv": "8" } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_dc=ipa,dc=example", "suffix": "dc=ipa,dc=example", "ruvs": [ [ "replica2.ipa.example", "7" ], [ "ipa.ipa.example", "4" ], [ "replica1.ipa.example", "3" ] ] } }, { "source": "ipahealthcheck.ds.ruv", "check": "KnownRUVCheck", "result": "SUCCESS", "kw": { "key": "ruvs_o=ipaca", "suffix": "o=ipaca", "ruvs": [ [ "replica2.ipa.example", "8" ], [ "ipa.ipa.example", "6" ], [ "replica1.ipa.example", "5" ], [ "replica1.ipa.example", "9" ] ] } } ] } freeipa-healthcheck-0.10/tests/mock_certmonger.py000066400000000000000000000112131420053437700222030ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import copy from datetime import datetime, timedelta, timezone from ipaplatform.paths import paths # Fake certmonger tracked request list. This is similar but can be # distinct from the value from the overrident get_defaults() method. template = paths.CERTMONGER_COMMAND_TEMPLATE CERT_EXPIRATION_DAYS = 30 pristine_cm_requests = [ { 'nickname': '1234', 'cert-file': paths.RA_AGENT_PEM, 'key-file': paths.RA_AGENT_KEY, 'ca-name': 'dogtag-ipa-ca-renew-agent', 'template_profile': 'caSubsystemCert', 'cert-storage': 'FILE', 'cert-presave-command': template % 'renew_ra_cert_pre', 'cert-postsave-command': template % 'renew_ra_cert', 'not-valid-after': ( int( datetime(1970, 1, 1, 0, 17, 4, tzinfo=timezone.utc).timestamp() ) ), }, { 'nickname': '5678', 'cert-file': paths.HTTPD_CERT_FILE, 'key-file': paths.HTTPD_KEY_FILE, 'ca-name': 'IPA', 'template_profile': 'caIPAserviceCert', 'cert-storage': 'FILE', 'cert-postsave-command': template % 'restart_httpd', 'not-valid-after': ( int( ( datetime.now(timezone.utc) + timedelta(days=CERT_EXPIRATION_DAYS + 1) ).timestamp() ) ), }, ] class dbus_results: """Class to manage the results returned by dbus""" def __init__(self): self.requests = copy.deepcopy(pristine_cm_requests) def __iter__(self): for entry in self.requests: yield entry def __len__(self): return len(self.requests) def __getitem__(self, index): return self.requests[index] def append(self, entry): self.requests.append(entry) def remove(self, index): self.requests.remove(self.requests[index]) def __repr__(self): return repr(self.requests) cm_requests = [] class mock_property: def __init__(self, index): self.index = index def Get(self, object_path, name): """Always return a match""" if self.index is None: return None return cm_requests[self.index].get(name) class mock_dbus: """Create a fake dbus representation of a tracked certificate The index is used to look up values within the cm_requests list of known tracked certificates. """ def __init__(self, request_id): self.index = None for i, cm_request in enumerate(cm_requests): if request_id == cm_request.get('nickname'): self.index = i break self.prop_if = mock_property(self.index) self.obj_if = mock_obj_if(self.index) class mock_obj_if: def __init__(self, index): self.index = index def find_request_by_nickname(self, nickname): return None def get_requests(self): """Return list of request ids that dbus would have returned""" return [n.get('nickname') for n in cm_requests] def get_nickname(self): """Retrieve the certmonger CA nickname""" if self.index is None: return None return cm_requests[self.index].get('ca-name') def get_ca(self): """Return the CA name for the current request""" return cm_requests[self.index].get('nickname') class _certmonger: """An empty object, not needed directly for testing Needed to keep the real certmonger from blowing up. """ def __init__(self): self.obj_if = mock_obj_if(None) self.bus = None def create_mock_dbus(bus, parent, object_path, object_dbus_interface, parent_dbus_interface=None, property_interface=False): """Create a fake dbus object for a given path (request_id)""" return mock_dbus(object_path) def get_expected_requests(): """The list of requests known by the IPACertCheck plugin The list is copied and the nickname popped off to match the format that the check uses. nickname has two meanings in certmonger: the request id and the NSS nickname. """ requests = copy.deepcopy(pristine_cm_requests) for request in requests: try: request.pop('nickname') request.pop('not-valid-after') except KeyError: pass return requests def set_requests(add=None, remove=None): """Set the list of requests within a test""" global cm_requests cm_requests = dbus_results() if add is not None: cm_requests.append(add) if remove is not None: cm_requests.remove(remove) freeipa-healthcheck-0.10/tests/test_cluster_ruv.py000066400000000000000000000066031420053437700224500ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from base import BaseTest from util import capture_results from ipahealthcheck.core import config from ipaclustercheck.ipa.plugin import ClusterRegistry from ipaclustercheck.ipa.ruv import ClusterRUVCheck import clusterdata class RUVRegistry(ClusterRegistry): def load_files(self, dir): self.json = dir class Options: def __init__(self, data): self.data = data @property def dir(self): return self.data registry = RUVRegistry() class TestClusterRUV(BaseTest): def test_no_ruvs(self): """Single master test that has never created a replica This type of master will have no RUVs created at all. """ framework = object() registry.initialize(framework, config.Config, Options(clusterdata.ONE_MASTER)) f = ClusterRUVCheck(registry) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.kw.get('name') == 'dangling_ruv' assert result.kw.get('value') == 'No dangling RUVs found' result = self.results.results[1] assert result.kw.get('name') == 'dangling_csruv' assert result.kw.get('value') == 'No dangling CS RUVs found' def test_six_ruvs_ok(self): """Three master test with each having a CA, no dangling """ framework = object() registry.initialize(framework, config.Config, Options(clusterdata.THREE_MASTERS_OK)) f = ClusterRUVCheck(registry) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.kw.get('name') == 'dangling_ruv' assert result.kw.get('value') == 'No dangling RUVs found' result = self.results.results[1] assert result.kw.get('name') == 'dangling_csruv' assert result.kw.get('value') == 'No dangling CS RUVs found' def test_six_ruvs_ipa_bad(self): """Three master test with each having a CA, dangling IPA RUV """ framework = object() registry.initialize(framework, config.Config, Options(clusterdata.THREE_MASTERS_BAD_IPA_RUV)) f = ClusterRUVCheck(registry) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.kw.get('name') == 'dangling_ruv' assert result.kw.get('value') == '9' result = self.results.results[1] assert result.kw.get('name') == 'dangling_csruv' assert result.kw.get('value') == 'No dangling CS RUVs found' def test_six_ruvs_cs_bad(self): """Three master test with each having a CA, dangling CA RUV """ framework = object() registry.initialize(framework, config.Config, Options(clusterdata.THREE_MASTERS_BAD_CS_RUV)) f = ClusterRUVCheck(registry) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.kw.get('name') == 'dangling_ruv' assert result.kw.get('value') == 'No dangling RUVs found' result = self.results.results[1] assert result.kw.get('name') == 'dangling_csruv' assert result.kw.get('value') == '9' freeipa-healthcheck-0.10/tests/test_commands.py000066400000000000000000000032511420053437700216700ustar00rootroot00000000000000# # Copyright (C) 2021 FreeIPA Contributors see COPYING for license # import os from ipapython.ipautil import run import pytest def test_version(): """ Test the --version option """ output = run(['ipa-healthcheck', '--version'], env=os.environ) assert 'ipahealthcheck' in output.raw_output.decode('utf-8') @pytest.fixture def python_ipalib_dir(tmpdir): ipalib_dir = tmpdir.mkdir("ipalib") ipalib_dir.join("__init__.py").write("") def _make_facts(configured=None): if configured is None: module_text = "" elif isinstance(configured, bool): module_text = f"def is_ipa_configured(): return {configured}" else: raise TypeError( f"'configured' must be None or bool, got '{configured!r}'" ) ipalib_dir.join("facts.py").write(module_text) return str(tmpdir) return _make_facts def test_ipa_notinstalled(python_ipalib_dir, monkeypatch): """ Test ipa-healthcheck handles the missing IPA stuff """ monkeypatch.setenv("PYTHONPATH", python_ipalib_dir(configured=None)) output = run(["ipa-healthcheck"], raiseonerr=False, env=os.environ) assert output.returncode == 1 assert "IPA server is not installed" in output.raw_output.decode("utf-8") def test_ipa_unconfigured(python_ipalib_dir, monkeypatch): """ Test ipa-healthcheck handles the unconfigured IPA server """ monkeypatch.setenv("PYTHONPATH", python_ipalib_dir(configured=False)) output = run(["ipa-healthcheck"], raiseonerr=False, env=os.environ) assert output.returncode == 1 assert "IPA server is not configured" in output.raw_output.decode("utf-8") freeipa-healthcheck-0.10/tests/test_config.py000066400000000000000000000023231420053437700213330ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import tempfile import pytest from ipahealthcheck.core.config import read_config def test_config_no_section(): with tempfile.NamedTemporaryFile('w') as f: f.write('\n') f.flush() config = read_config(f.name) assert config is None def test_config_bad_format(): with tempfile.NamedTemporaryFile('w') as f: f.write('bad\n') config = read_config(f.name) f.flush() assert config is None def test_config_values(): with tempfile.NamedTemporaryFile('w') as f: f.write('[default]\nfoo = bar\n') f.flush() config = read_config(f.name) assert config.foo == 'bar' with pytest.raises(KeyError): config.bar # pylint: disable=pointless-statement def test_config_recursion(): with tempfile.NamedTemporaryFile('w') as f: f.write('[default]\nfoo = bar\n') f.flush() config = read_config(f.name) assert config.foo == 'bar' # The config dict is in the object assert isinstance(config._Config__d, dict) # But it isn't recursive try: config._Config__d['_Config__d'] except KeyError: pass freeipa-healthcheck-0.10/tests/test_core_files.py000066400000000000000000000123701420053437700222030ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import pwd import posix from ipahealthcheck.core.files import FileCheck from ipahealthcheck.core import constants from ipahealthcheck.core.plugin import Results from unittest.mock import patch from util import capture_results nobody = pwd.getpwnam('nobody') # Mock files to test files = (('foo', 'root', 'root', '0660'), ('bar', 'nobody', 'nobody', '0664'), ('baz', ('root', 'nobody'), ('root', 'nobody'), '0664'), ('fiz', ('root', 'bin'), ('root', 'bin'), '0664'),) def make_stat(mode=33200, uid=0, gid=0): """Return a mocked-up stat. The default is: mode = 0660 owner = root group = root """ # (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) return posix.stat_result((mode, 1, 42, 1, uid, gid, 0, 1, 1, 1,)) def get_results(results, type): """Pull out the type of results I want to look at: owner, group or mode""" my_results = Results() for r in results.results: kw = r.kw if kw.get('type') != type: continue my_results.add(r) return my_results @patch('os.stat') def test_files_owner(mock_stat): """ Test the file owner. Our mocked files want root, nobody, (root, nobody), (root, root). """ f = FileCheck() f.files = files mock_stat.return_value = make_stat() results = capture_results(f) my_results = get_results(results, 'owner') assert my_results.results[0].result == constants.SUCCESS assert my_results.results[1].result == constants.WARNING assert my_results.results[2].result == constants.SUCCESS assert my_results.results[3].result == constants.SUCCESS mock_stat.return_value = make_stat(uid=nobody.pw_uid) results = capture_results(f) my_results = get_results(results, 'owner') assert my_results.results[0].result == constants.WARNING assert my_results.results[0].kw.get('got') == 'nobody' assert my_results.results[0].kw.get('expected') == 'root' assert my_results.results[0].kw.get('type') == 'owner' assert my_results.results[1].result == constants.SUCCESS assert my_results.results[2].result == constants.SUCCESS assert my_results.results[3].result == constants.WARNING assert my_results.results[3].kw.get('got') == 'nobody' assert my_results.results[3].kw.get('expected') == 'root,bin' assert my_results.results[3].kw.get('type') == 'owner' assert my_results.results[3].kw.get('msg') == \ 'Ownership of fiz is nobody and should be one of root,bin' @patch('os.stat') def test_files_group(mock_stat): """ Test the file group. Our mocked files want root, nobody, (root, nobody), (root, root). """ f = FileCheck() f.files = files mock_stat.return_value = make_stat() results = capture_results(f) my_results = get_results(results, 'group') assert my_results.results[0].result == constants.SUCCESS assert my_results.results[1].result == constants.WARNING assert my_results.results[2].result == constants.SUCCESS assert my_results.results[3].result == constants.SUCCESS mock_stat.return_value = make_stat(gid=nobody.pw_gid) results = capture_results(f) my_results = get_results(results, 'group') assert my_results.results[0].result == constants.WARNING assert my_results.results[0].kw.get('got') == 'nobody' assert my_results.results[0].kw.get('expected') == 'root' assert my_results.results[0].kw.get('type') == 'group' assert my_results.results[1].result == constants.SUCCESS assert my_results.results[2].result == constants.SUCCESS assert my_results.results[3].result == constants.WARNING assert my_results.results[3].kw.get('got') == 'nobody' assert my_results.results[3].kw.get('expected') == 'root,bin' assert my_results.results[3].kw.get('type') == 'group' assert my_results.results[3].kw.get('msg') == \ 'Group of fiz is nobody and should be one of root,bin' @patch('os.stat') def test_files_mode(mock_stat): mock_stat.return_value = make_stat() f = FileCheck() f.files = files results = capture_results(f) my_results = get_results(results, 'mode') assert my_results.results[0].result == constants.SUCCESS assert my_results.results[1].result == constants.ERROR mock_stat.return_value = make_stat(mode=33152) results = capture_results(f) my_results = get_results(results, 'mode') assert my_results.results[0].result == constants.ERROR assert my_results.results[1].result == constants.ERROR mock_stat.return_value = make_stat(mode=33206) results = capture_results(f) my_results = get_results(results, 'mode') assert my_results.results[0].result == constants.WARNING assert my_results.results[1].result == constants.WARNING @patch('os.path.exists') def test_files_not_found(mock_exists): mock_exists.return_value = False f = FileCheck() f.files = files results = capture_results(f) for type in ('mode', 'group', 'owner'): my_results = get_results(results, type) assert len(my_results.results) == 4 for result in my_results.results: assert result.result == constants.SUCCESS assert result.kw.get('msg') == 'File does not exist' freeipa-healthcheck-0.10/tests/test_dogtag_ca.py000066400000000000000000000111121420053437700217720ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results, CAInstance, KRAInstance from base import BaseTest from ipahealthcheck.core import config, constants from ipahealthcheck.dogtag.plugin import registry from ipahealthcheck.dogtag.ca import DogtagCertsConfigCheck from unittest.mock import Mock, patch class mock_Cert: """Fake up a certificate. The contents are the NSS nickname of the certificate. """ def __init__(self, text): self.text = text def public_bytes(self, encoding): return self.text.encode('utf-8') class mock_CertDB: def __init__(self, trust): """A dict of nickname + NSSdb trust flags""" self.trust = trust def list_certs(self): return [(nickname, self.trust[nickname]) for nickname in self.trust] def get_cert_from_db(self, nickname): """Return the nickname. This will match the value of get_directive""" return mock_Cert(nickname) class TestCACerts(BaseTest): patches = { 'ipaserver.install.cainstance.CAInstance': Mock(return_value=CAInstance()), 'ipaserver.install.krainstance.KRAInstance': Mock(return_value=KRAInstance()), } @patch('ipahealthcheck.dogtag.ca.get_directive') @patch('ipaserver.install.certs.CertDB') def test_ca_certs_ok(self, mock_certdb, mock_directive): """Test what should be the standard case""" trust = { 'ocspSigningCert cert-pki-ca': 'u,u,u', 'subsystemCert cert-pki-ca': 'u,u,u', 'auditSigningCert cert-pki-ca': 'u,u,Pu', 'Server-Cert cert-pki-ca': 'u,u,u', 'caSigningCert cert-pki-ca': 'CT,C,C', 'transportCert cert-pki-kra': 'u,u,u', } mock_certdb.return_value = mock_CertDB(trust) mock_directive.side_effect = [name for name, nsstrust in trust.items()] framework = object() registry.initialize(framework, config.Config()) f = DogtagCertsConfigCheck(registry) self.results = capture_results(f) assert len(self.results) == 6 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.dogtag.ca' assert result.check == 'DogtagCertsConfigCheck' @patch('ipahealthcheck.dogtag.ca.get_directive') @patch('ipaserver.install.certs.CertDB') def test_cert_missing_from_file(self, mock_certdb, mock_directive): """Test a missing certificate. Note that if it is missing from the database then this check will not catch the error but it will be caught elsewhere. """ trust = { 'ocspSigningCert cert-pki-ca': 'u,u,u', 'subsystemCert cert-pki-ca': 'u,u,u', 'auditSigningCert cert-pki-ca': 'u,u,Pu', 'Server-Cert cert-pki-ca': 'u,u,u', 'caSigningCert cert-pki-ca': 'CT,,', 'transportCert cert-pki-kra': 'u,u,u', } # The 3rd cert won't match the results nicknames = [name for name, nsstrust in trust.items()] location = nicknames.index('auditSigningCert cert-pki-ca') nicknames[location] = 'NOT auditSigningCert cert-pki-ca' mock_certdb.return_value = mock_CertDB(trust) mock_directive.side_effect = nicknames framework = object() registry.initialize(framework, config.Config) f = DogtagCertsConfigCheck(registry) self.results = capture_results(f) num = len(self.results.results) for r in range(0, num): if r == 2: # skip the one that should be bad continue result = self.results.results[r] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.dogtag.ca' assert result.check == 'DogtagCertsConfigCheck' result = self.results.results[2] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.dogtag.ca' assert result.check == 'DogtagCertsConfigCheck' assert result.kw.get('key') == 'auditSigningCert cert-pki-ca' assert len(self.results) == 6 @patch('ipaserver.install.cainstance.CAInstance') def test_cacert_caless(self, mock_cainstance): """Nothing to check if the master is CALess""" mock_cainstance.return_value = CAInstance(False) framework = object() registry.initialize(framework, config) f = DogtagCertsConfigCheck(registry) self.results = capture_results(f) assert len(self.results) == 0 freeipa-healthcheck-0.10/tests/test_dogtag_connectivity.py000066400000000000000000000054621420053437700241400ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results, CAInstance from util import m_api from base import BaseTest from ipahealthcheck.core import constants, config from ipahealthcheck.dogtag.plugin import registry from ipahealthcheck.dogtag.ca import DogtagCertsConnectivityCheck from unittest.mock import Mock from ipalib.errors import CertificateOperationError class TestCAConnectivity(BaseTest): patches = { 'ipaserver.install.cainstance.CAInstance': Mock(return_value=CAInstance()), } def test_ca_connection_ok(self): """CA connectivity check when cert_show returns a valid value""" m_api.Command.cert_show.side_effect = None m_api.Command.cert_show.return_value = { u'result': {u'revoked': False} } framework = object() registry.initialize(framework, config.Config) f = DogtagCertsConnectivityCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.dogtag.ca' assert result.check == 'DogtagCertsConnectivityCheck' def test_ca_connection_cert_not_found(self): """CA connectivity check for a cert that doesn't exist""" m_api.Command.cert_show.reset_mock() m_api.Command.cert_show.side_effect = CertificateOperationError( message='Certificate operation cannot be completed: ' 'EXCEPTION (Certificate serial number 0x0 not found)' ) framework = object() registry.initialize(framework, config.Config) f = DogtagCertsConnectivityCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.dogtag.ca' assert result.check == 'DogtagCertsConnectivityCheck' def test_ca_connection_down(self): """CA connectivity check with the CA down""" m_api.Command.cert_show.side_effect = CertificateOperationError( message='Certificate operation cannot be completed: ' 'Unable to communicate with CMS (503)' ) framework = object() registry.initialize(framework, config.Config) f = DogtagCertsConnectivityCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.dogtag.ca' assert result.check == 'DogtagCertsConnectivityCheck' assert 'Unable to communicate' in result.kw.get('msg') freeipa-healthcheck-0.10/tests/test_ds_ruv.py000066400000000000000000000101531420053437700213700ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from base import BaseTest from unittest.mock import Mock from util import capture_results, m_api from ipahealthcheck.core import config, constants from ipahealthcheck.ds.plugin import registry from ipahealthcheck.ds.ruv import RUVCheck from ipalib import errors from ipapython.dn import DN from ipapython.ipaldap import LDAPClient, LDAPEntry class mock_ldap: SCOPE_BASE = 1 SCOPE_ONELEVEL = 2 SCOPE_SUBTREE = 4 def __init__(self, ldapentry): """Initialize the results that we will return from get_entries""" self.results = ldapentry self.index = 0 def get_entry(self, dn, attrs_list=None, time_limit=None, size_limit=None, get_effective_rights=False): if len(self.results) == 0: raise errors.NotFound(reason='test') self.index += 1 if self.results[self.index - 1] is None: raise errors.NotFound(reason='test') return self.results[self.index - 1] class mock_ldap_conn: def set_option(self, option, invalue): pass def search_s(self, base, scope, filterstr=None, attrlist=None, attrsonly=0): return tuple() class TestRUV(BaseTest): patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), } def create_entry(self, conn, dn, attrs): """Create an LDAPEntry object from the provided dn and attrs dn: DN() object attrs: dict of name/value pairs of LDAP attributes """ ldapentry = LDAPEntry(conn, dn) for attr, values in attrs.items(): ldapentry[attr] = values return ldapentry def test_no_ruvs(self): framework = object() registry.initialize(framework, config.Config) f = RUVCheck(registry) f.conn = mock_ldap(None) self.results = capture_results(f) assert len(self.results) == 0 def test_both_ruvs(self): fake_conn = LDAPClient('ldap://localhost', no_schema=True) entries = [] entries.append( self.create_entry(fake_conn, DN('dc=example,cn=mapping tree,cn=config'), {'nsds5ReplicaId': ['3']}) ) entries.append( self.create_entry(fake_conn, DN('o=ipaca,cn=mapping tree,cn=config'), {'nsds5ReplicaId': ['5']}) ) framework = object() registry.initialize(framework, config.Config) f = RUVCheck(registry) f.conn = mock_ldap(entries) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ds.ruv' assert result.check == 'RUVCheck' assert result.kw.get('key') == str(m_api.env.basedn) assert result.kw.get('ruv') == '3' result = self.results.results[1] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ds.ruv' assert result.check == 'RUVCheck' assert result.kw.get('key') == 'o=ipaca' assert result.kw.get('ruv') == '5' def test_one_ruvs(self): fake_conn = LDAPClient('ldap://localhost', no_schema=True) entries = [] entries.append( self.create_entry(fake_conn, DN('dc=example,cn=mapping tree,cn=config'), {'nsds5ReplicaId': ['3']}) ) entries.append(None) framework = object() registry.initialize(framework, config.Config) f = RUVCheck(registry) f.conn = mock_ldap(entries) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ds.ruv' assert result.check == 'RUVCheck' assert result.kw.get('key') == str(m_api.env.basedn) assert result.kw.get('ruv') == '3' freeipa-healthcheck-0.10/tests/test_init.py000066400000000000000000000014261420053437700210340ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # import argparse from ipahealthcheck.core.output import output_registry class RunChecks: def run_healthcheck(self): options = argparse.Namespace(check=None, debug=False, indent=2, list_sources=False, output_file=None, output_type='json', source=None, verbose=False) for out in output_registry.plugins: if out.__name__.lower() == options.output_type: out(options) break def test_run_healthcheck(): """ Test typical initialization in run_healthcheck (based ok pki-healthcheck) """ run = RunChecks() run.run_healthcheck() freeipa-healthcheck-0.10/tests/test_ipa_agent.py000066400000000000000000000302531420053437700220200ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from base import BaseTest from unittest.mock import Mock, patch from util import capture_results, CAInstance, KRAInstance from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPARAAgent, IPAKRAAgent from ipalib import errors from ipapython.dn import DN from ipapython.ipaldap import LDAPClient, LDAPEntry from ipaplatform.paths import paths from ldap import OPT_X_SASL_SSF_MIN class IPACertificate: def __init__(self, serial_number=1): self.subject = 'CN=RA AGENT' self.issuer = 'CN=ISSUER' self.serial_number = serial_number class mock_ldap: SCOPE_BASE = 1 SCOPE_ONELEVEL = 2 SCOPE_SUBTREE = 4 def __init__(self, ldapentry): """Initialize the results that we will return from get_entries""" self.results = ldapentry def get_entries(self, base_dn, scope=SCOPE_SUBTREE, filter=None, attrs_list=None, get_effective_rights=False, **kwargs): if self.results is None: raise errors.NotFound(reason='test') return self.results class mock_ldap_conn: def set_option(self, option, invalue): pass def get_option(self, option): if option == OPT_X_SASL_SSF_MIN: return 256 return None def search_s(self, base, scope, filterstr=None, attrlist=None, attrsonly=0): return tuple() class TestNSSAgent(BaseTest): cert = IPACertificate() patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), 'ipaserver.install.cainstance.CAInstance': Mock(return_value=CAInstance()), 'ipalib.x509.load_certificate_from_file': Mock(return_value=cert), } def test_nss_agent_ok(self): attrs = dict( description=['2;1;CN=ISSUER;CN=RA AGENT'], usercertificate=[self.cert], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config()) f = IPARAAgent(registry) f.conn = mock_ldap([ldapentry]) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPARAAgent' def test_nss_agent_no_description(self): attrs = dict( usercertificate=[self.cert], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config()) f = IPARAAgent(registry) f.conn = mock_ldap([ldapentry]) self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert 'description' in result.kw.get('msg') @patch('ipalib.x509.load_certificate_from_file') def test_nss_agent_load_failure(self, mock_load_cert): mock_load_cert.side_effect = IOError('test') framework = object() registry.initialize(framework, config.Config()) f = IPARAAgent(registry) self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('error') == 'test' def test_nss_agent_no_entry_found(self): framework = object() registry.initialize(framework, config.Config()) f = IPARAAgent(registry) f.conn = mock_ldap(None) # None == NotFound self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('msg') == 'RA agent not found in LDAP' def test_nss_agent_too_many(self): attrs = dict( description=['2;1;CN=ISSUER;CN=RA AGENT'], usercertificate=[self.cert], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values ldapentry2 = LDAPEntry(fake_conn, DN('uid=ipara2,ou=people,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config()) f = IPARAAgent(registry) f.conn = mock_ldap([ldapentry, ldapentry2]) self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('found') == 2 def test_nss_agent_nonmatching_cert(self): cert2 = IPACertificate(2) attrs = dict( description=['2;1;CN=ISSUER;CN=RA AGENT'], usercertificate=[cert2], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config()) f = IPARAAgent(registry) f.conn = mock_ldap([ldapentry]) self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('certfile') == paths.RA_AGENT_PEM assert result.kw.get('dn') == 'uid=ipara,ou=people,o=ipaca' def test_nss_agent_multiple_certs(self): cert2 = IPACertificate(2) attrs = dict( description=['2;1;CN=ISSUER;CN=RA AGENT'], usercertificate=[cert2, self.cert], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipara,ou=people,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) f = IPARAAgent(registry) f.conn = mock_ldap([ldapentry]) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPARAAgent' class TestKRAAgent(BaseTest): cert = IPACertificate() patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), 'ipaserver.install.krainstance.KRAInstance': Mock(return_value=KRAInstance()), 'ipalib.x509.load_certificate_from_file': Mock(return_value=cert), } def test_kra_agent_ok(self): attrs = dict( description=['2;1;CN=ISSUER;CN=RA AGENT'], usercertificate=[self.cert], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipakra,ou=people,o=kra,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config()) f = IPAKRAAgent(registry) f.conn = mock_ldap([ldapentry]) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPAKRAAgent' def test_kra_agent_no_description(self): attrs = dict( usercertificate=[self.cert], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipakra,ou=people,o=kra,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config()) f = IPAKRAAgent(registry) f.conn = mock_ldap([ldapentry]) self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert 'description' in result.kw.get('msg') @patch('ipalib.x509.load_certificate_from_file') def test_kra_agent_load_failure(self, mock_load_cert): mock_load_cert.side_effect = IOError('test') framework = object() registry.initialize(framework, config.Config()) f = IPAKRAAgent(registry) self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('error') == 'test' def test_kra_agent_no_entry_found(self): framework = object() registry.initialize(framework, config.Config()) f = IPAKRAAgent(registry) f.conn = mock_ldap(None) # None == NotFound self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('msg') == 'KRA agent not found in LDAP' def test_kra_agent_too_many(self): attrs = dict( description=['2;1;CN=ISSUER;CN=RA AGENT'], usercertificate=[self.cert], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipakra,ou=people,o=kra,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values ldapentry2 = LDAPEntry(fake_conn, DN('uid=ipakra,ou=people,o=kra,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config()) f = IPAKRAAgent(registry) f.conn = mock_ldap([ldapentry, ldapentry2]) self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('found') == 2 def test_kra_agent_nonmatching_cert(self): cert2 = IPACertificate(2) attrs = dict( description=['2;1;CN=ISSUER;CN=RA AGENT'], usercertificate=[cert2], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipakra,ou=people,o=kra,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config()) f = IPAKRAAgent(registry) f.conn = mock_ldap([ldapentry]) self.results = capture_results(f) result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('certfile') == paths.RA_AGENT_PEM assert result.kw.get('dn') == 'uid=ipakra,ou=people,o=kra,o=ipaca' def test_kra_agent_multiple_certs(self): cert2 = IPACertificate(2) attrs = dict( description=['2;1;CN=ISSUER;CN=RA AGENT'], usercertificate=[cert2, self.cert], ) fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('uid=ipakra,ou=people,o=kra,o=ipaca')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) f = IPAKRAAgent(registry) f.conn = mock_ldap([ldapentry]) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPAKRAAgent' freeipa-healthcheck-0.10/tests/test_ipa_cert_match.py000066400000000000000000000253241420053437700230360ustar00rootroot00000000000000# # Copyright (C) 2021 FreeIPA Contributors see COPYING for license # from util import capture_results, m_api, CAInstance, KRAInstance from base import BaseTest from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPACertMatchCheck from ipahealthcheck.ipa.certs import IPADogtagCertsMatchCheck from unittest.mock import Mock, patch from ipalib import errors from ipapython.dn import DN from ipapython.ipaldap import LDAPClient, LDAPEntry class IPACertificate: def __init__(self, serial_number=1): self.serial_number = serial_number def __eq__(self, other): return self.serial_number == other.serial_number def __hash__(self): return hash(self.serial_number) class mock_ldap: SCOPE_BASE = 1 SCOPE_ONELEVEL = 2 SCOPE_SUBTREE = 4 def __init__(self, entries): """Initialize the results that we will return from get_entry""" self.results = {entry.dn: entry for entry in entries} def get_entry(self, dn, attrs_list=None, time_limit=None, size_limit=None, get_effective_rights=False): if self.results is None: raise errors.NotFound(reason='test') return self.results[dn] def get_entries(self, base_dn, scope=SCOPE_SUBTREE, filter=None, attrs_list=None, get_effective_rights=False, **kwargs): if self.results is None: raise errors.NotFound(reason='test') return self.results.values() class mock_ldap_conn: def set_option(self, option, invalue): pass def search_s(self, base, scope, filterstr=None, attrlist=None, attrsonly=0): return tuple() class mock_CertDB: def __init__(self, trust): """A dict of nickname + NSSdb trust flags""" self.trust = trust self.secdir = '/foo/bar/testdir' def get_cert_from_db(self, nickname): if nickname not in self.trust.keys(): raise errors.NotFound(reason='test') return IPACertificate() def run_certutil(self, args, capture_output): class RunResult: def __init__(self, output): self.raw_output = output return RunResult(b'test output') class TestIPACertMatch(BaseTest): patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()) } @patch('ipalib.x509.load_certificate_list_from_file') @patch('ipaserver.install.certs.CertDB') def test_certs_match_ok(self, mock_certdb, mock_load_cert): """ Ensure match check is ok""" fake_conn = LDAPClient('ldap://localhost', no_schema=True) cacertentry = LDAPEntry(fake_conn, DN('cn=%s IPA CA' % m_api.env.realm, 'cn=certificates,cn=ipa,cn=etc', m_api.env.basedn), CACertificate=[IPACertificate()]) trust = { ('%s IPA CA' % m_api.env.realm): 'u,u,u' } mock_certdb.return_value = mock_CertDB(trust) mock_load_cert.return_value = [IPACertificate()] framework = object() registry.initialize(framework, config.Config()) f = IPACertMatchCheck(registry) f.conn = mock_ldap([cacertentry]) self.results = capture_results(f) assert len(self.results) == 3 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertMatchCheck' @patch('ipalib.x509.load_certificate_list_from_file') @patch('ipaserver.install.certs.CertDB') def test_etc_cacert_mismatch(self, mock_certdb, mock_load_cert): """ Test mismatch with /etc/ipa/ca.crt """ fake_conn = LDAPClient('ldap://localhost', no_schema=True) cacertentry = LDAPEntry(fake_conn, DN('cn=%s IPA CA' % m_api.env.realm, 'cn=certificates,cn=ipa,cn=etc', m_api.env.basedn), CACertificate=[IPACertificate()]) trust = { ('%s IPA CA' % m_api.env.realm): 'u,u,u' } mock_certdb.return_value = mock_CertDB(trust) mock_load_cert.return_value = [IPACertificate(serial_number=2)] framework = object() registry.initialize(framework, config.Config()) f = IPACertMatchCheck(registry) f.conn = mock_ldap([cacertentry]) self.results = capture_results(f) assert len(self.results) == 3 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertMatchCheck' @patch('ipaserver.install.cainstance.CAInstance') def test_cacert_caless(self, mock_cainstance): """Nothing to check if the master is CALess""" mock_cainstance.return_value = CAInstance(False) framework = object() registry.initialize(framework, config) f = IPACertMatchCheck(registry) self.results = capture_results(f) assert len(self.results) == 0 class TestIPADogtagCertMatch(BaseTest): patches = { 'ipaserver.install.krainstance.KRAInstance': Mock(return_value=KRAInstance()), } @patch('ipaserver.install.certs.CertDB') def test_certs_match_ok(self, mock_certdb): """ Ensure match check is ok""" fake_conn = LDAPClient('ldap://localhost', no_schema=True) pkidbentry = LDAPEntry(fake_conn, DN('uid=pkidbuser,ou=people,o=ipaca'), userCertificate=[IPACertificate()], subjectName=['test']) casignentry = LDAPEntry(fake_conn, DN('cn=%s IPA CA' % m_api.env.realm, 'cn=certificates,cn=ipa,cn=etc', m_api.env.basedn), CACertificate=[IPACertificate()], userCertificate=[IPACertificate()], subjectName=['test']) ldap_entries = [pkidbentry, casignentry] trust = { 'ocspSigningCert cert-pki-ca': 'u,u,u', 'caSigningCert cert-pki-ca': 'u,u,u', 'subsystemCert cert-pki-ca': 'u,u,u', 'auditSigningCert cert-pki-ca': 'u,u,Pu', 'Server-Cert cert-pki-ca': 'u,u,u', 'transportCert cert-pki-kra': 'u,u,u', 'storageCert cert-pki-kra': 'u,u,u', 'auditSigningCert cert-pki-kra': 'u,u,Pu', } dogtag_entries_subjects = ( 'CN=OCSP Subsystem,O=%s' % m_api.env.realm, 'CN=CA Subsystem,O=%s' % m_api.env.realm, 'CN=CA Audit,O=%s' % m_api.env.realm, 'CN=%s,O=%s' % (m_api.env.host, m_api.env.realm), 'CN=KRA Transport Certificate,O=%s' % m_api.env.realm, 'CN=KRA Storage Certificate,O=%s' % m_api.env.realm, 'CN=KRA Audit,O=%s' % m_api.env.realm, ) for i, subject in enumerate(dogtag_entries_subjects): entry = LDAPEntry(fake_conn, DN('cn=%i,ou=certificateRepository' % i, 'ou=ca,o=ipaca'), userCertificate=[IPACertificate()], subjectName=[subject]) ldap_entries.append(entry) mock_certdb.return_value = mock_CertDB(trust) framework = object() registry.initialize(framework, config.Config()) f = IPADogtagCertsMatchCheck(registry) f.conn = mock_ldap(ldap_entries) self.results = capture_results(f) assert len(self.results) == 3 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPADogtagCertsMatchCheck' @patch('ipaserver.install.certs.CertDB') def test_certs_mismatch(self, mock_certdb): """ Ensure mismatches are detected""" fake_conn = LDAPClient('ldap://localhost', no_schema=True) pkidbentry = LDAPEntry(fake_conn, DN('uid=pkidbuser,ou=people,o=ipaca'), userCertificate=[IPACertificate( serial_number=2 )], subjectName=['test']) casignentry = LDAPEntry(fake_conn, DN('cn=%s IPA CA' % m_api.env.realm, 'cn=certificates,cn=ipa,cn=etc', m_api.env.basedn), CACertificate=[IPACertificate()], userCertificate=[IPACertificate()], subjectName=['test']) ldap_entries = [pkidbentry, casignentry] trust = { 'ocspSigningCert cert-pki-ca': 'u,u,u', 'caSigningCert cert-pki-ca': 'u,u,u', 'subsystemCert cert-pki-ca': 'u,u,u', 'auditSigningCert cert-pki-ca': 'u,u,Pu', 'Server-Cert cert-pki-ca': 'u,u,u', 'transportCert cert-pki-kra': 'u,u,u', 'storageCert cert-pki-kra': 'u,u,u', 'auditSigningCert cert-pki-kra': 'u,u,Pu', } dogtag_entries_subjects = ( 'CN=OCSP Subsystem,O=%s' % m_api.env.realm, 'CN=CA Subsystem,O=%s' % m_api.env.realm, 'CN=CA Audit,O=%s' % m_api.env.realm, 'CN=%s,O=%s' % (m_api.env.host, m_api.env.realm), 'CN=KRA Transport Certificate,O=%s' % m_api.env.realm, 'CN=KRA Storage Certificate,O=%s' % m_api.env.realm, 'CN=KRA Audit,O=%s' % m_api.env.realm, ) for i, subject in enumerate(dogtag_entries_subjects): entry = LDAPEntry(fake_conn, DN('cn=%i,ou=certificateRepository' % i, 'ou=ca,o=ipaca'), userCertificate=[IPACertificate()], subjectName=[subject]) ldap_entries.append(entry) mock_certdb.return_value = mock_CertDB(trust) framework = object() registry.initialize(framework, config.Config()) f = IPADogtagCertsMatchCheck(registry) f.conn = mock_ldap(ldap_entries) self.results = capture_results(f) assert len(self.results) == 3 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPADogtagCertsMatchCheck' freeipa-healthcheck-0.10/tests/test_ipa_certfile_expiration.py000066400000000000000000000072521420053437700247640ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results from base import BaseTest from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPACertfileExpirationCheck from unittest.mock import Mock, patch from mock_certmonger import create_mock_dbus, _certmonger from mock_certmonger import ( get_expected_requests, set_requests, CERT_EXPIRATION_DAYS, ) from datetime import datetime, timedelta class IPACertificate: def __init__(self, not_valid_after, serial_number=1): self.subject = 'CN=RA AGENT' self.issuer = 'CN=ISSUER' self.serial_number = serial_number self.not_valid_after = not_valid_after class TestIPACertificateFile(BaseTest): patches = { 'ipahealthcheck.ipa.certs.get_expected_requests': Mock(return_value=get_expected_requests()), 'ipalib.install.certmonger._cm_dbus_object': Mock(side_effect=create_mock_dbus), 'ipalib.install.certmonger._certmonger': Mock(return_value=_certmonger()), } @patch('ipalib.x509.load_certificate_from_file') def test_certfile_expiration(self, mock_load_cert): set_requests(remove=1) cert = IPACertificate(not_valid_after=datetime.utcnow() + timedelta(days=CERT_EXPIRATION_DAYS)) mock_load_cert.return_value = cert framework = object() registry.initialize(framework, config.Config) f = IPACertfileExpirationCheck(registry) f.config.cert_expiration_days = '28' self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertfileExpirationCheck' assert result.kw.get('key') == '1234' @patch('ipalib.x509.load_certificate_from_file') def test_certfile_expiration_warning(self, mock_load_cert): set_requests(remove=1) cert = IPACertificate(not_valid_after=datetime.utcnow() + timedelta(days=7)) mock_load_cert.return_value = cert framework = object() registry.initialize(framework, config.Config) f = IPACertfileExpirationCheck(registry) f.config.cert_expiration_days = str(CERT_EXPIRATION_DAYS) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.WARNING assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertfileExpirationCheck' assert result.kw.get('key') == '1234' assert result.kw.get('days') == 6 @patch('ipalib.x509.load_certificate_from_file') def test_certfile_expiration_expired(self, mock_load_cert): set_requests(remove=1) cert = IPACertificate(not_valid_after=datetime.utcnow() + timedelta(days=-100)) mock_load_cert.return_value = cert framework = object() registry.initialize(framework, config.Config) f = IPACertfileExpirationCheck(registry) f.config.cert_expiration_days = str(CERT_EXPIRATION_DAYS) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertfileExpirationCheck' assert result.kw.get('key') == '1234' assert 'expiration_date' in result.kw freeipa-healthcheck-0.10/tests/test_ipa_certmonger_ca.py000066400000000000000000000037251420053437700235360ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results, CAInstance from base import BaseTest from ipahealthcheck.core import constants, config from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPACertmongerCA from unittest.mock import Mock, patch class TestCertmonger(BaseTest): patches = { 'ipaserver.install.cainstance.CAInstance': Mock(return_value=CAInstance()), } @patch('ipahealthcheck.ipa.certs.IPACertmongerCA.find_ca') def test_certmogner_ok(self, mock_find_ca): mock_find_ca.side_effect = [ 'IPA', 'dogtag-ipa-ca-renew-agent', 'dogtag-ipa-ca-renew-agent-reuse' ] framework = object() registry.initialize(framework, config.Config) f = IPACertmongerCA(registry) self.results = capture_results(f) assert len(self.results) == 3 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertmongerCA' @patch('ipahealthcheck.ipa.certs.IPACertmongerCA.find_ca') def test_certmogner_missing(self, mock_find_ca): mock_find_ca.side_effect = [ 'IPA', 'dogtag-ipa-ca-renew-agent', ] framework = object() registry.initialize(framework, config.Config) f = IPACertmongerCA(registry) self.results = capture_results(f) assert len(self.results) == 3 for r in range(0, 1): result = self.results.results[r] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertmongerCA' assert self.results.results[2].result == constants.ERROR assert self.results.results[2].kw.get('key') == \ 'dogtag-ipa-ca-renew-agent-reuse' freeipa-healthcheck-0.10/tests/test_ipa_dna.py000066400000000000000000000063361420053437700214710ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from base import BaseTest from unittest.mock import patch from util import capture_results from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.dna import IPADNARangeCheck class mock_ReplicationManager: def __init__(self, realm=None, host=None, start=None, max=None, next=None, next_max=None): self.start = start self.max = max self.next = next self.next_max = next_max def get_DNA_range(self, host): return self.start, self.max def get_DNA_next_range(self, host): return self.next, self.next_max class TestDNARange(BaseTest): @patch('ipaserver.install.replication.ReplicationManager') def test_dnarange_set(self, mock_manager): mock_manager.return_value = mock_ReplicationManager(start=1, max=100) framework = object() registry.initialize(framework, config.Config) f = IPADNARangeCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.dna' assert result.check == 'IPADNARangeCheck' assert result.kw.get('range_start') == 1 assert result.kw.get('range_max') == 100 assert result.kw.get('next_start') == 0 assert result.kw.get('next_max') == 0 @patch('ipaserver.install.replication.ReplicationManager') def test_dnarange_noset(self, mock_manager): mock_manager.return_value = mock_ReplicationManager() framework = object() registry.initialize(framework, config.Config) f = IPADNARangeCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.WARNING assert result.source == 'ipahealthcheck.ipa.dna' assert result.check == 'IPADNARangeCheck' assert result.kw.get('range_start') == 0 assert result.kw.get('range_max') == 0 assert result.kw.get('next_start') == 0 assert result.kw.get('next_max') == 0 @patch('ipaserver.install.replication.ReplicationManager') def test_dnarange_next(self, mock_manager): mock_manager.return_value = mock_ReplicationManager(start=1, max=100, next=101, next_max=200) framework = object() registry.initialize(framework, config.Config) f = IPADNARangeCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.dna' assert result.check == 'IPADNARangeCheck' assert result.kw.get('range_start') == 1 assert result.kw.get('range_max') == 100 assert result.kw.get('next_start') == 101 assert result.kw.get('next_max') == 200 freeipa-healthcheck-0.10/tests/test_ipa_dns.py000066400000000000000000000677621420053437700215250ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import re from dns import ( rdata, rdataclass, rdatatype, message, rrset, version, ) from dns.resolver import Answer from base import BaseTest from util import capture_results, m_api from unittest.mock import patch from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.idns import IPADNSSystemRecordsCheck from ipapython.dnsutil import DNSName from ipaserver.dns_data_management import ( IPA_DEFAULT_MASTER_SRV_REC, IPA_DEFAULT_ADTRUST_SRV_REC ) try: # pylint: disable=unused-import from ipaserver.dns_data_management import IPA_DEFAULT_MASTER_URI_REC # noqa except ImportError: has_uri_support = False else: has_uri_support = True try: # pylint: disable=unused-import from ipaserver.install.installutils import resolve_rrsets_nss # noqa: F401 # pylint: enable=unused-import except ImportError: resolve_rrsets_import = 'ipaserver.dns_data_management.resolve_rrsets' else: resolve_rrsets_import = 'ipaserver.install.installutils.resolve_rrsets_nss' def add_srv_records(qname, port_map, priority=0, weight=100): rdlist = [] for _unused, port in port_map: answerlist = [] for host in qname: hostname = DNSName(host) rd = rdata.from_text( rdataclass.IN, rdatatype.SRV, '{0} {1} {2} {3}'.format( priority, weight, port, hostname.make_absolute() ) ) answerlist.append(rd) rdlist.append(answerlist) return rdlist def resolve_rrsets(fqdn, rdtypes): """ Return an A record for the hostname in an RRset type in a list. """ rset = [] for rdtype in rdtypes: rlist = rrset.from_text_list(fqdn, 86400, rdataclass.IN, rdtype, gen_addrs(rdtype, 1)) rset.append(rlist) return rset def query_srv(qname, ad_records=False): """ Return a SRV for each service IPA cares about for all the hosts. This is pre-generated as a side-effect for each test. """ rdlist = add_srv_records(qname, IPA_DEFAULT_MASTER_SRV_REC) if ad_records: rdlist.extend(add_srv_records(qname, IPA_DEFAULT_ADTRUST_SRV_REC)) return rdlist def query_uri(hosts): """ Return a list containing two answers, one for each uri type """ answers = [] if version.MAJOR < 2 or (version.MAJOR == 2 and version.MINOR == 0): m = message.Message() elif version.MAJOR == 2 and version.MINOR > 0: m = message.QueryMessage() # pylint: disable=E1101 m = message.make_response(m) # pylint: disable=E1101 rdtype = rdatatype.URI for name in ('_kerberos.', '_kpasswd.'): qname = DNSName(name + m_api.env.domain) qname = qname.make_absolute() if version.MAJOR < 2: # pylint: disable=unexpected-keyword-arg answer = Answer(qname, rdataclass.IN, rdtype, m, raise_on_no_answer=False) # pylint: enable=unexpected-keyword-arg else: if version.MAJOR == 2 and version.MINOR > 0: question = rrset.RRset(qname, rdataclass.IN, rdtype) m.question = [question] answer = Answer(qname, rdataclass.IN, rdtype, m) rl = [] for host in hosts: rlist = rrset.from_text_list( qname, 86400, rdataclass.IN, rdatatype.URI, ['0 100 "krb5srv:m:tcp:%s."' % host, '0 100 "krb5srv:m:udp:%s."' % host, ] ) rl.extend(rlist) answer.rrset = rl answers.append(answer) return answers def gen_addrs(rdtype=rdatatype.A, num=1): """Generate sequential IP addresses for the ipa-ca A record lookup""" ips = [] if rdtype == rdatatype.A: ip_template = '192.168.0.%d' if rdtype == rdatatype.AAAA: ip_template = '2001:db8:1::%d' for i in range(num): ips.append(ip_template % (i + 1)) return ips def fake_query(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN, count=1, fake_txt=False): """Fake a DNS query, returning count responses to the request Three kinds of lookups are faked: 1. A query for A/AAAA records for a service will return the count as requested in the test. This simulates lookups for the ipa-ca A/AAAA record. To force a difference in responses one can vary the count. 2. TXT queries will return the Kerberos realm fake_txt will set an invalid Kerberos realm entry to provoke a warning. """ if version.MAJOR < 2 or (version.MAJOR == 2 and version.MINOR == 0): m = message.Message() elif version.MAJOR == 2 and version.MINOR > 0: m = message.QueryMessage() # pylint: disable=E1101 m = message.make_response(m) # pylint: disable=E1101 if rdtype in (rdatatype.A, rdatatype.AAAA): fqdn = DNSName(qname) fqdn = fqdn.make_absolute() if version.MAJOR < 2: # pylint: disable=unexpected-keyword-arg answers = Answer(fqdn, rdataclass.IN, rdtype, m, raise_on_no_answer=False) # pylint: enable=unexpected-keyword-arg else: if version.MAJOR == 2 and version.MINOR > 0: question = rrset.RRset(fqdn, rdataclass.IN, rdtype) m.question = [question] answers = Answer(fqdn, rdataclass.IN, rdtype, m) rlist = rrset.from_text_list(fqdn, 86400, rdataclass.IN, rdtype, gen_addrs(rdtype, count)) answers.rrset = rlist elif rdtype == rdatatype.TXT: if fake_txt: realm = 'FAKE_REALM' else: realm = m_api.env.realm qname = DNSName('_kerberos.' + m_api.env.domain) qname = qname.make_absolute() if version.MAJOR < 2: # pylint: disable=unexpected-keyword-arg answers = Answer(qname, rdataclass.IN, rdatatype.TXT, m, raise_on_no_answer=False) # pylint: enable=unexpected-keyword-arg else: if version.MAJOR == 2 and version.MINOR > 0: question = rrset.RRset(qname, rdataclass.IN, rdtype) m.question = [question] answers = Answer(qname, rdataclass.IN, rdatatype.TXT, m) rlist = rrset.from_text_list(qname, 86400, rdataclass.IN, rdatatype.TXT, [realm]) answers.rrset = rlist return answers # Helpers to generate an appropriate number of A records for the # ipa-ca and Kerberos realm responses. Optionally return a bogus # TXT record. def fake_query_one(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN, count=1): return fake_query(qname, rdtype, rdclass, count) def fake_query_two(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN, count=2): return fake_query(qname, rdtype, rdclass, count) def fake_query_three(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN, count=3): return fake_query(qname, rdtype, rdclass, count) def fake_query_one_txt(qname, rdtype=rdatatype.A, rdclass=rdataclass.IN, count=1): return fake_query(qname, rdtype, rdclass, count, fake_txt=True) def get_results_by_severity(results, severity): """Return the results with a matching severity""" new_results = [] for result in results: if result.result == severity: new_results.append(result) return new_results class TestDNSSystemRecords(BaseTest): """Test that the SRV records checks are working properly The intention was to not override IPASystemRecords since this is the core mechanism that IPA uses to determine what recoreds should exist. Instead the DNS lookups are managed. This is done in two ways: 1. The query_srv() override returns the set of configured servers for each type of SRV record. 2. fake_query() overrides ipahealthcheck.ipa.idns.resolve to simulate A, AAAA and TXT record lookups. """ @patch(resolve_rrsets_import) @patch('ipapython.dnsutil.query_srv') @patch('ipahealthcheck.ipa.idns.query_uri') @patch('ipahealthcheck.ipa.idns.resolve') def test_dnsrecords_single(self, mock_query, mock_query_uri, mock_query_srv, mock_rrset): """Test single CA master, all SRV records""" mock_query.side_effect = fake_query_one mock_query_srv.side_effect = query_srv([m_api.env.host]) mock_query_uri.side_effect = query_uri([m_api.env.host]) mock_rrset.side_effect = [ resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)) ] m_api.Command.server_find.side_effect = [{ 'result': [ { 'cn': [m_api.env.host], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, ] }] framework = object() registry.initialize(framework, config.Config) f = IPADNSSystemRecordsCheck(registry) self.results = capture_results(f) if has_uri_support: expected = 14 else: expected = 10 assert len(self.results) == expected for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.idns' assert result.check == 'IPADNSSystemRecordsCheck' @patch(resolve_rrsets_import) @patch('ipapython.dnsutil.query_srv') @patch('ipahealthcheck.ipa.idns.query_uri') @patch('ipahealthcheck.ipa.idns.resolve') def test_dnsrecords_two(self, mock_query, mock_query_uri, mock_query_srv, mock_rrset): """Test two CA masters, all SRV records""" mock_query_srv.side_effect = query_srv([ m_api.env.host, 'replica.' + m_api.env.domain ]) mock_query_uri.side_effect = query_uri([ m_api.env.host, 'replica.' + m_api.env.domain ]) mock_query.side_effect = fake_query_two mock_rrset.side_effect = [ resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), ] m_api.Command.server_find.side_effect = [{ 'result': [ { 'cn': [m_api.env.host], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica.' + m_api.env.domain], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, ] }] framework = object() registry.initialize(framework, config.Config) f = IPADNSSystemRecordsCheck(registry) self.results = capture_results(f) if has_uri_support: expected = 27 else: expected = 19 assert len(self.results) == expected for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.idns' assert result.check == 'IPADNSSystemRecordsCheck' @patch(resolve_rrsets_import) @patch('ipapython.dnsutil.query_srv') @patch('ipahealthcheck.ipa.idns.query_uri') @patch('ipahealthcheck.ipa.idns.resolve') def test_dnsrecords_three(self, mock_query, mock_query_uri, mock_query_srv, mock_rrset): """Test three CA masters, all SRV records""" mock_query_srv.side_effect = query_srv([ m_api.env.host, 'replica.' + m_api.env.domain, 'replica2.' + m_api.env.domain ]) mock_query_uri.side_effect = query_uri([ m_api.env.host, 'replica.' + m_api.env.domain, 'replica2.' + m_api.env.domain ]) mock_query.side_effect = fake_query_three mock_rrset.side_effect = [ resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica2.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), ] m_api.Command.server_find.side_effect = [{ 'result': [ { 'cn': [m_api.env.host], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica.' + m_api.env.domain], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica2.' + m_api.env.domain], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, ] }] framework = object() registry.initialize(framework, config.Config) f = IPADNSSystemRecordsCheck(registry) self.results = capture_results(f) if has_uri_support: expected = 40 else: expected = 28 assert len(self.results) == expected for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.idns' assert result.check == 'IPADNSSystemRecordsCheck' @patch(resolve_rrsets_import) @patch('ipapython.dnsutil.query_srv') @patch('ipahealthcheck.ipa.idns.query_uri') @patch('ipahealthcheck.ipa.idns.resolve') def test_dnsrecords_three_mixed(self, mock_query, mock_query_uri, mock_query_srv, mock_rrset): """Test three masters, only one with a CA, all SRV records""" mock_query_srv.side_effect = query_srv([ m_api.env.host, 'replica.' + m_api.env.domain, 'replica2.' + m_api.env.domain ]) mock_query_uri.side_effect = query_uri([ m_api.env.host, 'replica.' + m_api.env.domain, 'replica2.' + m_api.env.domain ]) mock_query.side_effect = fake_query_one mock_rrset.side_effect = [ resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica2.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)) ] m_api.Command.server_find.side_effect = [{ 'result': [ { 'cn': [m_api.env.host], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica.' + m_api.env.domain], 'enabled_role_servrole': [ 'IPA master' ], }, { 'cn': ['replica2.' + m_api.env.domain], 'enabled_role_servrole': [ 'IPA master' ], }, ] }] framework = object() registry.initialize(framework, config.Config) f = IPADNSSystemRecordsCheck(registry) self.results = capture_results(f) if has_uri_support: expected = 36 else: expected = 24 assert len(self.results) == expected for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.idns' @patch(resolve_rrsets_import) @patch('ipapython.dnsutil.query_srv') @patch('ipahealthcheck.ipa.idns.query_uri') @patch('ipahealthcheck.ipa.idns.resolve') def test_dnsrecords_missing_server(self, mock_query, mock_query_uri, mock_query_srv, mock_rrset): """Drop one of the masters from query_srv This will simulate missing SRV records and cause a number of warnings to be thrown. """ mock_query_srv.side_effect = query_srv([ m_api.env.host, 'replica.' + m_api.env.domain # replica2 is missing ]) mock_query_uri.side_effect = query_uri([ m_api.env.host, 'replica.' + m_api.env.domain, 'replica2.' + m_api.env.domain ]) mock_query.side_effect = fake_query_three mock_rrset.side_effect = [ resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica2.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), ] m_api.Command.server_find.side_effect = [{ 'result': [ { 'cn': [m_api.env.host], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica.' + m_api.env.domain], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica2.' + m_api.env.domain], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, ] }] framework = object() registry.initialize(framework, config.Config) f = IPADNSSystemRecordsCheck(registry) self.results = capture_results(f) if has_uri_support: expected = 40 else: expected = 28 assert len(self.results) == expected ok = get_results_by_severity(self.results.results, constants.SUCCESS) warn = get_results_by_severity(self.results.results, constants.WARNING) if has_uri_support: assert len(ok) == 33 assert len(warn) == 7 else: assert len(ok) == 21 assert len(warn) == 7 for result in warn: assert result.kw.get('msg') == 'Expected SRV record missing' @patch(resolve_rrsets_import) @patch('ipapython.dnsutil.query_srv') @patch('ipahealthcheck.ipa.idns.query_uri') @patch('ipahealthcheck.ipa.idns.resolve') def test_dnsrecords_missing_ipa_ca(self, mock_query, mock_query_uri, mock_query_srv, mock_rrset): """Drop one of the masters from query_srv This will simulate missing SRV records and cause a number of warnings to be thrown. """ mock_query_srv.side_effect = query_srv([ m_api.env.host, 'replica.' + m_api.env.domain, 'replica2.' + m_api.env.domain ]) mock_query_uri.side_effect = query_uri([ m_api.env.host, 'replica.' + m_api.env.domain, 'replica2.' + m_api.env.domain ]) mock_query.side_effect = fake_query_two mock_rrset.side_effect = [ resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica2.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)) ] m_api.Command.server_find.side_effect = [{ 'result': [ { 'cn': [m_api.env.host], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica.' + m_api.env.domain], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica2.' + m_api.env.domain], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, ] }] framework = object() registry.initialize(framework, config.Config) f = IPADNSSystemRecordsCheck(registry) self.results = capture_results(f) if has_uri_support: expected = 40 else: expected = 28 assert len(self.results) == expected ok = get_results_by_severity(self.results.results, constants.SUCCESS) warn = get_results_by_severity(self.results.results, constants.WARNING) if has_uri_support: assert len(ok) == 38 assert len(warn) == 2 else: assert len(ok) == 26 assert len(warn) == 2 for result in warn: assert re.match( r'^Got {count} ipa-ca (A|AAAA) records, expected {expected}$', result.kw.get('msg') ) assert result.kw.get('count') == 2 assert result.kw.get('expected') == 3 @patch(resolve_rrsets_import) @patch('ipapython.dnsutil.query_srv') @patch('ipahealthcheck.ipa.idns.query_uri') @patch('ipahealthcheck.ipa.idns.resolve') def test_dnsrecords_extra_srv(self, mock_query, mock_query_uri, mock_query_srv, mock_rrset): """An extra SRV record set exists, report it. Add an extra master to the query_srv() which will generate a full extra set of SRV records for the master. """ mock_query_srv.side_effect = query_srv([ m_api.env.host, 'replica.' + m_api.env.domain, 'replica2.' + m_api.env.domain, 'replica3.' + m_api.env.domain ]) mock_query_uri.side_effect = query_uri([ m_api.env.host, 'replica.' + m_api.env.domain, 'replica2.' + m_api.env.domain, ]) mock_query.side_effect = fake_query_three mock_rrset.side_effect = [ resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica2.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), resolve_rrsets('replica3.' + m_api.env.domain, (rdatatype.A, rdatatype.AAAA)), ] m_api.Command.server_find.side_effect = [{ 'result': [ { 'cn': [m_api.env.host], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica.' + m_api.env.domain], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, { 'cn': ['replica2.' + m_api.env.domain], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, ] }] framework = object() registry.initialize(framework, config.Config) f = IPADNSSystemRecordsCheck(registry) self.results = capture_results(f) if has_uri_support: expected = 47 else: expected = 35 assert len(self.results) == expected ok = get_results_by_severity(self.results.results, constants.SUCCESS) warn = get_results_by_severity(self.results.results, constants.WARNING) if has_uri_support: assert len(ok) == 40 assert len(warn) == 7 else: assert len(ok) == 28 assert len(warn) == 7 for result in warn: assert result.kw.get('msg') == \ 'Unexpected SRV entry in DNS' @patch(resolve_rrsets_import) @patch('ipapython.dnsutil.query_srv') @patch('ipahealthcheck.ipa.idns.query_uri') @patch('ipahealthcheck.ipa.idns.resolve') def test_dnsrecords_bad_realm(self, mock_query, mock_query_uri, mock_query_srv, mock_rrset): """Unexpected Kerberos TXT record""" mock_query.side_effect = fake_query_one_txt mock_query_srv.side_effect = query_srv([m_api.env.host]) mock_query_uri.side_effect = query_uri([m_api.env.host]) mock_rrset.side_effect = [ resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)) ] m_api.Command.server_find.side_effect = [{ 'result': [ { 'cn': [m_api.env.host], 'enabled_role_servrole': [ 'CA server', 'IPA master' ], }, ] }] framework = object() registry.initialize(framework, config.Config) f = IPADNSSystemRecordsCheck(registry) self.results = capture_results(f) if has_uri_support: expected = 14 else: expected = 10 assert len(self.results) == expected ok = get_results_by_severity(self.results.results, constants.SUCCESS) warn = get_results_by_severity(self.results.results, constants.WARNING) if has_uri_support: assert len(ok) == 13 assert len(warn) == 1 else: assert len(ok) == 9 assert len(warn) == 1 result = warn[0] assert result.kw.get('msg') == 'expected realm missing' assert result.kw.get('key') == '\"FAKE_REALM\"' @patch(resolve_rrsets_import) @patch('ipapython.dnsutil.query_srv') @patch('ipahealthcheck.ipa.idns.query_uri') @patch('ipahealthcheck.ipa.idns.resolve') def test_dnsrecords_one_with_ad(self, mock_query, mock_query_uri, mock_query_srv, mock_rrset): mock_query.side_effect = fake_query_one mock_query_srv.side_effect = query_srv([m_api.env.host], True) mock_query_uri.side_effect = query_uri([m_api.env.host]) mock_rrset.side_effect = [ resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)) ] m_api.Command.server_find.side_effect = [{ 'result': [ { 'cn': [m_api.env.host], 'enabled_role_servrole': [ 'CA server', 'IPA master', 'AD trust controller' ], }, ] }] framework = object() registry.initialize(framework, config.Config) f = IPADNSSystemRecordsCheck(registry) self.results = capture_results(f) if has_uri_support: expected = 20 else: expected = 16 assert len(self.results) == expected for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.idns' assert result.check == 'IPADNSSystemRecordsCheck' freeipa-healthcheck-0.10/tests/test_ipa_dnssan.py000066400000000000000000000077001420053437700222110ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from util import capture_results, CAInstance from util import m_api from base import BaseTest from unittest.mock import Mock, patch from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPACertDNSSAN from mock_certmonger import create_mock_dbus, _certmonger from mock_certmonger import get_expected_requests, set_requests class IPACertificate: def __init__(self, serial_number=1, no_san=False): self.subject = 'CN=%s' % m_api.env.host self.issuer = 'CN=ISSUER' self.serial_number = serial_number self.san_a_label_dns_names = [m_api.env.host] if not no_san: self.san_a_label_dns_names.append('ipa-ca.%s' % m_api.env.domain) class TestDNSSAN(BaseTest): patches = { 'ipaserver.install.certs.is_ipa_issued_cert': Mock(return_value=True), 'ipahealthcheck.ipa.certs.get_expected_requests': Mock(return_value=get_expected_requests()), 'ipalib.install.certmonger._cm_dbus_object': Mock(side_effect=create_mock_dbus), 'ipalib.install.certmonger._certmonger': Mock(return_value=_certmonger()), 'ipaserver.install.cainstance.CAInstance': Mock(return_value=CAInstance()), 'socket.getfqdn': Mock(return_value=m_api.env.host), } @patch('ipalib.install.certmonger.get_request_value') @patch('ipalib.x509.load_certificate_from_file') def test_dnssan_ok(self, mock_cert, mock_value): set_requests() mock_value.side_effect = ['dogtag-ipa-ca-renew-agent', 'IPA', 'caIPAserviceCert'] mock_cert.return_value = IPACertificate() framework = object() registry.initialize(framework, config.Config) f = IPACertDNSSAN(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.kw.get('san') == [m_api.env.host, 'ipa-ca.%s' % m_api.env.domain] assert result.kw.get('hostname') == [m_api.env.host, 'ipa-ca.%s' % m_api.env.domain] assert result.kw.get('profile') == 'caIPAserviceCert' assert result.check == 'IPACertDNSSAN' @patch('ipalib.install.certmonger.get_request_value') def test_sandns_no_certs(self, mock_value): set_requests() mock_value.side_effect = ['dogtag-ipa-ca-renew-agent', 'dogtag-ipa-ca-renew-agent'] framework = object() registry.initialize(framework, config.Config) f = IPACertDNSSAN(registry) self.results = capture_results(f) # No IPA CA, no results assert len(self.results) == 0 @patch('ipalib.install.certmonger.get_request_value') @patch('ipalib.x509.load_certificate_from_file') def test_dnssan_missing_ipaca(self, mock_cert, mock_value): set_requests() mock_value.side_effect = ['dogtag-ipa-ca-renew-agent', 'IPA', 'caIPAserviceCert'] mock_cert.return_value = IPACertificate(no_san=True) framework = object() registry.initialize(framework, config.Config) f = IPACertDNSSAN(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.kw.get('san') == [m_api.env.host] assert result.kw.get('hostname') == 'ipa-ca.%s' % m_api.env.domain assert result.kw.get('profile') == 'caIPAserviceCert' assert result.kw.get('ca') == 'IPA' assert result.kw.get('key') == '5678' freeipa-healthcheck-0.10/tests/test_ipa_expiration.py000066400000000000000000000217341420053437700231100ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results from base import BaseTest from ipaplatform.paths import paths from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPACertmongerExpirationCheck from ipahealthcheck.ipa.certs import IPACAChainExpirationCheck from unittest.mock import Mock, patch from mock_certmonger import create_mock_dbus, _certmonger from mock_certmonger import ( get_expected_requests, set_requests, CERT_EXPIRATION_DAYS, ) from datetime import datetime, timedelta, timezone class TestExpiration(BaseTest): patches = { 'ipahealthcheck.ipa.certs.get_expected_requests': Mock(return_value=get_expected_requests()), 'ipalib.install.certmonger._cm_dbus_object': Mock(side_effect=create_mock_dbus), 'ipalib.install.certmonger._certmonger': Mock(return_value=_certmonger()) } def test_expiration(self): set_requests() framework = object() registry.initialize(framework, config.Config) f = IPACertmongerExpirationCheck(registry) f.config.cert_expiration_days = '7' self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertmongerExpirationCheck' assert result.kw.get('key') == '1234' assert result.kw.get('expiration_date') == '19700101001704Z' result = self.results.results[1] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertmongerExpirationCheck' assert result.kw.get('key') == '5678' def test_expiration_warning(self): warning = datetime.now(timezone.utc) + timedelta(days=20) replaceme = { 'nickname': '7777', 'cert-file': paths.RA_AGENT_PEM, 'key-file': paths.RA_AGENT_KEY, 'ca-name': 'dogtag-ipa-ca-renew-agent', 'not-valid-after': int(warning.timestamp()), } set_requests(remove=0, add=replaceme) framework = object() registry.initialize(framework, config.Config) f = IPACertmongerExpirationCheck(registry) f.config.cert_expiration_days = str(CERT_EXPIRATION_DAYS) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertmongerExpirationCheck' assert result.kw.get('key') == '5678' result = self.results.results[1] assert result.result == constants.WARNING assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertmongerExpirationCheck' assert result.kw.get('key') == '7777' assert result.kw.get('days') == 19 class FakeIPACertificate: def __init__(self, cert, backend=None, subject=None, not_after=None): self.subj = subject self.not_after = not_after @property def subject(self): return self.subj @property def not_valid_after(self): return self.not_after class TestChainExpiration(BaseTest): root_ca = 'CN=Certificate Shack Root CA,O=Certificate Shack Ltd' sub_ca = 'CN=Certificate Shack Intermediate CA,O=Certificate Shack Ltd' @patch('ipalib.x509.load_certificate_list_from_file') def test_still_valid(self, mock_load): mock_load.return_value = [ FakeIPACertificate( None, subject=self.sub_ca, not_after=datetime.now(timezone.utc) + timedelta(days=20) ), FakeIPACertificate( None, subject=self.root_ca, not_after=datetime.now(timezone.utc) + timedelta(days=20) ) ] framework = object() registry.initialize(framework, config.Config) f = IPACAChainExpirationCheck(registry) f.config.cert_expiration_days = '7' self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACAChainExpirationCheck' assert result.kw.get('key') == self.sub_ca result = self.results.results[1] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACAChainExpirationCheck' assert result.kw.get('key') == self.root_ca @patch('ipalib.x509.load_certificate_list_from_file') def test_expiring_soon(self, mock_load): mock_load.return_value = [ FakeIPACertificate( None, subject=self.sub_ca, not_after=datetime.now(timezone.utc) + timedelta(days=3, minutes=1) ), FakeIPACertificate( None, subject=self.root_ca, not_after=datetime.now(timezone.utc) + timedelta(days=3, minutes=1) ) ] framework = object() registry.initialize(framework, config.Config) f = IPACAChainExpirationCheck(registry) f.config.cert_expiration_days = '7' self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.WARNING assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACAChainExpirationCheck' assert result.kw.get('key') == self.sub_ca assert result.kw.get('days') == 3 assert 'expiring' in result.kw.get('msg') result = self.results.results[1] assert result.result == constants.WARNING assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACAChainExpirationCheck' assert result.kw.get('key') == self.root_ca assert result.kw.get('days') == 3 assert 'expiring' in result.kw.get('msg') @patch('ipalib.x509.load_certificate_list_from_file') def test_all_expired(self, mock_load): mock_load.return_value = [ FakeIPACertificate( None, subject=self.sub_ca, not_after=datetime.now(timezone.utc) + timedelta(days=-3) ), FakeIPACertificate( None, subject=self.root_ca, not_after=datetime.now(timezone.utc) + timedelta(days=-3) ) ] framework = object() registry.initialize(framework, config.Config) f = IPACAChainExpirationCheck(registry) f.config.cert_expiration_days = '7' self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.CRITICAL assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACAChainExpirationCheck' assert result.kw.get('key') == self.sub_ca assert 'expired' in result.kw.get('msg') result = self.results.results[1] assert result.result == constants.CRITICAL assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACAChainExpirationCheck' assert result.kw.get('key') == self.root_ca assert 'expired' in result.kw.get('msg') @patch('ipalib.x509.load_certificate_list_from_file') def test_one_expired(self, mock_load): mock_load.return_value = [ FakeIPACertificate( None, subject=self.sub_ca, not_after=datetime.now(timezone.utc) + timedelta(days=-3) ), FakeIPACertificate( None, subject=self.root_ca, not_after=datetime.now(timezone.utc) + timedelta(days=20) ) ] framework = object() registry.initialize(framework, config.Config) f = IPACAChainExpirationCheck(registry) f.config.cert_expiration_days = '7' self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.CRITICAL assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACAChainExpirationCheck' assert result.kw.get('key') == self.sub_ca assert 'expired' in result.kw.get('msg') result = self.results.results[1] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACAChainExpirationCheck' assert result.kw.get('key') == self.root_ca freeipa-healthcheck-0.10/tests/test_ipa_nss.py000066400000000000000000000043251420053437700215260ustar00rootroot00000000000000# # Copyright (C) 2021 FreeIPA Contributors see COPYING for license # from base import BaseTest from collections import namedtuple from unittest.mock import patch from util import capture_results from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.nss import IPAGroupMemberCheck struct_group = namedtuple( 'struct_group', ['gr_name', 'gr_passwd', 'gr_gid', 'gr_mem'] ) def make_group(name, members): return struct_group(name, 'x', 999, members) class TestGroupMember(BaseTest): @patch('grp.getgrnam') def test_ipaapi_group_ok(self, mock_grp): mock_grp.return_value = make_group('apache', ('apache', 'ipaapi',)) framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPAGroupMemberCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS @patch('grp.getgrnam') def test_ipaapi_bad_group(self, mock_grp): mock_grp.side_effect = KeyError("name not found: 'ipaapi'") framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPAGroupMemberCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('key') == 'ipaapi' assert result.kw.get('msg') == 'group {key} does not exist' @patch('grp.getgrnam') def test_ipaapi_missing_member(self, mock_grp): mock_grp.return_value = make_group('apache', ('foo',)) framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPAGroupMemberCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('key') == 'ipaapi' assert result.kw.get('msg') == \ '{member} is not a member of group {key}' freeipa-healthcheck-0.10/tests/test_ipa_nssdb.py000066400000000000000000000122151420053437700220310ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results, CAInstance, KRAInstance from base import BaseTest from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPACertNSSTrust from ipaplatform.paths import paths from unittest.mock import Mock, patch class mock_CertDB: def __init__(self, trust): """A dict of nickname + NSSdb trust flags""" self.trust = trust def list_certs(self): return [(nickname, self.trust[nickname]) for nickname in self.trust] def my_unparse_trust_flags(trust_flags): return trust_flags # These tests make some assumptions about the order in which the # results are returned. class TestNSSDBTrust(BaseTest): patches = { 'ipaserver.install.cainstance.CAInstance': Mock(return_value=CAInstance()), 'ipaserver.install.krainstance.KRAInstance': Mock(return_value=KRAInstance(False)), 'ipapython.certdb.unparse_trust_flags': Mock(side_effect=my_unparse_trust_flags), } @patch('ipaserver.install.certs.CertDB') def test_trust_default_ok(self, mock_certdb): """Test what should be the standard case""" trust = { 'ocspSigningCert cert-pki-ca': 'u,u,u', 'subsystemCert cert-pki-ca': 'u,u,u', 'auditSigningCert cert-pki-ca': 'u,u,Pu', 'Server-Cert cert-pki-ca': 'u,u,u' } mock_certdb.return_value = mock_CertDB(trust) framework = object() registry.initialize(framework, config.Config) f = IPACertNSSTrust(registry) self.results = capture_results(f) assert len(self.results) == 4 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertNSSTrust' assert 'cert-pki-ca' in result.kw.get('key') @patch('ipaserver.install.certs.CertDB') def test_trust_ocsp_missing(self, mock_certdb): """Test a missing certificate""" trust = { 'subsystemCert cert-pki-ca': 'u,u,u', 'auditSigningCert cert-pki-ca': 'u,u,Pu', 'Server-Cert cert-pki-ca': 'u,u,u' } mock_certdb.return_value = mock_CertDB(trust) framework = object() registry.initialize(framework, config.Config) f = IPACertNSSTrust(registry) self.results = capture_results(f) # The check reports success for those that it found and are correct and # reports missing certs last. num = len(self.results.results) - 2 for r in range(0, num): result = self.results.results[r] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertNSSTrust' assert 'cert-pki-ca' in result.kw.get('key') result = self.results.results[-1] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertNSSTrust' assert result.kw.get('key') == 'ocspSigningCert cert-pki-ca' assert result.kw.get('nickname') == 'ocspSigningCert cert-pki-ca' assert result.kw.get('dbdir') == paths.PKI_TOMCAT_ALIAS_DIR assert len(self.results) == 4 @patch('ipaserver.install.certs.CertDB') def test_trust_bad(self, mock_certdb): """Test multiple unexpected trust flags""" trust = { 'ocspSigningCert cert-pki-ca': 'u,u,u', 'subsystemCert cert-pki-ca': 'X,u,u', 'auditSigningCert cert-pki-ca': 'u,u,Pu', 'Server-Cert cert-pki-ca': 'X,u,u' } mock_certdb.return_value = mock_CertDB(trust) framework = object() registry.initialize(framework, config.Config) f = IPACertNSSTrust(registry) self.results = capture_results(f) result = self.results.results[1] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertNSSTrust' assert result.kw.get('key') == 'subsystemCert cert-pki-ca' assert result.kw.get('got') == 'X,u,u' assert result.kw.get('expected') == 'u,u,u' result = self.results.results[3] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertNSSTrust' assert result.kw.get('key') == 'Server-Cert cert-pki-ca' assert result.kw.get('got') == 'X,u,u' assert result.kw.get('expected') == 'u,u,u' assert len(self.results) == 4 @patch('ipaserver.install.cainstance.CAInstance') def test_trust_caless(self, mock_cainstance): """Nothing to check if the master is CALess""" mock_cainstance.return_value = CAInstance(False) framework = object() registry.initialize(framework, config.Config) f = IPACertNSSTrust(registry) self.results = capture_results(f) assert len(self.results) == 0 freeipa-healthcheck-0.10/tests/test_ipa_nssvalidation.py000066400000000000000000000072621420053437700236040ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from base import BaseTest from unittest.mock import Mock, patch from util import capture_results, CAInstance from ipapython.ipautil import _RunResult from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPANSSChainValidation class DsInstance: def get_server_cert_nickname(self, serverid): return 'Server-Cert' class TestNSSValidation(BaseTest): patches = { 'ipahealthcheck.ipa.certs.get_dogtag_cert_password': Mock(return_value='foo'), 'ipaserver.install.dsinstance.DsInstance': Mock(return_value=DsInstance()), } @patch('ipaserver.install.cainstance.CAInstance') @patch('ipapython.ipautil.run') def test_nss_validation_ok(self, mock_run, mock_cainstance): def run(args, raiseonerr=True): result = _RunResult('', '', 0) result.raw_output = b'certutil: certificate is valid\n' result.raw_error_output = b'' return result mock_run.side_effect = run mock_cainstance.return_value = CAInstance() framework = object() registry.initialize(framework, config.Config) f = IPANSSChainValidation(registry) self.results = capture_results(f) assert len(self.results) == 2 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPANSSChainValidation' @patch('ipaserver.install.cainstance.CAInstance') @patch('ipapython.ipautil.run') def test_nss_validation_bad(self, mock_run, mock_cainstance): def run(args, raiseonerr=True): result = _RunResult('', '', 255) result.raw_output = str.encode( 'certutil: certificate is invalid: Peer\'s certificate issuer ' 'has been marked as not trusted by the user.' ) result.raw_error_output = b'' result.error_log = '' return result mock_run.side_effect = run mock_cainstance.return_value = CAInstance() framework = object() registry.initialize(framework, config.Config) f = IPANSSChainValidation(registry) self.results = capture_results(f) assert len(self.results) == 2 for result in self.results.results: assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPANSSChainValidation' @patch('ipaserver.install.cainstance.CAInstance') @patch('ipapython.ipautil.run') def test_nss_validation_ok_no_ca(self, mock_run, mock_cainstance): """Test with the CA marked as not configured so there should only be a DS certificate to check. """ def run(args, raiseonerr=True): result = _RunResult('', '', 0) result.raw_output = b'certutil: certificate is valid\n' result.raw_error_output = b'' return result mock_run.side_effect = run mock_cainstance.return_value = CAInstance(False) framework = object() registry.initialize(framework, config.Config) f = IPANSSChainValidation(registry) self.results = capture_results(f) assert len(self.results) == 1 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPANSSChainValidation' assert 'slapd-' in result.kw.get('key') freeipa-healthcheck-0.10/tests/test_ipa_opensslvalidation.py000066400000000000000000000046031420053437700244600ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from base import BaseTest from unittest.mock import Mock, patch from util import capture_results, CAInstance from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPAOpenSSLChainValidation from ipapython.ipautil import _RunResult class TestOpenSSLValidation(BaseTest): patches = { 'ipaserver.install.cainstance.CAInstance': Mock(return_value=CAInstance()), } @patch('ipapython.ipautil.run') def test_openssl_validation_ok(self, mock_run): def run(args, raiseonerr=True): result = _RunResult('', '', 0) result.raw_output = bytes( '{}: OK'.format(args[-1]).encode('utf-8')) result.raw_error_output = b'' return result mock_run.side_effect = run framework = object() registry.initialize(framework, config.Config) f = IPAOpenSSLChainValidation(registry) self.results = capture_results(f) assert len(self.results) == 2 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPAOpenSSLChainValidation' @patch('ipapython.ipautil.run') def test_openssl_validation_bad(self, mock_run): def run(args, raiseonerr=True): result = _RunResult('', '', 2) result.raw_output = bytes( 'O = EXAMPLE.TEST, CN = ipa.example.test\n' 'error 20 at 0 depth lookup: unable to get local issuer ' 'certificate\nerror {}: verification failed'.format(args[-1]) .encode('utf-8')) result.raw_error_output = b'' result.error_log = '' return result mock_run.side_effect = run framework = object() registry.initialize(framework, config.Config) f = IPAOpenSSLChainValidation(registry) self.results = capture_results(f) assert len(self.results) == 2 for result in self.results.results: assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPAOpenSSLChainValidation' assert 'failed' in result.kw.get('msg') freeipa-healthcheck-0.10/tests/test_ipa_proxy.py000066400000000000000000000206601420053437700221040ustar00rootroot00000000000000# # Copyright (C) 2021 FreeIPA Contributors see COPYING for license # from base import BaseTest from io import BytesIO from unittest.mock import patch from util import capture_results import lxml.etree from ipahealthcheck.core import config, constants from ipahealthcheck.meta.plugin import registry from ipahealthcheck.ipa.proxy import IPAProxySecretCheck # Pre-parse the XML to avoid Mock weirdness good_xml_input = """ """ # noqa: E501 good_xml = lxml.etree.parse(BytesIO(good_xml_input.encode('utf-8'))) good_ipa_proxy = """ SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate SSLVerifyClient none ProxyPassMatch ajp://localhost:8009 secret=somesecret ProxyPassReverse ajp://localhost:8009 """ empty_ipa_proxy = """ """ different_secrets_ipa_proxy = """ SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate SSLVerifyClient none ProxyPassMatch ajp://localhost:8009 secret=somesecret ProxyPassReverse ajp://localhost:8009 SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate SSLVerifyClient none ProxyPassMatch ajp://localhost:8009 secret=othersecret ProxyPassReverse ajp://localhost:8009 """ # server.xml secret won't match Apache secret mismatch1_xml_input = """ """ # noqa: E501 mismatch1_xml = lxml.etree.parse(BytesIO(mismatch1_xml_input.encode('utf-8'))) both_secrets_xml_input = """ """ # noqa: E501 both_secrets_xml = lxml.etree.parse( BytesIO(both_secrets_xml_input.encode('utf-8')) ) both_secrets_mismatch_xml_input = """ """ # noqa: E501 both_secrets_mismatch_xml = lxml.etree.parse( BytesIO(both_secrets_mismatch_xml_input.encode('utf-8')) ) empty_xml_input = """ """ empty_xml = lxml.etree.parse(BytesIO(empty_xml_input.encode('utf-8'))) class TestIPAProxySecretCheck(BaseTest): @patch('lxml.etree.parse') @patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy') def test_matching_secrets(self, mock_proxy, mock_ltree): """The passwords match""" mock_ltree.return_value = good_xml mock_proxy.return_value = good_ipa_proxy.split('\n') framework = object() registry.initialize(framework, config.Config()) f = IPAProxySecretCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS @patch('lxml.etree.parse') @patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy') def test_xml_both_secrets(self, mock_proxy, mock_ltree): """server.xml defines both secret types and they match""" mock_ltree.return_value = both_secrets_xml mock_proxy.return_value = good_ipa_proxy.split('\n') framework = object() registry.initialize(framework, config.Config()) f = IPAProxySecretCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS @patch('lxml.etree.parse') @patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy') def test_xml_both_secret_type_mismatch(self, mock_proxy, mock_ltree): """XML has both secret attributes and they do not match""" mock_ltree.return_value = both_secrets_mismatch_xml mock_proxy.return_value = good_ipa_proxy.split('\n') framework = object() registry.initialize(framework, config.Config()) f = IPAProxySecretCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.WARNING assert result.kw.get('msg') == 'The AJP secrets in {server_xml} do '\ 'not match' @patch('lxml.etree.parse') @patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy') def test_xml_secret_mismatch(self, mock_proxy, mock_ltree): """The Apache secret doesn't match the tomcat secret""" mock_ltree.return_value = mismatch1_xml mock_proxy.return_value = good_ipa_proxy.split('\n') framework = object() registry.initialize(framework, config.Config()) f = IPAProxySecretCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.CRITICAL assert result.kw.get('msg') == 'A ProxyPassMatch secret not found ' \ 'in {server_xml}' @patch('lxml.etree.parse') @patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy') def test_xml_no_connectors(self, mock_proxy, mock_ltree): """No connectors found in server.xml""" mock_ltree.return_value = empty_xml mock_proxy.return_value = good_ipa_proxy.split('\n') framework = object() registry.initialize(framework, config.Config()) f = IPAProxySecretCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.CRITICAL assert result.kw.get('msg') == 'No AJP/1.3 Connectors defined in ' \ '{server_xml}' @patch('lxml.etree.parse') @patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy') def test_no_proxypassmatch(self, mock_proxy, mock_ltree): """No connectors found in server.xml""" mock_ltree.return_value = good_xml mock_proxy.return_value = empty_ipa_proxy.split('\n') framework = object() registry.initialize(framework, config.Config()) f = IPAProxySecretCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.CRITICAL assert result.kw.get('msg') == 'No ProxyPassMatch secrets found ' \ 'in {proxy_conf}' @patch('lxml.etree.parse') @patch('ipahealthcheck.ipa.proxy.read_ipa_pki_proxy') def test_proxypassmatch_different_secrets(self, mock_proxy, mock_ltree): """No connectors found in server.xml""" mock_ltree.return_value = good_xml mock_proxy.return_value = different_secrets_ipa_proxy.split('\n') framework = object() registry.initialize(framework, config.Config()) f = IPAProxySecretCheck(registry) self.results = capture_results(f) print(self.results.results) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.CRITICAL assert result.kw.get('msg') == 'Not all ProxyPassMatch secrets ' \ 'match in {proxy_conf}' result = self.results.results[1] assert result.result == constants.CRITICAL assert result.kw.get('msg') == 'A ProxyPassMatch secret not found ' \ 'in {server_xml}' freeipa-healthcheck-0.10/tests/test_ipa_revocation.py000066400000000000000000000060551420053437700230760ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results, CAInstance from util import m_api from base import BaseTest from unittest.mock import Mock from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPACertRevocation from mock_certmonger import create_mock_dbus, _certmonger from mock_certmonger import get_expected_requests, set_requests class IPACertificate: def __init__(self, serial_number=1): self.subject = 'CN=RA AGENT' self.issuer = 'CN=ISSUER' self.serial_number = serial_number class TestRevocation(BaseTest): patches = { 'ipaserver.install.certs.is_ipa_issued_cert': Mock(return_value=True), 'ipalib.x509.load_certificate_from_file': Mock(return_value=IPACertificate()), 'ipahealthcheck.ipa.certs.get_expected_requests': Mock(return_value=get_expected_requests()), 'ipalib.install.certmonger._cm_dbus_object': Mock(side_effect=create_mock_dbus), 'ipalib.install.certmonger._certmonger': Mock(return_value=_certmonger()), 'ipaserver.install.cainstance.CAInstance': Mock(return_value=CAInstance()), } def test_revocation_ok(self): m_api.Command.cert_show.side_effect = [ { u'result': { u"revoked": False, } }, { u'result': { u"revoked": False, } }, ] set_requests() framework = object() registry.initialize(framework, config.Config) f = IPACertRevocation(registry) self.results = capture_results(f) assert len(self.results) == 2 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertRevocation' def test_revocation_one_bad(self): m_api.Command.cert_show.side_effect = [ { u'result': { u"revoked": False, } }, { u'result': { u"revoked": True, u"revocation_reason": 4, } }, ] set_requests() framework = object() registry.initialize(framework, config.Config) f = IPACertRevocation(registry) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertRevocation' result = self.results.results[1] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertRevocation' assert result.kw.get('revocation_reason') == 'superseded' freeipa-healthcheck-0.10/tests/test_ipa_roles.py000066400000000000000000000077531420053437700220570ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from base import BaseTest from unittest.mock import patch from util import capture_results, CAInstance from util import m_api from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.roles import (IPACRLManagerCheck, IPARenewalMasterCheck) class TestCRLManagerRole(BaseTest): @patch('ipaserver.install.cainstance.CAInstance') def test_not_crlmanager(self, mock_ca): mock_ca.return_value = CAInstance(crlgen=False) framework = object() registry.initialize(framework, config.Config) f = IPACRLManagerCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.roles' assert result.check == 'IPACRLManagerCheck' assert result.kw.get('crlgen_enabled') is False @patch('ipaserver.install.cainstance.CAInstance') def test_crlmanager(self, mock_ca): mock_ca.return_value = CAInstance() framework = object() registry.initialize(framework, config.Config) f = IPACRLManagerCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.roles' assert result.check == 'IPACRLManagerCheck' assert result.kw.get('crlgen_enabled') is True @patch('ipaserver.install.cainstance.CAInstance') def test_crlmanager_no_ca(self, mock_ca): """There should be no CRLManagerCheck without a CA""" mock_ca.return_value = CAInstance(False) framework = object() registry.initialize(framework, config.Config) f = IPACRLManagerCheck(registry) self.results = capture_results(f) assert len(self.results) == 0 class TestRenewalMaster(BaseTest): def test_renewal_master_not_set(self): framework = object() registry.initialize(framework, config.Config) f = IPARenewalMasterCheck(registry) m_api.Command.config_show.side_effect = [{ 'result': { } }] self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.roles' assert result.check == 'IPARenewalMasterCheck' assert result.kw.get('master') is False def test_not_renewal_master(self): framework = object() registry.initialize(framework, config.Config) f = IPARenewalMasterCheck(registry) m_api.Command.config_show.side_effect = [{ 'result': { 'ca_renewal_master_server': 'something.ipa.example' } }] self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.roles' assert result.check == 'IPARenewalMasterCheck' assert result.kw.get('master') is False def test_is_renewal_master(self): framework = object() registry.initialize(framework, config.Config) f = IPARenewalMasterCheck(registry) m_api.Command.config_show.side_effect = [{ 'result': { 'ca_renewal_master_server': 'server.ipa.example' } }] self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.roles' assert result.check == 'IPARenewalMasterCheck' assert result.kw.get('master') is True freeipa-healthcheck-0.10/tests/test_ipa_topology.py000066400000000000000000000165341420053437700226040ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results from util import m_api from base import BaseTest from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.topology import IPATopologyDomainCheck class TestTopology(BaseTest): def test_topology_ok(self): m_api.Command.topologysuffix_verify.side_effect = [ { u'result': { u"in_order": True, } }, { u'result': { u"in_order": True, } }, ] m_api.Command.ca_is_enabled.return_value = {'result': True} framework = object() registry.initialize(framework, config.Config) f = IPATopologyDomainCheck(registry) self.results = capture_results(f) assert len(self.results) == 2 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.topology' assert result.check == 'IPATopologyDomainCheck' def test_topology_domain_bad(self): m_api.Command.topologysuffix_verify.side_effect = [ { u'result': { u"connect_errors": [ [ u"ipa.example.test", [u"ipa.example.test"], [u"replica2.example.test"] ], [ u"replica2.example.test", [u"replica2.example.test"], [u"ipa.example.test"] ] ], u"in_order": False, u"max_agmts": 4, u"max_agmts_errors": [] } }, { u'result': { u"in_order": True, } }, ] m_api.Command.ca_is_enabled.return_value = {'result': True} framework = object() registry.initialize(framework, config.Config) f = IPATopologyDomainCheck(registry) self.results = capture_results(f) assert len(self.results) == 3 for result in self.results.results: assert result.source == 'ipahealthcheck.ipa.topology' assert result.check == 'IPATopologyDomainCheck' # The first two results are failures in the domain suffix, the # third is a success in the ca suffix. result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('key') == 'ipa.example.test' assert result.kw.get('replicas') == ['replica2.example.test'] assert result.kw.get('suffix') == 'domain' assert result.kw.get('type') == 'connect' assert 'can\'t contact servers' in result.kw.get('msg') result = self.results.results[1] assert result.result == constants.ERROR assert result.kw.get('key') == 'replica2.example.test' assert result.kw.get('replicas') == ['ipa.example.test'] assert result.kw.get('suffix') == 'domain' assert result.kw.get('type') == 'connect' assert 'can\'t contact servers' in result.kw.get('msg') result = self.results.results[2] assert result.result == constants.SUCCESS assert result.kw.get('suffix') == 'ca' def test_topology_ca_bad(self): m_api.Command.topologysuffix_verify.side_effect = [ { u'result': { u"in_order": True, } }, { u'result': { u"connect_errors": [ [ u"ipa.example.test", [u"ipa.example.test"], [u"replica2.example.test"] ], [ u"replica2.example.test", [u"replica2.example.test"], [u"ipa.example.test"] ] ], u"in_order": False, u"max_agmts": 4, u"max_agmts_errors": [] } }, ] m_api.Command.ca_is_enabled.return_value = {'result': True} framework = object() registry.initialize(framework, config.Config) f = IPATopologyDomainCheck(registry) self.results = capture_results(f) assert len(self.results) == 3 for result in self.results.results: assert result.source == 'ipahealthcheck.ipa.topology' assert result.check == 'IPATopologyDomainCheck' # The first result is ok (domain) and the last two are failures # (ca) result = self.results.results[0] assert result.result == constants.SUCCESS assert result.kw.get('suffix') == 'domain' result = self.results.results[1] assert result.result == constants.ERROR assert result.kw.get('key') == 'ipa.example.test' assert result.kw.get('replicas') == ['replica2.example.test'] assert result.kw.get('suffix') == 'ca' assert result.kw.get('type') == 'connect' assert 'can\'t contact servers' in result.kw.get('msg') result = self.results.results[2] assert result.result == constants.ERROR assert result.kw.get('key') == 'replica2.example.test' assert result.kw.get('replicas') == ['ipa.example.test'] assert result.kw.get('suffix') == 'ca' assert result.kw.get('type') == 'connect' assert 'can\'t contact servers' in result.kw.get('msg') def test_topology_domain_max_agmts(self): m_api.Command.topologysuffix_verify.side_effect = [ { u'result': { u"connect_errors": [], u"in_order": False, u"max_agmts": 1, u"max_agmts_errors": [ [ u"ipa.example.test", [u"replica2.example.test"], ], ], } }, { u'result': { u"in_order": True, } }, ] m_api.Command.ca_is_enabled.return_value = {'result': True} framework = object() registry.initialize(framework, config.Config) f = IPATopologyDomainCheck(registry) self.results = capture_results(f) assert len(self.results) == 2 for result in self.results.results: assert result.source == 'ipahealthcheck.ipa.topology' assert result.check == 'IPATopologyDomainCheck' result = self.results.results[0] assert result.result == constants.ERROR assert result.kw.get('key') == 'ipa.example.test' assert result.kw.get('replicas') == ['replica2.example.test'] assert result.kw.get('suffix') == 'domain' assert result.kw.get('type') == 'max' assert 'recommended max' in result.kw.get('msg') result = self.results.results[1] assert result.result == constants.SUCCESS assert result.kw.get('suffix') == 'ca' freeipa-healthcheck-0.10/tests/test_ipa_tracking.py000066400000000000000000000054011420053437700225210ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results from base import BaseTest from ipahealthcheck.core import constants, config from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.certs import IPACertTracking from unittest.mock import Mock from mock_certmonger import create_mock_dbus, _certmonger from mock_certmonger import get_expected_requests, set_requests class TestTracking(BaseTest): patches = { 'ipahealthcheck.ipa.certs.get_expected_requests': Mock(return_value=get_expected_requests()), 'ipalib.install.certmonger._cm_dbus_object': Mock(side_effect=create_mock_dbus), 'ipalib.install.certmonger._certmonger': Mock(return_value=_certmonger()) } def test_known_cert_tracking(self): set_requests() framework = object() registry.initialize(framework, config.Config) f = IPACertTracking(registry) self.results = capture_results(f) assert len(self.results) == 2 def test_missing_cert_tracking(self): # remove one of the requests to force it to be missing set_requests(remove=0) framework = object() registry.initialize(framework, config.Config) f = IPACertTracking(registry) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertTracking' assert result.kw.get('key') == \ "cert-file=/var/lib/ipa/ra-agent.pem, " \ "key-file=/var/lib/ipa/ra-agent.key, " \ "ca-name=dogtag-ipa-ca-renew-agent, " \ "template_profile=caSubsystemCert, " \ "cert-storage=FILE, "\ "cert-presave-command=" \ "/usr/libexec/ipa/certmonger/renew_ra_cert_pre, " \ "cert-postsave-command=" \ "/usr/libexec/ipa/certmonger/renew_ra_cert" def test_unknown_cert_tracking(self): # Add a custom, unknown request unknown = { 'nickname': '7777', 'cert-file': '/tmp/test.crt', 'key-file': '/tmp/test.key', 'ca-name': 'IPA', } set_requests(add=unknown) framework = object() registry.initialize(framework, config.Config) f = IPACertTracking(registry) self.results = capture_results(f) assert len(self.results) == 3 result = self.results.results[2] assert result.result == constants.WARNING assert result.source == 'ipahealthcheck.ipa.certs' assert result.check == 'IPACertTracking' assert result.kw.get('key') == '7777' freeipa-healthcheck-0.10/tests/test_ipa_trust.py000066400000000000000000001133171420053437700221060ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # import sys from base import BaseTest from collections import namedtuple from unittest.mock import Mock, patch from util import capture_results from util import m_api from ipahealthcheck.core import config, constants from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.ipa.trust import (IPATrustAgentCheck, IPATrustDomainsCheck, IPADomainCheck, IPATrustCatalogCheck, IPAsidgenpluginCheck, IPATrustAgentMemberCheck, IPATrustControllerPrincipalCheck, IPATrustControllerServiceCheck, IPATrustControllerGroupSIDCheck, IPATrustControllerAdminSIDCheck, IPATrustControllerConfCheck, IPATrustPackageCheck) from ipalib import errors from ipapython.dn import DN from ipapython.ipaldap import LDAPClient, LDAPEntry try: from ipaserver.masters import ENABLED_SERVICE, HIDDEN_SERVICE except ImportError: from ipaserver.install.service import ENABLED_SERVICE, HIDDEN_SERVICE try: from ipapython.ipaldap import realm_to_serverid except ImportError: from ipaserver.install.installutils import realm_to_serverid from ldap import OPT_X_SASL_SSF_MIN from SSSDConfig import NoOptionError class mock_ldap: SCOPE_BASE = 1 SCOPE_ONELEVEL = 2 SCOPE_SUBTREE = 4 def __init__(self, ldapentry): """Initialize the results that we will return from get_entries""" self.results = ldapentry # def get_entries(self, base_dn, scope=SCOPE_SUBTREE, filter=None, # attrs_list=None, get_effective_rights=False, **kwargs): # if self.results is None: # raise errors.NotFound(reason='test') # return self.results def get_entry(self, dn, attrs_list=None, time_limit=None, size_limit=None, get_effective_rights=False): if self.results is None: raise errors.NotFound(reason='test') return self.results class mock_ldap_conn: def set_option(self, option, invalue): pass def get_option(self, option): if option == OPT_X_SASL_SSF_MIN: return 256 return None def search_s(self, base, scope, filterstr=None, attrlist=None, attrsonly=0): return tuple() # # Construct a setup with two direct trusts and one sub domain # def trust_find(): return [{ 'result': [ { 'cn': ['ad.example'], }, { 'cn': ['child.example'], }, ] }] def trustdomain_find(): return [ { "result": [ { "cn": ["ad.example"], "ipantflatname": ["ADROOT"], "ipanttrusteddomainsid": ["S-1-5-21-abc"], "ipanttrusttype": ["2"], "ipanttrustattributes": ["8"], }, { "cn": ["child.ad.example"], "ipantflatname": ["CHILD.ADROOT"], "ipanttrusteddomainsid": ["S-1-5-22-def"], "ipanttrusttype": ["2"], "ipanttrustattributes": ["1"], }, ], }, { "result": [ { "cn": ["child.example"], "ipantflatname": ["CHILD"], "ipanttrusteddomainsid": ["S-1-5-21-ghi"], "ipanttrusttype": ["2"], "ipanttrustattributes": ["8"], }, ], }, ] class SSSDDomain: def __init__(self, return_ipa_server_mode=True, provider='ipa'): self.return_ipa_server_mode = return_ipa_server_mode self.provider = provider def get_option(self, option): if option in ('id_provider', 'auth_provider', 'chpass_provider', 'access_provider'): return self.provider if option == 'ipa_server_mode': if self.return_ipa_server_mode is None: raise NoOptionError() return self.return_ipa_server_mode return None class SSSDConfig(): def __init__(self, return_domains=True, return_ipa_server_mode=True, provider='ipa'): """ Knobs to control what data the configuration returns. """ self.return_domains = return_domains self.return_ipa_server_mode = return_ipa_server_mode self.provider = provider def import_config(self): pass def list_active_domains(self): return ('ipa.example',) def get_domain(self, name): return SSSDDomain(self.return_ipa_server_mode, self.provider) class TestTrustAgent(BaseTest): def test_no_trust_agent(self): framework = object() registry.initialize(framework, config.Config) registry.trust_agent = False f = IPATrustAgentCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 @patch('SSSDConfig.SSSDConfig') def test_trust_agent_ok(self, mock_sssd): mock_sssd.return_value = SSSDConfig(return_domains=True, return_ipa_server_mode=True) framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustAgentCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustAgentCheck' @patch('SSSDConfig.SSSDConfig') def test_trust_agent_not_ipa(self, mock_sssd): mock_sssd.return_value = SSSDConfig(return_domains=True, return_ipa_server_mode=False) framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustAgentCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustAgentCheck' assert result.kw.get('key') == 'ipa_server_mode_false' assert result.kw.get('domain') == 'ipa.example' @patch('SSSDConfig.SSSDConfig') def test_trust_agent_fail(self, mock_sssd): mock_sssd.return_value = SSSDConfig(return_domains=True, return_ipa_server_mode=None) framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustAgentCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustAgentCheck' assert result.kw.get('key') == 'ipa_server_mode_missing' assert result.kw.get('domain') == 'ipa.example' class TestTrustDomains(BaseTest): def test_no_trust_agent(self): framework = object() registry.initialize(framework, config.Config) registry.trust_agent = False f = IPATrustDomainsCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 @patch('ipapython.ipautil.run') def test_trust_domain_list_fail(self, mock_run): run_result = namedtuple('run', ['returncode', 'error_log']) run_result.returncode = 1 run_result.error_log = 'error' mock_run.return_value = run_result framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustDomainsCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustDomainsCheck' assert result.kw.get('key') == 'domain_list_error' @patch('ipapython.ipautil.run') @patch('ipahealthcheck.ipa.trust.get_trust_domains') def test_trust_get_trust_domains_fail(self, mock_trust, mock_run): # sssctl domain-list run_result = namedtuple('run', ['returncode', 'error_log']) run_result.returncode = 0 run_result.error_log = '' run_result.output = 'implicit_files\nipa.example\nad.example\n' mock_run.return_value = run_result mock_trust.side_effect = errors.NotFound(reason='bad') framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustDomainsCheck(registry) self.results = capture_results(f) # There are more than one result I just care about this particular # value. The error is not fatal. result = self.results.results[0] assert result.result == constants.WARNING assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustDomainsCheck' assert result.kw.get('key') == 'trust-find' @patch('ipapython.ipautil.run') def test_trust_get_trust_domains_ok(self, mock_run): # sssctl domain-list dlresult = namedtuple('run', ['returncode', 'error_log']) dlresult.returncode = 0 dlresult.error_log = '' dlresult.output = 'implicit_files\nipa.example\nad.example\n' \ 'child.ad.example\nchild.example\n' olresult = namedtuple('run', ['returncode', 'error_log']) olresult.returncode = 0 olresult.error_log = '' olresult.output = 'Online status: Online\n\n' mock_run.side_effect = [dlresult, olresult, olresult, olresult] # get_trust_domains() m_api.Command.trust_find.side_effect = trust_find() m_api.Command.trustdomain_find.side_effect = trustdomain_find() framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustDomainsCheck(registry) self.results = capture_results(f) assert len(self.results) == 4 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustDomainsCheck' assert result.kw.get('key') == 'domain-list' assert result.kw.get('trust_domains') == \ 'ad.example, child.ad.example, child.example' assert result.kw.get('sssd_domains') == \ 'ad.example, child.ad.example, child.example' result = self.results.results[1] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustDomainsCheck' assert result.kw.get('key') == 'domain-status' assert result.kw.get('domain') == 'ad.example' result = self.results.results[2] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustDomainsCheck' assert result.kw.get('key') == 'domain-status' assert result.kw.get('domain') == 'child.ad.example' result = self.results.results[3] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustDomainsCheck' assert result.kw.get('key') == 'domain-status' assert result.kw.get('domain') == 'child.example' @patch('ipapython.ipautil.run') def test_trust_get_trust_domains_mismatch(self, mock_run): # sssctl domain-list dlresult = namedtuple('run', ['returncode', 'error_log']) dlresult.returncode = 0 dlresult.error_log = '' dlresult.output = 'implicit_files\nipa.example\n' \ 'child.example\n' olresult = namedtuple('run', ['returncode', 'error_log']) olresult.returncode = 0 olresult.error_log = '' olresult.output = 'Online status: Online\n\n' mock_run.side_effect = [dlresult, olresult, olresult] # get_trust_domains() m_api.Command.trust_find.side_effect = trust_find() m_api.Command.trustdomain_find.side_effect = trustdomain_find() framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustDomainsCheck(registry) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustDomainsCheck' assert result.kw.get('key') == 'domain-list' assert result.kw.get('trust_domains') == \ 'ad.example, child.ad.example, child.example' assert result.kw.get('sssd_domains') == 'child.example' result = self.results.results[1] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustDomainsCheck' assert result.kw.get('key') == 'domain-status' assert result.kw.get('domain') == 'child.example' class TestIPADomain(BaseTest): @patch('SSSDConfig.SSSDConfig') def test_ipa_domain_ok(self, mock_sssd): mock_sssd.return_value = SSSDConfig(provider='ipa') framework = object() registry.initialize(framework, config.Config) # being a trust agent isn't mandatory, test without registry.trust_agent = False f = IPADomainCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPADomainCheck' @patch('SSSDConfig.SSSDConfig') def test_ipa_domain_ad(self, mock_sssd): mock_sssd.return_value = SSSDConfig(provider='ad') framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPADomainCheck(registry) self.results = capture_results(f) assert len(self.results) == 4 for result in self.results.results: assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPADomainCheck' assert result.kw.get('provider') == 'ad' class TestTrustCatalog(BaseTest): def test_no_trust_agent(self): framework = object() registry.initialize(framework, config.Config) registry.trust_agent = False f = IPATrustCatalogCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 @patch('pysss_nss_idmap.getnamebysid') @patch('ipapython.ipautil.run') def test_trust_catalog_ok(self, mock_run, mock_getnamebysid): # id Administrator@ad.example dsresult = namedtuple('run', ['returncode', 'error_log']) dsresult.returncode = 0 dsresult.error_log = '' dsresult.output = 'Active servers:\nAD Global Catalog: ' \ 'root-dc.ad.vm\nAD Domain Controller: root-dc.ad.vm\n' \ 'IPA: master.ipa.vm\n\n' ds2result = namedtuple('run', ['returncode', 'error_log']) ds2result.returncode = 0 ds2result.error_log = '' ds2result.output = 'Active servers:\nAD Global Catalog: ' \ 'root-dc.ad.vm\nAD Domain Controller: root-dc.ad.vm\n' \ mock_run.side_effect = [dsresult, dsresult, ds2result] mock_getnamebysid.side_effect = [ {'S-1-5-21-abc-500': {'name': 'admin@ad.example', 'type': 3}}, {'S-1-5-21-ghi-500': {'name': 'admin@child.ad.example', 'type': 3}}, {'S-1-5-21-def-500': {'name': 'admin@child.example', 'type': 3}} ] # get_trust_domains() m_api.Command.trust_find.side_effect = trust_find() m_api.Command.trustdomain_find.side_effect = trustdomain_find() framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustCatalogCheck(registry) self.results = capture_results(f) assert len(self.results) == 9 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustCatalogCheck' assert result.kw.get('key') == 'Domain Security Identifier' assert result.kw.get('sid') == 'S-1-5-21-abc' result = self.results.results[1] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustCatalogCheck' assert result.kw.get('key') == 'AD Global Catalog' assert result.kw.get('domain') == 'ad.example' result = self.results.results[2] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustCatalogCheck' assert result.kw.get('key') == 'AD Domain Controller' assert result.kw.get('domain') == 'ad.example' result = self.results.results[3] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustCatalogCheck' assert result.kw.get('key') == 'Domain Security Identifier' assert result.kw.get('sid') == 'S-1-5-22-def' result = self.results.results[4] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustCatalogCheck' assert result.kw.get('key') == 'AD Global Catalog' assert result.kw.get('domain') == 'child.ad.example' result = self.results.results[5] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustCatalogCheck' assert result.kw.get('key') == 'AD Domain Controller' result = self.results.results[6] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustCatalogCheck' assert result.kw.get('key') == 'Domain Security Identifier' assert result.kw.get('sid') == 'S-1-5-21-ghi' result = self.results.results[7] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustCatalogCheck' assert result.kw.get('key') == 'AD Global Catalog' assert result.kw.get('domain') == 'child.example' result = self.results.results[8] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustCatalogCheck' assert result.kw.get('key') == 'AD Domain Controller' assert result.kw.get('domain') == 'child.example' class Testsidgen(BaseTest): patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), } def test_no_trust_agent(self): framework = object() registry.initialize(framework, config.Config) registry.trust_agent = False f = IPAsidgenpluginCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 def test_sidgen_ok(self): attrs = { 'nsslapd-pluginEnabled': ['on'], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('cn=plugin, cn=config')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPAsidgenpluginCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPAsidgenpluginCheck' assert result.kw.get('key') == 'IPA SIDGEN' result = self.results.results[1] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPAsidgenpluginCheck' assert result.kw.get('key') == 'ipa-sidgen-task' def test_sidgen_fail(self): attrs = { 'nsslapd-pluginEnabled': ['off'], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, DN('cn=plugin, cn=config')) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPAsidgenpluginCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 2 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPAsidgenpluginCheck' assert result.kw.get('key') == 'IPA SIDGEN' result = self.results.results[1] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPAsidgenpluginCheck' assert result.kw.get('key') == 'ipa-sidgen-task' class TestTrustAgentMember(BaseTest): patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), } def test_no_trust_agent(self): framework = object() registry.initialize(framework, config.Config) registry.trust_agent = False f = IPATrustAgentMemberCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 def test_member_ok(self): agent_dn = DN(('fqdn', m_api.env.host), m_api.env.container_host, m_api.env.basedn) group_dn = DN(('cn', 'adtrust agents'), m_api.env.container_sysaccounts, m_api.env.basedn) attrs = { 'memberof': [group_dn], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, agent_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustAgentMemberCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustAgentMemberCheck' assert result.kw.get('key') == m_api.env.host def test_member_fail(self): agent_dn = DN(('fqdn', m_api.env.host), m_api.env.container_host, m_api.env.basedn) attrs = { 'memberof': [agent_dn], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, agent_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_agent = True f = IPATrustAgentMemberCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustAgentMemberCheck' assert result.kw.get('key') == m_api.env.host class TestControllerPrincipal(BaseTest): patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), } def test_not_trust_controller(self): framework = object() registry.initialize(framework, config.Config) registry.trust_controller = False f = IPATrustControllerPrincipalCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 def test_principal_ok(self): agent_dn = DN(('krbprincipalname', 'cifs/%s@%s' % (m_api.env.host, m_api.env.realm)), m_api.env.container_service, m_api.env.basedn) group_dn = DN(('cn', 'adtrust agents'), m_api.env.container_sysaccounts, m_api.env.basedn) attrs = { 'memberof': [group_dn], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, agent_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_controller = True f = IPATrustControllerPrincipalCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustControllerPrincipalCheck' assert result.kw.get('key') == 'cifs/%s@%s' % \ (m_api.env.host, m_api.env.realm) def test_member_fail(self): agent_dn = DN(('fqdn', m_api.env.host), m_api.env.container_host, m_api.env.basedn) attrs = { 'memberof': [agent_dn], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, agent_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_controller = True f = IPATrustControllerPrincipalCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.kw.get('key') == 'cifs/%s@%s' % \ (m_api.env.host, m_api.env.realm) class TestControllerService(BaseTest): patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), } def test_not_trust_controller(self): framework = object() registry.initialize(framework, config.Config) registry.trust_controller = False f = IPATrustControllerServiceCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 def test_service_enabled(self): service_dn = DN(('cn', 'ADTRUST')) for type in [ENABLED_SERVICE, HIDDEN_SERVICE]: attrs = { 'ipaconfigstring': [type], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, service_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_controller = True f = IPATrustControllerServiceCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustControllerServiceCheck' assert result.kw.get('key') == 'ADTRUST' def test_principal_fail(self): service_dn = DN(('cn', 'ADTRUST')) attrs = { 'ipaconfigstring': ['disabledService'], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, service_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_controller = True f = IPATrustControllerServiceCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.kw.get('key') == 'ADTRUST' class TestControllerGroupSID(BaseTest): patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), } def test_not_trust_controller(self): framework = object() registry.initialize(framework, config.Config) registry.trust_controller = False f = IPATrustControllerGroupSIDCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 def test_principal_ok(self): admins_dn = DN(('cn', 'admins')) attrs = { 'ipantsecurityidentifier': ['S-1-5-21-1234-5678-1976041503-512'], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, admins_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_controller = True f = IPATrustControllerGroupSIDCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustControllerGroupSIDCheck' assert result.kw.get('key') == 'ipantsecurityidentifier' assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-512' def test_principal_fail(self): admins_dn = DN(('cn', 'admins')) attrs = { 'ipantsecurityidentifier': ['S-1-5-21-1234-5678-1976041503-500'], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, admins_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_controller = True f = IPATrustControllerGroupSIDCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustControllerGroupSIDCheck' assert result.kw.get('key') == 'ipantsecurityidentifier' assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-500' class TestControllerAdminSID(BaseTest): patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), } def test_not_trust_controller(self): framework = object() registry.initialize(framework, config.Config) registry.trust_controller = False f = IPATrustControllerAdminSIDCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 def test_principal_ok(self): admin_dn = DN(('uid', 'admin')) attrs = { 'ipantsecurityidentifier': ['S-1-5-21-1234-5678-1976041503-500'], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, admin_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_controller = True f = IPATrustControllerAdminSIDCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustControllerAdminSIDCheck' assert result.kw.get('key') == 'ipantsecurityidentifier' assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-500' def test_principal_fail(self): admin_dn = DN(('uid', 'admin')) attrs = { 'ipantsecurityidentifier': ['S-1-5-21-1234-5678-1976041503-400'], } fake_conn = LDAPClient('ldap://localhost', no_schema=True) ldapentry = LDAPEntry(fake_conn, admin_dn) for attr, values in attrs.items(): ldapentry[attr] = values framework = object() registry.initialize(framework, config.Config) registry.trust_controller = True f = IPATrustControllerAdminSIDCheck(registry) f.conn = mock_ldap(ldapentry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustControllerAdminSIDCheck' assert result.kw.get('key') == 'ipantsecurityidentifier' assert result.kw.get('rid') == 'S-1-5-21-1234-5678-1976041503-400' class TestControllerConf(BaseTest): patches = { 'ldap.initialize': Mock(return_value=mock_ldap_conn()), } def test_not_trust_controller(self): framework = object() registry.initialize(framework, config.Config) registry.trust_controller = False f = IPATrustControllerConfCheck(registry) self.results = capture_results(f) # Zero because the call was skipped altogether assert len(self.results) == 0 @patch('ipapython.ipautil.run') def test_ldapi_ok(self, mock_run): ldapi_socket = "ipasam:ldapi://%%2fvar%%2frun%%2fslapd-%s.socket" % \ realm_to_serverid(m_api.env.realm) run_result = namedtuple('run', ['returncode', 'output']) run_result.returncode = 0 run_result.output = '[global]\n\tpassdb backend=%s' % ldapi_socket mock_run.return_value = run_result framework = object() registry.initialize(framework, config.Config) registry.trust_controller = True f = IPATrustControllerConfCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustControllerConfCheck' assert result.kw.get('key') == 'net conf list' class TestPackageCheck(BaseTest): def test_agent_with_package(self): # Note that this test assumes the import is installed framework = object() registry.initialize(framework, config.Config) registry.trust_controller = False registry.trust_agent = True f = IPATrustPackageCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustPackageCheck' def test_agent_without_package(self): # Note that this test assumes the import is installed framework = object() registry.initialize(framework, config.Config) registry.trust_controller = False registry.trust_agent = True # Hose up the module so the import fails save = sys.modules['ipaserver.install'] sys.modules['ipaserver.install'] = 'foo' f = IPATrustPackageCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.WARNING assert result.source == 'ipahealthcheck.ipa.trust' assert result.check == 'IPATrustPackageCheck' sys.modules['ipaserver.install'] = save freeipa-healthcheck-0.10/tests/test_meta.py000066400000000000000000000201261420053437700210150ustar00rootroot00000000000000# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # from base import BaseTest from collections import namedtuple from unittest.mock import patch from util import capture_results from ipahealthcheck.core import config, constants from ipahealthcheck.meta.plugin import registry from ipahealthcheck.meta.core import MetaCheck from ipapython import ipautil from ipaplatform.paths import paths if 'FIPS_MODE_SETUP' not in dir(paths): paths.FIPS_MODE_SETUP = '/usr/bin/fips-mode-setup' def gen_result(returncode, output='', error=''): """ Generate the result of an execution. Creates a run namespace and sets the output as provided. """ run_result = namedtuple( 'run', ['returncode', 'raw_output', 'output_log', 'error_log'] ) run_result.returncode = returncode run_result.raw_output = output.encode('utf-8') run_result.output_log = output run_result.error_log = error return run_result class TestMetaFIPS(BaseTest): @patch('os.path.exists') def test_fips_no_fips_mode_setup(self, mock_exists): mock_exists.return_value = False framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('fips') == 'missing %s' % paths.FIPS_MODE_SETUP @patch('os.path.exists') @patch('ipapython.ipautil.run') def test_fips_disabled(self, mock_run, mock_exists): mock_exists.return_value = True mock_run.side_effect = [ gen_result(2), gen_result(0, output='ACME is disabled'), ] framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('fips') == 'disabled' @patch('os.path.exists') @patch('ipapython.ipautil.run') def test_fips_enabled(self, mock_run, mock_exists): mock_exists.return_value = True mock_run.side_effect = [ gen_result(0), gen_result(0, output='ACME is disabled'), ] framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('fips') == 'enabled' @patch('os.path.exists') @patch('ipapython.ipautil.run') def test_fips_inconsistent(self, mock_run, mock_exists): mock_exists.return_value = True mock_run.side_effect = [ gen_result(1), gen_result(0, output='ACME is disabled'), ] framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('fips') == 'inconsistent' @patch('os.path.exists') @patch('ipapython.ipautil.run') def test_fips_unknown(self, mock_run, mock_exists): mock_exists.return_value = True mock_run.side_effect = [ gen_result(103), gen_result(0, output='ACME is disabled'), ] framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('fips') == 'unknown' @patch('os.path.exists') @patch('ipapython.ipautil.run') def test_fips_failed(self, mock_run, mock_exists): mock_exists.return_value = True mock_run.side_effect = [ ipautil.CalledProcessError( 1, 'fips-mode-setup', output='execution failed' ), gen_result(0, output='ACME is disabled'), ] framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.ERROR assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('fips') == 'failed to check' class TestMetaACME(BaseTest): @patch('os.path.exists') def test_acme_no_ipa_acme_status(self, mock_exists): mock_exists.return_value = False framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('acme') == \ 'missing %s' % '/usr/sbin/ipa-acme-manage' @patch('os.path.exists') @patch('ipapython.ipautil.run') def test_acme_disabled(self, mock_run, mock_exists): mock_exists.return_value = True mock_run.side_effect = [ gen_result(0), gen_result(0, output='ACME is disabled'), ] framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('acme') == 'disabled' @patch('os.path.exists') @patch('ipapython.ipautil.run') def test_acme_enabled(self, mock_run, mock_exists): mock_exists.return_value = True mock_run.side_effect = [ gen_result(0), gen_result(0, output='ACME is enabled'), ] framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('acme') == 'enabled' @patch('os.path.exists') @patch('ipapython.ipautil.run') def test_acme_unknown(self, mock_run, mock_exists): mock_exists.return_value = True mock_run.side_effect = [ gen_result(0), gen_result( 0, error="cannot connect to 'https://somewhere/acme/login" ), ] framework = object() registry.initialize(framework, config.Config()) f = MetaCheck(registry) self.results = capture_results(f) assert len(self.results) == 1 result = self.results.results[0] assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.meta.core' assert result.check == 'MetaCheck' assert result.kw.get('acme') == 'unknown' freeipa-healthcheck-0.10/tests/test_meta_services.py000066400000000000000000000013531420053437700227210ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import capture_results from base import BaseTest from ipahealthcheck.ipa.plugin import registry from ipahealthcheck.meta.services import httpd from ipahealthcheck.core import config class TestServices(BaseTest): def test_simple_service(self): """ Test a service. It was chosen at random. The purpose of this test is to exercise the service check code path and not to confirm that a particular service is running. """ framework = object() registry.initialize(framework, config.Config) f = httpd(registry) self.results = capture_results(f) assert len(self.results) == 1 freeipa-healthcheck-0.10/tests/test_options.py000066400000000000000000000026121420053437700215620ustar00rootroot00000000000000# # Copyright (C) 2022 FreeIPA Contributors see COPYING for license # import argparse import os import tempfile from unittest.mock import patch from ipahealthcheck.core.core import RunChecks from ipahealthcheck.core.plugin import Results options = argparse.Namespace(check=None, source=None, debug=False, indent=2, list_sources=False, output_type='json', output_file=None, verbose=False, version=False, config=None) @patch('ipahealthcheck.core.core.run_service_plugins') @patch('ipahealthcheck.core.core.run_plugins') @patch('ipahealthcheck.core.core.parse_options') def test_options_merge(mock_parse, mock_run, mock_service): """ Test merging file-based and CLI options """ mock_service.return_value = (Results(), []) mock_run.return_value = Results() mock_parse.return_value = options fd, config_path = tempfile.mkstemp() os.close(fd) with open(config_path, "w") as fd: fd.write('[default]\n') fd.write('output_type=human\n') fd.write('indent=5\n') try: run = RunChecks(['ipahealthcheck.registry'], config_path) run.run_healthcheck() # verify two valus that have defaults with our overriden values assert run.options.output_type == 'human' assert run.options.indent == '5' finally: os.remove(config_path) freeipa-healthcheck-0.10/tests/test_plugins.py000066400000000000000000000017531420053437700215550ustar00rootroot00000000000000# # Copyright (C) 2021 FreeIPA Contributors see COPYING for license # import time from ipahealthcheck.core.plugin import Plugin, Registry, Result from ipahealthcheck.core.core import run_plugins from ipahealthcheck.core import constants def test_timeout(): """ Test that timeouts are detected. """ class plugin1(Plugin): def check(self): time.sleep(5) class plugin2(Plugin): def check(self): yield Result(self, constants.SUCCESS, key='test', msg='pass') # Create a registry r = Registry() # Register the plugins r(plugin1) r(plugin2) # Collect the results results = run_plugins(r.get_plugins(), (), None, None, {}, timeout=1) assert len(results.results) == 2 assert results.results[0].result == constants.ERROR assert results.results[0].kw.get('exception') == 'Request timed out' assert results.results[1].result == constants.SUCCESS assert results.results[1].kw.get('msg') == 'pass' freeipa-healthcheck-0.10/tests/test_registry.py000066400000000000000000000014441420053437700217410ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import raises from ipahealthcheck.core.plugin import Plugin, Registry def test_Registry(): """ Test the `ipahealthcheck.core.Registry` class """ class plugin1(Plugin): pass class plugin2(Plugin): pass # Create a registry r = Registry() # Check that TypeError is raised trying to register something that isn't # a class: p = plugin1(r) e = raises(TypeError, r, p) assert str(e) == 'plugin must be callable; got %r' % p # Register the plugins r(plugin1) r(plugin2) # TODO: enforce plugin uniqueness # Test registration names = [plugin.__class__.__name__ for plugin in r.get_plugins()] assert(names == ['plugin1', 'plugin2']) freeipa-healthcheck-0.10/tests/test_results.py000066400000000000000000000043631420053437700215750ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from util import raises from ipahealthcheck.core.plugin import Registry, Plugin, Result, Results from ipahealthcheck.core import constants def test_Result(): """ Test the `ipahealthcheck.plugin.Result` class """ registry = Registry() p = Plugin(registry) # Standard case of passing plugin to Result r = Result(p, constants.SUCCESS) kw = dict(key='value') r = Result(p, constants.SUCCESS, **kw) e = raises(TypeError, Result) assert "__init__() missing 2 required positional arguments: " \ "'plugin' and 'result'" in str(e) # Test passing source and check to Result. This is used for loading # a previous output. try: r = Result(None, constants.SUCCESS) except TypeError as e: assert str(e) == "source and check or plugin must be provided" try: r = Result(None, constants.SUCCESS, source='test') except TypeError as e: assert str(e) == "source and check or plugin must be provided" try: r = Result(None, constants.SUCCESS, check='test') except TypeError as e: assert str(e) == "source and check or plugin must be provided" r = Result(None, constants.SUCCESS, source='test', check='test') # Test results r = Result(p, constants.SUCCESS) results = Results() results.add(r) assert len(results) == 1 r = Result(p, constants.CRITICAL) results2 = Results() results2.add(r) assert len(results2) == 1 results.extend(results2) assert len(results) == 2 output = list(results.output()) assert len(output) == 2 for x in output: assert x['source'] == 'ipahealthcheck.core.plugin' assert x['check'] == 'Plugin' assert x['result'] in (constants.getLevelName(constants.SUCCESS), constants.getLevelName(constants.CRITICAL)) assert len(x['kw']) == 0 def test_getLevel(): assert constants.getLevel('SUCCESS') == constants.SUCCESS assert constants.getLevel('WARNING') == constants.WARNING assert constants.getLevel('ERROR') == constants.ERROR assert constants.getLevel('CRITICAL') == constants.CRITICAL assert constants.getLevel('FOO') == 'FOO' freeipa-healthcheck-0.10/tests/test_suppress.py000066400000000000000000000111371420053437700217550ustar00rootroot00000000000000# # Copyright (C) 2022 FreeIPA Contributors see COPYING for license # import argparse import os import tempfile from unittest.mock import patch from ipahealthcheck.core import constants from ipahealthcheck.core.core import RunChecks from ipahealthcheck.core.plugin import Result, Results, Plugin, duration from ipahealthcheck.system.plugin import Registry from ipahealthcheck.core.output import output_registry, Output options = argparse.Namespace(check=None, source=None, debug=False, indent=2, list_sources=False, output_type='suppresstest', output_file=None, verbose=False, version=False, config=None) outputdata = None @output_registry class SuppressTest(Output): """Test suppression""" options = () def generate(self, data): global outputdata outputdata = data class UserPlugin(Plugin): pass class UserRegistry(Registry): def initialize(self, framework, config, options=None): pass registry = UserRegistry() @registry class PluginOne(UserPlugin): @duration def check(self): yield Result(self, constants.ERROR, key="test1", msg="test1") @registry class PluginTwo(UserPlugin): @duration def check(self): yield Result(self, constants.ERROR, key="test2", msg="test2") @patch('ipahealthcheck.core.core.run_service_plugins') @patch('ipahealthcheck.core.core.parse_options') @patch('ipahealthcheck.core.core.find_registries') def test_suppress_none(mock_find, mock_parse, mock_service): """ Test suppressing plugins """ global outputdata mock_service.return_value = (Results(), []) mock_parse.return_value = options mock_find.return_value = {'test': registry} outputdata = None fd, config_path = tempfile.mkstemp() os.close(fd) with open(config_path, "w") as fd: fd.write('[default]\n') try: run = RunChecks(['test'], config_path) run.run_healthcheck() assert len(outputdata) == 2 finally: os.remove(config_path) @patch('ipahealthcheck.core.core.run_service_plugins') @patch('ipahealthcheck.core.core.parse_options') @patch('ipahealthcheck.core.core.find_registries') def test_suppress_source(mock_find, mock_parse, mock_service): """ Test suppressing plugins """ global outputdata mock_service.return_value = (Results(), []) mock_parse.return_value = options mock_find.return_value = {'test': registry} outputdata = None fd, config_path = tempfile.mkstemp() os.close(fd) with open(config_path, "w") as fd: fd.write('[default]\n') fd.write('[excludes]\n') fd.write('source=test_suppress\n') try: run = RunChecks(['test'], config_path) run.run_healthcheck() assert len(outputdata) == 0 finally: os.remove(config_path) @patch('ipahealthcheck.core.core.run_service_plugins') @patch('ipahealthcheck.core.core.parse_options') @patch('ipahealthcheck.core.core.find_registries') def test_suppress_check(mock_find, mock_parse, mock_service): """ Test suppressing plugins """ global outputdata mock_service.return_value = (Results(), []) mock_parse.return_value = options mock_find.return_value = {'test': registry} outputdata = None fd, config_path = tempfile.mkstemp() os.close(fd) with open(config_path, "w") as fd: fd.write('[default]\n') fd.write('[excludes]\n') fd.write('check=PluginOne\n') try: run = RunChecks(['test'], config_path) run.run_healthcheck() assert len(outputdata) == 1 assert outputdata[0].get('check') == 'PluginTwo' finally: os.remove(config_path) @patch('ipahealthcheck.core.core.run_service_plugins') @patch('ipahealthcheck.core.core.parse_options') @patch('ipahealthcheck.core.core.find_registries') def test_suppress_key(mock_find, mock_parse, mock_service): """ Test suppressing plugins """ global outputdata mock_service.return_value = (Results(), []) mock_parse.return_value = options mock_find.return_value = {'test': registry} outputdata = None fd, config_path = tempfile.mkstemp() os.close(fd) with open(config_path, "w") as fd: fd.write('[default]\n') fd.write('[excludes]\n') fd.write('key=test2\n') try: run = RunChecks(['test'], config_path) run.run_healthcheck() assert len(outputdata) == 1 assert outputdata[0].get('check') == 'PluginOne' assert outputdata[0].get('kw').get('msg') == 'test1' finally: os.remove(config_path) freeipa-healthcheck-0.10/tests/test_system_filesystemspace.py000066400000000000000000000071761420053437700247050ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from __future__ import division from base import BaseTest from unittest.mock import Mock from util import capture_results from collections import namedtuple from ipahealthcheck.core import config, constants from ipahealthcheck.system.plugin import registry from ipahealthcheck.system.filesystemspace import FileSystemSpaceCheck from ipahealthcheck.system.filesystemspace import in_container class TestFileSystemNotEnoughFreeSpace(BaseTest): usage = namedtuple('usage', ['total', 'used', 'free']) usage.total = 2087428096 usage.used = 1628193914 usage.free = 459234182 patches = { 'shutil.disk_usage': Mock(return_value=usage), } def test_filesystem_near_enospc(self): framework = object() registry.initialize(framework, config.Config) f = FileSystemSpaceCheck(registry) self.results = capture_results(f) expected_results = 10 if in_container() else 12 count = 0 for result in self.results.results: if result.result == constants.ERROR: count += 1 assert result.source == 'ipahealthcheck.system.filesystemspace' assert result.check == 'FileSystemSpaceCheck' assert 'free space under threshold' in result.kw.get('msg') else: assert 'free space percentage within' in result.kw.get('msg') assert len(self.results) == expected_results assert count == expected_results / 2 class TestFileSystemNotEnoughFreeSpacePercentage(BaseTest): usage = namedtuple('usage', ['total', 'used', 'free']) usage.total = 10437140480 usage.used = 8913305600 usage.free = 1523834880 patches = { 'shutil.disk_usage': Mock(return_value=usage), } def test_filesystem_risking_fragmentation(self): framework = object() registry.initialize(framework, config.Config) f = FileSystemSpaceCheck(registry) self.results = capture_results(f) expected_results = 10 if in_container() else 12 count = 0 for result in self.results.results: if result.result == constants.ERROR: count += 1 assert result.source == 'ipahealthcheck.system.filesystemspace' assert result.check == 'FileSystemSpaceCheck' assert 'free space percentage under' in result.kw.get('msg') else: assert 'free space within limits' in result.kw.get('msg') assert len(self.results) == expected_results assert count == expected_results / 2 class TestFileSystemEnoughFreeSpace(BaseTest): usage = namedtuple('usage', ['total', 'used', 'free']) usage.total = 10437140480 usage.used = 1523834880 usage.free = 8913305600 patches = { 'shutil.disk_usage': Mock(return_value=usage), } def test_filesystem_with_enough_space(self): framework = object() registry.initialize(framework, config.Config) f = FileSystemSpaceCheck(registry) self.results = capture_results(f) expected_results = 10 if in_container() else 12 for result in self.results.results: assert result.result == constants.SUCCESS assert result.source == 'ipahealthcheck.system.filesystemspace' assert result.check == 'FileSystemSpaceCheck' assert ( 'free space percentage within' in result.kw.get('msg') or 'free space within limits' in result.kw.get('msg') ) assert len(self.results) == expected_results freeipa-healthcheck-0.10/tests/util.py000066400000000000000000000072161420053437700200120ustar00rootroot00000000000000# # Copyright (C) 2019 FreeIPA Contributors see COPYING for license # from ipahealthcheck.core.plugin import Results from unittest.mock import patch, Mock import ipalib from ipapython.dn import DN class ExceptionNotRaised(Exception): """ Exception raised when an *expected* exception is *not* raised during a unit test. """ msg = 'expected %s' def __init__(self, expected): self.expected = expected def __str__(self): return self.msg % self.expected.__name__ def raises(exception, callback, *args, **kw): """ Tests that the expected exception is raised; raises ExceptionNotRaised if test fails. """ try: callback(*args, **kw) except exception as e: return e raise ExceptionNotRaised(exception) def capture_results(f): """ Loop over check() and collect the results. """ results = Results() for result in f.check(): if result is not None: results.add(result) return results class CAInstance: """A bare-bones CAinistance override This is needed to control whether the underlying master is CAless or CAful. """ def __init__(self, enabled=True, crlgen=True): self.enabled = enabled self.crlgen = crlgen def is_configured(self): return self.enabled def is_crlgen_enabled(self): return self.crlgen class KRAInstance: """A bare-bones KRAinistance override This is needed to control whether the underlying master is has a KRA installed or not. """ def __init__(self, installed=True): self.installed = installed def is_installed(self): return self.installed class ServiceBasedRole: """A bare-bones role override This is just enough to satisfy the initialization code so the AD Trust status can be determined. It will always default to false and the registry should be overridden directly in the test cases. """ def __init__(self, attr_name=None, name=None, component_services=None): pass def status(self, api_instance, server=None, attrs_list=("*",)): return [dict()] class ADtrustBasedRole(ServiceBasedRole): """A bare-bones role override This is just enough to satisfy the initialization code so the AD Trust status can be determined. It will always default to false and the registry should be overridden directly in the test cases. """ def __init__(self, attr_name=None, name=None): pass # Mock api. This file needs to be imported before anything that would # import ipalib.api in order for it to be replaced properly. p_api = patch('ipalib.api', autospec=ipalib.api) m_api = p_api.start() m_api.isdone.return_value = True m_api.env = Mock() m_api.env.host = 'server.ipa.example' m_api.env.server = 'server.ipa.example' m_api.env.realm = u'IPA.EXAMPLE' m_api.env.domain = u'ipa.example' m_api.env.basedn = u'dc=ipa,dc=example' m_api.env.container_user = DN(('cn', 'users'), ('cn', 'accounts')) m_api.env.container_group = DN(('cn', 'groups'), ('cn', 'accounts')) m_api.env.container_host = DN(('cn', 'computers'), ('cn', 'accounts')) m_api.env.container_sysaccounts = DN(('cn', 'sysaccounts'), ('cn', 'etc')) m_api.env.container_service = DN(('cn', 'services'), ('cn', 'accounts')) m_api.env.container_masters = DN(('cn', 'masters')) m_api.Backend = Mock() m_api.Command = Mock() m_api.Command.ping.return_value = { u'summary': u'IPA server version 4.4.3. API version 2.215', } def no_exceptions(results): """Given Results ensure that an except was not raised""" for result in results.results: assert 'exception' not in result.kw freeipa-healthcheck-0.10/tox.ini000066400000000000000000000012421420053437700166250ustar00rootroot00000000000000[tox] minversion=2.3.1 envlist=py3,flake8,pep8,pylint [testenv] # sitepackages is needed for ipalib but this confuses the deps for pytest # pep8 and flake8 so those must be installed globally as well. sitepackages=True [testenv:py3] basepython=python3 commands= {envpython} -m pytest [testenv:flake8] basepython=python3 deps=flake8 commands= {envpython} -m flake8 src/ipahealthcheck tests [testenv:lint] deps=pylint setenv= PYTHONPATH={env:PYTHONPATH:}{:}{toxinidir} commands= {envpython} -m pylint --rcfile=pylintrc --load-plugins=pylint_plugins src tests [testenv:pep8] deps=pycodestyle commands= {envpython} -m pycodestyle src/ipahealthcheck tests