pax_global_header00006660000000000000000000000064152147434220014516gustar00rootroot0000000000000052 comment=66fbd0382be49d99cb70410a4696c796684ffbf8 account-utils-1.3.0/000077500000000000000000000000001521474342200143115ustar00rootroot00000000000000account-utils-1.3.0/.github/000077500000000000000000000000001521474342200156515ustar00rootroot00000000000000account-utils-1.3.0/.github/workflows/000077500000000000000000000000001521474342200177065ustar00rootroot00000000000000account-utils-1.3.0/.github/workflows/ci-opensuse.yml000066400000000000000000000041071521474342200226650ustar00rootroot00000000000000name: openSUSE build & test on: [push, pull_request] jobs: build-gcc: runs-on: ubuntu-latest container: registry.opensuse.org/opensuse/tumbleweed:latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install devel packages run: | zypper ref -f zypper --non-interactive in --no-recommends meson gcc libeconf-devel systemd-devel pam-devel libselinux-devel libcap-devel valgrind docbook5-xsl-stylesheets libxslt-tools - name: Setup meson run: meson setup build --auto-features=enabled - name: Compile code run: meson compile -v -C build - name: Run tests run: meson test -v -C build build-clang: runs-on: ubuntu-latest env: CC: clang container: registry.opensuse.org/opensuse/tumbleweed:latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install devel packages run: | zypper ref -f zypper --non-interactive in --no-recommends meson clang llvm-gold gcc libeconf-devel systemd-devel pam-devel libselinux-devel libcap-devel valgrind docbook5-xsl-stylesheets libxslt-tools - name: Setup meson run: meson setup build --auto-features=enabled - name: Compile code run: meson compile -v -C build - name: Run tests run: meson test -v -C build sanitizer: runs-on: ubuntu-latest container: registry.opensuse.org/opensuse/tumbleweed:latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install devel packages run: | zypper ref zypper --non-interactive in --no-recommends meson gcc libeconf-devel systemd-devel pam-devel libselinux-devel libcap-devel valgrind docbook5-xsl-stylesheets libxslt-tools - name: Setup meson run: meson setup build --auto-features=enabled -Db_sanitize=address,undefined - name: Compile code run: meson compile -v -C build - name: Run tests run: meson test -v -C build # meson test -v -C build --wrap='valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1' account-utils-1.3.0/.gitignore000066400000000000000000000001721521474342200163010ustar00rootroot00000000000000# Object files *.o *.ko *.obj *.elf # Libraries *.lib *.a *.la *.lo # Shared objects *.so *.so.* # Misc build *~ *.log account-utils-1.3.0/INSTALL.md000066400000000000000000000007571521474342200157520ustar00rootroot00000000000000# Building and installing account-utils ## Building with Meson account-utils requires Meson 0.61.0 or newer. Building with Meson is quite simple: ```shell $ meson setup build $ meson compile -C build $ meson test -C build $ sudo meson install -C build ``` On openSUSE or SUSE Linux Enterprise, you should add `-Ddistribution=suse` to get adjusted PAM configuration files. If you want to build with the address sanitizer enabled, add `-Db_sanitize=address` as an argument to `meson setup`. account-utils-1.3.0/LICENSE.BSD-2-Clause000066400000000000000000000023631521474342200173020ustar00rootroot00000000000000BSD 2-Clause License Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. account-utils-1.3.0/LICENSE.GPL2000066400000000000000000000431031521474342200160220ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE 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. 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 convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This 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. account-utils-1.3.0/LICENSE.LGPL2.1000066400000000000000000000636421521474342200163070ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! account-utils-1.3.0/NEWS000066400000000000000000000014061521474342200150110ustar00rootroot00000000000000account-utils NEWS -- history of user-visible changes. Copyright (C) 2025, 2026 Thorsten Kukuk . Please enter bug reports at https://github.com/thkukuk/account-utils/issues Version 1.3.0 * meson: try to autodetect distribution * CI: add a systemd-nspawn container test suite * chfn/chsh/passwd: don't return 0 in error case * pwupdd: fix crash in Chfn method (#25) Version 1.2.0 * Several small fixes Version 1.1.0 * pam_unix_ng: start pwaccessd.socket if not running Version 1.0.1 * drop_privs: don't run the check as root * Add pwaccessd and pwupdd manual pages Version 1.0.0 * Many bug fixes and other improvements Version 0.4.0 * Big update, first more or less future complete release Version 0.2.0 * Solve issues fround during security review account-utils-1.3.0/README.md000066400000000000000000000041061521474342200155710ustar00rootroot00000000000000# account-utils The account-utils package contains the utilities and services to do user management and authentication without the need for setuid/setgid bits. This allows the stack to work with `NoNewPrivs` enabled (means setuid/setgid binaries will no longer work). Communication happens via [varlink](https://varlink.org). There are two services: * `pwaccessd` is a systemd socket activated service which provides account information in `passwd` and `shadow` format, checks if the password or account is expired and verifies the password. Normal users have only access to their own passwd and shadow entry, root has access to all accounts. * `pwupdd` is a inetd style socket activated service, which means for every request a own instance is started. It provides methods to change the password, shell and the GECOS field. An user is allowed to modify it's own data after authentication via PAM, root can additional update all passwd and shadow entries via an own method. There are PAM modules: * `pam_unix_ng.so` is a UNIX style PAM module like `pam_unix.so`, except that it uses `pwaccessd` to get access to the account information and do the authentication. If `pwaccessd` is not running, it falls back to traditional, local authentication. For this it needs to run as root. Changing the password is always done local, but there is a `passwd` command which uses `pwupdd` with a PAM stack for this. * `pam_debuginfo.so` is a simple PAM module for debugging purpose, it prints all available relevant information like PAM flags, PAM data, euid, uid, no_new_privs state, etc. There are additional utilities, which don't use the standard glibc functions to modify passwd and shadow, but `pwaccessd` and `pwupdd`: * chage * chfn * chsh * expiry * passwd ## pam_unix_ng.so The `pam_unix_ng.so` PAM module uses `pwaccessd` as backend for authentication and to check if the account is expired. Changing the password is only possible if run as root, no varlink call for this. Use `passwd` from this package instead. If `pwaccessd` is not running, it tries authentication and account expiration itself as fallback. account-utils-1.3.0/TODO000066400000000000000000000001611521474342200147770ustar00rootroot00000000000000- pwupd/pwaccess varlink definition: add OUTPUT fields Bugs: - "passwd -e " reports "password changed" account-utils-1.3.0/example/000077500000000000000000000000001521474342200157445ustar00rootroot00000000000000account-utils-1.3.0/example/check_expired.c000066400000000000000000000015211521474342200207040ustar00rootroot00000000000000//SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include "pwaccess.h" #include "basics.h" int main(int argc, char **argv) { _cleanup_free_ char *error = NULL; bool pwchangeable = false; long daysleft = -1; int r; if (argc != 2) { fprintf(stderr, "Usage: check_expired \n"); return 1; } r = pwaccess_check_expired(argv[1], &daysleft, &pwchangeable, &error); if (r < 0) { if (error) fprintf(stderr, "%s\n", error); else fprintf(stderr, "check_expired failed: %s\n", strerror(-r)); return 1; } printf("Expired: %i\n", r); printf("Days left: %li\n", daysleft); if (pwchangeable) fprintf(stdout, "Password can be changed.\n"); else fprintf(stdout, "Password cannot be changed.\n"); return 0; } account-utils-1.3.0/example/get_account_name.c000066400000000000000000000012571521474342200214100ustar00rootroot00000000000000//SPDX-License-Identifier: GPL-2.0-or-later #include "pwaccess.h" #include "basics.h" int main(int argc, char **argv) { _cleanup_free_ char *error = NULL; _cleanup_free_ char *name = NULL; uid_t uid; int r; if (argc == 1) uid = getuid(); else if (argc == 2) uid = atol(argv[1]); else { fprintf(stderr, "Usage: get_account_name \n"); return 1; } r = pwaccess_get_account_name(uid, &name, &error); if (r < 0) { if (error) fprintf(stderr, "%s\n", error); else fprintf(stderr, "check_expired failed: %s\n", strerror(-r)); return 1; } printf("Your account name: %s\n", name); return 0; } account-utils-1.3.0/example/get_user_record.c000066400000000000000000000031101521474342200212560ustar00rootroot00000000000000//SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include "pwaccess.h" #include "basics.h" int main(int argc, char **argv) { _cleanup_free_ char *error = NULL; _cleanup_(struct_passwd_freep) struct passwd *pw = NULL; _cleanup_(struct_shadow_freep) struct spwd *sp = NULL; bool complete = false; int r; if (argc >= 2) r = pwaccess_get_user_record(-1, argv[1], &pw, &sp, &complete, &error); else r = pwaccess_get_user_record(getuid(), NULL, &pw, &sp, &complete, &error); if (r < 0) { if (error) fprintf (stderr, "%s\n", error); else fprintf (stderr, "get_user_record failed: %s\n", strerror(-r)); return 1; } if (pw == NULL) { fprintf(stderr, "ERROR: no password entry found!\n"); return 1; } printf("Name: %s\n", pw->pw_name); printf("Password: %s\n", strna(sp?sp->sp_pwdp:pw->pw_passwd)); printf("UID: %i\n", pw->pw_uid); printf("GID: %i\n", pw->pw_gid); printf("GECOS: %s\n", strna(pw->pw_gecos)); printf("Dir: %s\n", strna(pw->pw_dir)); printf("Shell: %s\n", strna(pw->pw_shell)); if (sp) { printf("LstChg: %li\n", sp->sp_lstchg); printf("Min: %li\n", sp->sp_min); printf("Max: %li\n", sp->sp_max); printf("Warn: %li\n", sp->sp_warn); printf("Inact: %li\n", sp->sp_inact); printf("Expire: %li\n", sp->sp_expire); printf("Flag: %li\n", sp->sp_flag); } if (!complete) printf("For permission reasons the result is incomplete.\n"); return 0; } account-utils-1.3.0/example/meson.build000066400000000000000000000012451521474342200201100ustar00rootroot00000000000000check_expired = executable ('check_expired', 'check_expired.c', include_directories : inc, link_with : libpwaccess) get_account_name = executable ('get_account_name', 'get_account_name.c', include_directories : inc, link_with : libpwaccess) get_user_record = executable ('get_user_record', 'get_user_record.c', include_directories : inc, link_with : libpwaccess) verify_password = executable ('verify_password', 'verify_password.c', include_directories : inc, link_with : libpwaccess) account-utils-1.3.0/example/verify_password.c000066400000000000000000000014761521474342200213460ustar00rootroot00000000000000//SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include "pwaccess.h" #include "basics.h" int main(int argc, char **argv) { _cleanup_free_ char *error = NULL; bool authenticated = false; bool nullok = true; int r; if (argc < 2) { fprintf(stderr, "Usage: verify_password [password]\n"); return 1; } r = pwaccess_verify_password(argv[1], strempty(argv[2]), nullok, &authenticated, &error); if (r < 0) { if (error) fprintf(stderr, "%s\n", error); else fprintf(stderr, "verify_password failed: %s\n", strerror(-r)); return 1; } if (authenticated) fprintf(stdout, "Access granted.\n"); else { fprintf(stderr, "Access denied!\n"); return 1; } return 0; } account-utils-1.3.0/include/000077500000000000000000000000001521474342200157345ustar00rootroot00000000000000account-utils-1.3.0/include/basics.h000066400000000000000000000050521521474342200173530ustar00rootroot00000000000000//SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include #include #include #include #include #include #define _unused_ __attribute__((unused)) #define _pure_ __attribute__((__pure__)) #define _const_ __attribute__((__const__)) #define _sentinel_ __attribute__((__sentinel__)) /* Takes inspiration from Rust's Option::take() method: reads and returns a pointer, but at the same time * resets it to NULL. See: https://doc.rust-lang.org/std/option/enum.Option.html#method.take */ #define TAKE_GENERIC(var, type, nullvalue) \ ({ \ type *_pvar_ = &(var); \ type _var_ = *_pvar_; \ type _nullvalue_ = nullvalue; \ *_pvar_ = _nullvalue_; \ _var_; \ }) #define TAKE_PTR_TYPE(ptr, type) TAKE_GENERIC(ptr, type, NULL) #define TAKE_PTR(ptr) TAKE_PTR_TYPE(ptr, typeof(ptr)) #define TAKE_FD(fd) TAKE_GENERIC(fd, int, -EBADF) #define mfree(memory) \ ({ \ free(memory); \ (typeof(memory)) NULL; \ }) static inline void freep(void *p) { *(void**)p = mfree(*(void**) p); } static inline void closep(int *fd) { if (*fd >= 0) close(*fd); *fd = -EBADF; } static inline void fclosep(FILE **f) { if (*f) fclose(*f); *f = NULL; } #define _cleanup_(x) __attribute__((__cleanup__(x))) #define _cleanup_close_ _cleanup_(closep) #define _cleanup_fclose_ _cleanup_(fclosep) #define _cleanup_free_ _cleanup_(freep) /* from string-util-fundamental.h */ #define WHITESPACE " \t\n\r" #define streq(a,b) (strcmp((a),(b)) == 0) #define strneq(a, b, n) (strncmp((a), (b), (n)) == 0) #define strcaseeq(a,b) (strcasecmp((a),(b)) == 0) #define strncaseeq(a, b, n) (strncasecmp((a), (b), (n)) == 0) static inline const char *strempty(const char *s) { return s ?:""; } static inline const char *strna(const char *s) { return s ?: "n/a"; } static inline const char *stroom(const char *s) { return s ?: "Out of memory"; } extern char *startswith(const char *s, const char *prefix) _pure_; extern char *endswith(const char *s, const char *suffix) _pure_; static inline bool isempty(const char *a) { return !a || a[0] == '\0'; } account-utils-1.3.0/include/meson.build000066400000000000000000000000701521474342200200730ustar00rootroot00000000000000configure_file(output: 'config.h', configuration: conf) account-utils-1.3.0/include/pwaccess.h000066400000000000000000000031121521474342200177120ustar00rootroot00000000000000//SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include #include #include #include #define PWACCESS_IS_NOT_RUNNING(r) (r == -ECONNREFUSED || r == -ENOENT || r == -ECONNRESET || r == -EACCES) typedef enum { PWA_EXPIRED_NO = 0, /* account is valid */ #define PWA_EXPIRED_NO PWA_EXPIRED_NO PWA_EXPIRED_ACCT = 1, /* account is expired */ #define PWA_EXPIRED_ACCT PWA_EXPIRED_ACCT PWA_EXPIRED_CHANGE_PW = 2, /* password is expired, change password */ #define PWA_EXPIRED_CHANGE_PW PWA_EXPIRED_CHANGE_PW PWA_EXPIRED_PW = 3, /* password is expired, password change not possible */ #define PWA_EXPIRED_PW PWA_EXPIRED_PW } pwa_expire_flag_t; extern struct passwd *struct_passwd_free(struct passwd *var); extern void struct_passwd_freep(struct passwd **var); extern struct spwd *struct_shadow_free(struct spwd *var); extern void struct_shadow_freep(struct spwd **var); /* All returning structs and strings, if not "const", need to be free'd after usage. This are especially name, pw, sp and error. */ extern int pwaccess_check_expired(const char *user, long *daysleft, bool *pwchangeable, char **error); extern int pwaccess_get_account_name(int64_t uid, char **name, char **error); extern int pwaccess_get_user_record(int64_t uid, const char *user, struct passwd **pw, struct spwd **sp, bool *complete, char **error); extern int pwaccess_verify_password(const char *user, const char *password, bool nullok, bool *ret_authenticated, char **error); account-utils-1.3.0/lib/000077500000000000000000000000001521474342200150575ustar00rootroot00000000000000account-utils-1.3.0/lib/libpwaccess.map000066400000000000000000000003641521474342200200600ustar00rootroot00000000000000LIBPWACCESS_0.4 { global: pwaccess_check_expired; pwaccess_get_account_name; pwaccess_get_user_record; pwaccess_verify_password; struct_passwd_freep; struct_passwd_free; struct_shadow_freep; struct_shadow_free; local: *; }; account-utils-1.3.0/lib/varlink.c000066400000000000000000000422321521474342200166740ustar00rootroot00000000000000//SPDX-License-Identifier: LGPL-2.1-or-later #include "config.h" #include #include #include #include "basics.h" #include "pwaccess.h" static int connect_to_pwaccessd(sd_varlink **ret, const char *socket, char **error) { _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; int r; r = sd_varlink_connect_address(&link, socket); if (r < 0) { if (error) if (asprintf (error, "Failed to connect to %s: %s", socket, strerror(-r)) < 0) { error = NULL; r = -ENOMEM; } return r; } /* Mark anything we get from the service as sensitive */ r = sd_varlink_set_input_sensitive(link); if (r < 0) { if (error) if (asprintf (error, "Failed to enable sensitive Varlink input: %s", strerror(-r)) < 0) { error = NULL; r = -ENOMEM; } return r; } *ret = TAKE_PTR(link); return 0; } struct user_record { bool success; char *error; bool complete; bool pwchangeable; int expired; long daysleft; char *account_name; sd_json_variant *content_passwd; sd_json_variant *content_shadow; }; struct passwd * struct_passwd_free(struct passwd *var) { var->pw_name = mfree(var->pw_name); if (var->pw_passwd) { explicit_bzero(var->pw_passwd, strlen(var->pw_passwd)); var->pw_passwd = mfree(var->pw_passwd); } var->pw_gecos = mfree(var->pw_gecos); var->pw_dir = mfree(var->pw_dir); var->pw_shell = mfree(var->pw_shell); return NULL; } void struct_passwd_freep(struct passwd **var) { if (!var || !*var) return; struct_passwd_free(*var); *var = mfree(*var); } struct spwd * struct_shadow_free(struct spwd *var) { var->sp_namp = mfree(var->sp_namp); if (var->sp_pwdp) { explicit_bzero(var->sp_pwdp, strlen(var->sp_pwdp)); var->sp_pwdp = mfree(var->sp_pwdp); } return NULL; } void struct_shadow_freep(struct spwd **var) { if (!var || !*var) return; struct_shadow_free(*var); *var = mfree(*var); } static void user_record_free(struct user_record *var) { var->error = mfree(var->error); var->account_name = mfree(var->account_name); var->content_passwd = sd_json_variant_unref(var->content_passwd); var->content_shadow = sd_json_variant_unref(var->content_shadow); } /* long is different on 32bit and 64bit architectures, but we don't have a sd_json_dispatch_long function */ struct spwd64 { char *sp_namp; char *sp_pwdp; int64_t sp_lstchg; int64_t sp_min; int64_t sp_max; int64_t sp_warn; int64_t sp_inact; int64_t sp_expire; uint64_t sp_flag; }; static inline int assign_check_range(long *dest, int64_t src) { if (src > LONG_MAX) return -EOVERFLOW; *dest = (long)src; return 0; } int pwaccess_get_user_record(int64_t uid, const char *user, struct passwd **ret_pw, struct spwd **ret_sp, bool *complete, char **error) { _cleanup_(user_record_free) struct user_record p = { .success = false, .error = NULL, .account_name = NULL, .complete = false, .content_passwd = NULL, .content_shadow = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "Success", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct user_record, success), 0 }, { "ErrorMsg", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct user_record, error), 0 }, { "Complete", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct user_record, complete), 0 }, { "passwd", SD_JSON_VARIANT_OBJECT, sd_json_dispatch_variant, offsetof(struct user_record, content_passwd), SD_JSON_NULLABLE }, { "shadow", SD_JSON_VARIANT_OBJECT, sd_json_dispatch_variant, offsetof(struct user_record, content_shadow), SD_JSON_NULLABLE }, {} }; static const sd_json_dispatch_field dispatch_passwd_table[] = { { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_name), SD_JSON_MANDATORY }, { "passwd", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_passwd), SD_JSON_NULLABLE }, { "UID", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, offsetof(struct passwd, pw_uid), SD_JSON_MANDATORY }, { "GID", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, offsetof(struct passwd, pw_gid), SD_JSON_MANDATORY }, { "GECOS", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_gecos), SD_JSON_NULLABLE }, { "dir", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_dir), SD_JSON_NULLABLE }, { "shell", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_shell), SD_JSON_NULLABLE }, {} }; _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; _cleanup_(struct_passwd_freep) struct passwd *pw = NULL; _cleanup_(struct_shadow_freep) struct spwd *sp = NULL; sd_json_variant *result = NULL; const char *error_id = NULL; int r; r = connect_to_pwaccessd(&link, _VARLINK_PWACCESS_SOCKET, error); if (r < 0) return r; if (uid < 0 && user == NULL) { fprintf(stderr, "Invalid combination of UID and user name provided (-1 and NULL)\n"); return -EINVAL; } if (uid >= 0) r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_INTEGER("uid", uid)); if (r >= 0 && user) r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_STRING("userName", user)); if (r < 0) { fprintf(stderr, "Failed to build param list: %s\n", strerror(-r)); return r; } r = sd_varlink_call(link, "org.openSUSE.pwaccess.GetUserRecord", params, &result, &error_id); if (r < 0) { fprintf(stderr, "Failed to call GetUserRecord method: %s\n", strerror(-r)); return r; } /* dispatch before checking error_id, we may need the result for the error message */ r = sd_json_dispatch(result, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) { fprintf(stderr, "Failed to parse JSON answer: %s\n", strerror(-r)); return r; } if (error_id && strlen(error_id) > 0) { int retval = -EIO; if (error) { if (p.error) *error = TAKE_PTR(p.error); else { *error = strdup(error_id); if (*error == NULL) retval = -ENOMEM; } } /* Yes, we will overwrite a possible ENOMEM, but this shouldn't matter here */ if (streq(error_id, "org.openSUSE.pwaccess.NoEntryFound")) retval = -ENODATA; return retval; } if (!p.success) /* we should never have this case, but be safe */ { if (error) *error = TAKE_PTR(p.error); return -EIO; } if (sd_json_variant_is_null(p.content_passwd)) { printf("No entry found\n"); return 0; } pw = calloc(1, sizeof(struct passwd)); if (pw == NULL) return -ENOMEM; r = sd_json_dispatch(p.content_passwd, dispatch_passwd_table, SD_JSON_ALLOW_EXTENSIONS, pw); if (r < 0) { fprintf(stderr, "Failed to parse JSON passwd entry: %s\n", strerror(-r)); return r; } if (!sd_json_variant_is_null(p.content_shadow) && sd_json_variant_elements(p.content_shadow) > 0) { struct spwd64 sp64 = {NULL, NULL, 0, 0, 0, 0, 0, 0, 0}; static const sd_json_dispatch_field dispatch_shadow_table[] = { { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct spwd64, sp_namp), SD_JSON_MANDATORY }, { "passwd", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct spwd64, sp_pwdp), SD_JSON_NULLABLE }, { "lstchg", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd64, sp_lstchg), 0 }, { "min", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd64, sp_min), 0 }, { "max", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd64, sp_max), 0 }, { "warn", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd64, sp_warn), 0 }, { "inact", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd64, sp_inact), 0 }, { "expire", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd64, sp_expire), 0 }, { "flag", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd64, sp_flag), 0 }, {} }; r = sd_json_dispatch(p.content_shadow, dispatch_shadow_table, SD_JSON_ALLOW_EXTENSIONS, &sp64); if (r < 0) { fprintf(stderr, "Failed to parse JSON shadow entry: %s\n", strerror(-r)); return r; } sp = calloc(1, sizeof(struct spwd)); if (sp == NULL) return -ENOMEM; sp->sp_namp = sp64.sp_namp; sp->sp_pwdp = sp64.sp_pwdp; r = assign_check_range(&sp->sp_lstchg, sp64.sp_lstchg); if (r < 0) return r; r = assign_check_range(&sp->sp_min, sp64.sp_min); if (r < 0) return r; r = assign_check_range(&sp->sp_max, sp64.sp_max); if (r < 0) return r; r = assign_check_range(&sp->sp_warn, sp64.sp_warn); if (r < 0) return r; r = assign_check_range(&sp->sp_inact, sp64.sp_inact); if (r < 0) return r; r = assign_check_range(&sp->sp_expire, sp64.sp_expire); if (r < 0) return r; /* sp_flag is ~0ul if unset, but sd-json/sd-varlink convert this always to -1 */ if (sp64.sp_flag == (uint64_t)-1) sp->sp_flag = ~0ul; else sp->sp_flag = sp64.sp_flag; } if (complete) *complete = p.complete; if (ret_pw) *ret_pw = TAKE_PTR(pw); if (ret_sp) *ret_sp = TAKE_PTR(sp); return 0; } int pwaccess_verify_password(const char *user, const char *password, bool nullok, bool *ret_authenticated, char **error) { _cleanup_(user_record_free) struct user_record p = { .success = false, .error = NULL, .account_name = NULL, .content_passwd = NULL, .content_shadow = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "Success", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct user_record, success), 0 }, { "ErrorMsg", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct user_record, error), 0 }, {} }; _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; sd_json_variant *result = NULL; const char *error_id = NULL; int r; /* make sure caller does not ignore error code but uses this instead */ if (ret_authenticated) *ret_authenticated = false; if (!user || !ret_authenticated) return -EINVAL; r = connect_to_pwaccessd(&link, _VARLINK_PWACCESS_SOCKET, error); if (r < 0) return r; r = sd_json_buildo(¶ms, SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(user)), SD_JSON_BUILD_PAIR("password", SD_JSON_BUILD_STRING(strempty(password))), SD_JSON_BUILD_PAIR("nullOK", SD_JSON_BUILD_BOOLEAN(nullok))); if (r < 0) { fprintf(stderr, "Failed to build param list: %s\n", strerror(-r)); return r; } sd_json_variant_sensitive(params); /* password is sensitive */ r = sd_varlink_call(link, "org.openSUSE.pwaccess.VerifyPassword", params, &result, &error_id); if (r < 0) { fprintf(stderr, "Failed to call VerifyPassword method: %s\n", strerror(-r)); return r; } /* dispatch before checking error_id, we may need the result for the error message */ r = sd_json_dispatch(result, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) { fprintf(stderr, "Failed to parse JSON answer: %s\n", strerror(-r)); return r; } if (error_id && strlen(error_id) > 0) { int retval = -EIO; if (error) { if (p.error) *error = TAKE_PTR(p.error); else { *error = strdup(error_id); if (*error == NULL) retval = -ENOMEM; } } /* Yes, we will overwrite a possible ENOMEM, but this shouldn't matter here */ if (streq(error_id, "org.openSUSE.pwaccess.NoEntryFound")) retval = -ENODATA; return retval; } if (!p.success) /* no success and no error means password does not match */ { if (error) *error = TAKE_PTR(p.error); return 0; } *ret_authenticated = true; return 0; } /* return values: < 0: error occured 0: all fine > 0: PWA_EXPIRED_* */ int pwaccess_check_expired(const char *user, long *daysleft, bool *pwchangeable, char **error) { _cleanup_(user_record_free) struct user_record p = { .success = false, .error = NULL, .account_name = NULL, .expired = 0, .daysleft = -1, .pwchangeable = true, /* if we don't get a no, no shadow information exist and thus it's changeable */ .content_passwd = NULL, .content_shadow = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "Success", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct user_record, success), 0 }, { "ErrorMsg", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct user_record, error), 0 }, { "Expired", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, offsetof(struct user_record, expired), 0 }, { "DaysLeft", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct user_record, daysleft), 0 }, { "PWChangeAble", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct user_record, pwchangeable), 0 }, {} }; _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; sd_json_variant *result = NULL; const char *error_id = NULL; int r; if (!user) return -EINVAL; r = connect_to_pwaccessd(&link, _VARLINK_PWACCESS_SOCKET, error); if (r < 0) return r; r = sd_json_buildo(¶ms, SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(user))); if (r < 0) { fprintf(stderr, "Failed to build param list: %s\n", strerror(-r)); return r; } r = sd_varlink_call(link, "org.openSUSE.pwaccess.ExpiredCheck", params, &result, &error_id); if (r < 0) { fprintf(stderr, "Failed to call ExpiredCheck method: %s\n", strerror(-r)); return r; } /* dispatch before checking error_id, we may need the result for the error message */ r = sd_json_dispatch(result, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) { fprintf(stderr, "Failed to parse JSON answer: %s\n", strerror(-r)); return r; } if (error_id && strlen(error_id) > 0) { int retval = -EIO; if (error) { if (p.error) *error = TAKE_PTR(p.error); else { *error = strdup(error_id); if (*error == NULL) retval = -ENOMEM; } } /* Yes, we will overwrite a possible ENOMEM, but this shouldn't matter here */ if (streq(error_id, "org.openSUSE.pwaccess.NoEntryFound")) retval = -ENODATA; return retval; } if (!p.success) { if (error) *error = TAKE_PTR(p.error); return -EPROTO; } if (daysleft && p.daysleft >= 0) *daysleft = p.daysleft; if (pwchangeable) *pwchangeable = p.pwchangeable; return p.expired; } int pwaccess_get_account_name(int64_t uid, char **name, char **error) { _cleanup_(user_record_free) struct user_record p = { .success = false, .error = NULL, .account_name = NULL, .content_passwd = NULL, .content_shadow = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "Success", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct user_record, success), 0 }, { "ErrorMsg", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct user_record, error), 0 }, { "userName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct user_record, account_name), 0 }, {} }; _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; sd_json_variant *result = NULL; const char *error_id = NULL; int r; if (name) *name = NULL; if (uid < 0) return -EINVAL; r = connect_to_pwaccessd(&link, _VARLINK_PWACCESS_SOCKET, error); if (r < 0) return r; r = sd_json_buildo(¶ms, SD_JSON_BUILD_PAIR("uid", SD_JSON_BUILD_INTEGER(uid))); if (r < 0) { fprintf(stderr, "Failed to build param list: %s\n", strerror(-r)); return r; } r = sd_varlink_call(link, "org.openSUSE.pwaccess.GetAccountName", params, &result, &error_id); if (r < 0) { fprintf(stderr, "Failed to call GetAccountName method: %s\n", strerror(-r)); return r; } /* dispatch before checking error_id, we may need the result for the error message */ r = sd_json_dispatch(result, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) { fprintf(stderr, "Failed to parse JSON answer: %s\n", strerror(-r)); return r; } if (error_id && strlen(error_id) > 0) { int retval = -EIO; if (error) { if (p.error) *error = TAKE_PTR(p.error); else { *error = strdup(error_id); if (*error == NULL) retval = -ENOMEM; } } /* Yes, we will overwrite a possible ENOMEM, but this shouldn't matter here */ if (streq(error_id, "org.openSUSE.pwaccess.NoEntryFound")) retval = -ENODATA; return retval; } if (!p.success) { if (error) *error = TAKE_PTR(p.error); return 0; } if (name) *name = TAKE_PTR(p.account_name); return 0; } account-utils-1.3.0/libclient/000077500000000000000000000000001521474342200162565ustar00rootroot00000000000000account-utils-1.3.0/libclient/chauthtok.c000066400000000000000000000055341521474342200204230ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include "chauthtok.h" #include "basics.h" #include "varlink-client-common.h" #define USEC_INFINITY ((uint64_t) UINT64_MAX) int chauthtok(const char *user, int pam_flags) { _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; _cleanup_free_ char *error = NULL; struct callback_data cb_data = { .resp = NULL, .error_code = 0 }; int r; r = connect_to_pwupdd(&link, _VARLINK_PWUPD_SOCKET, &error); if (r < 0) { if (error) fprintf(stderr, "%s\n", error); else fprintf(stderr, "Cannot connect to pwupd! (%s)\n", strerror(-r)); return -r; } sd_varlink_set_userdata(link, &cb_data); r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_STRING("userName", user)); if (r < 0) { fprintf(stderr, "Failed to build param list: %s\n", strerror(-r)); return -r; } if (pam_flags) { r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_INTEGER("flags", pam_flags)); if (r < 0) { fprintf(stderr, "Failed to build param list: %s\n", strerror(-r)); return -r; } } r = sd_varlink_bind_reply(link, reply_callback); if (r < 0) { fprintf(stderr, "Failed to bind reply callback: %s\n", strerror(-r)); return -r; } r = sd_varlink_observe(link, "org.openSUSE.pwupd.Chauthtok", params); if (r < 0) { fprintf(stderr, "Failed to call chauthtok method: %s\n", strerror(-r)); return -r; } loop: for (;;) { r = sd_varlink_is_idle(link); if (r < 0) { fprintf(stderr, "Failed to check if varlink connection is idle: %s\n", strerror(-r)); return -r; } if (r > 0) break; r = sd_varlink_process(link); if (r < 0) { fprintf(stderr, "Failed to process varlink connection: %s\n", strerror(-r)); return -r; } if (r != 0) continue; r = sd_varlink_wait(link, USEC_INFINITY); if (r < 0) { fprintf(stderr, "Failed to wait for varlink connection events: %s\n", strerror(-r)); return -r; } } if (cb_data.resp) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *answer = NULL; r = sd_json_buildo(&answer, SD_JSON_BUILD_PAIR("response", SD_JSON_BUILD_STRING(cb_data.resp->resp))); if (r < 0) { fprintf(stderr, "Failed to build response list: %s\n", strerror(-r)); return -r; } free(cb_data.resp->resp); cb_data.resp = mfree(cb_data.resp); sd_json_variant_sensitive(answer); /* password is sensitive */ r = sd_varlink_observe(link, "org.openSUSE.pwupd.Conv", answer); if (r < 0) { fprintf(stderr, "Failed to call conv method: %s\n", strerror(-r)); return -r; } goto loop; } return cb_data.error_code; } account-utils-1.3.0/libclient/chauthtok.h000066400000000000000000000001641521474342200204220ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once extern int chauthtok(const char *user, int pam_flags); account-utils-1.3.0/libclient/drop_privs.c000066400000000000000000000015351521474342200206150ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include "drop_privs.h" int drop_privs(void) { /* drop gid */ if (setgid(getgid()) != 0) return -errno; /* drop uid */ if (setuid(getuid()) != 0) return -errno; /* Try to regain root. If this succeeds, we failed to drop privileges. Don't do this as root, else the check will fail. */ if (getuid() != 0 && setuid(0) != -1) return -EPERM; return 0; } int check_and_drop_privs(void) { int r; if (geteuid() == getuid() && getegid() == getgid()) return 0; fprintf(stderr, "Binary has the setuid or setgid bit set, please remove it.\n"); r = drop_privs(); if (r < 0) { fprintf(stderr, "Dropping privileges failed: %s\n", strerror(-r)); return r; } return 0; } account-utils-1.3.0/libclient/drop_privs.h000066400000000000000000000002011521474342200206070ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #pragma once extern int drop_privs(void); extern int check_and_drop_privs(void); account-utils-1.3.0/libclient/get_logindefs.c000066400000000000000000000033131521474342200212330ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-2.1-or-later #include "config.h" #include #include "basics.h" #include "get_logindefs.h" static econf_err load_logindefs_config(econf_file **key_file) { return econf_readConfig(key_file, NULL /* project */, _PATH_VENDORDIR /* usr_conf_dir */, "login" /* config_name */, "defs" /* config_suffix */, "= \t" /* delim */, "#" /* comment */); } long get_logindefs_num(const char *key, long def) { _cleanup_(econf_freeFilep) econf_file *key_file = NULL; int32_t val; econf_err error; error = load_logindefs_config(&key_file); if (error != ECONF_SUCCESS) { fprintf(stderr, "Cannot parse login.defs: %s\n", econf_errString(error)); return def; } error = econf_getIntValueDef(key_file, NULL, key, &val, def); if (error != ECONF_SUCCESS) { if (error != ECONF_NOKEY) fprintf(stderr, "Error reading '%s': %s\n", key, econf_errString(error)); return def; } return val; } char * get_logindefs_string(const char *key, const char *def) { _cleanup_(econf_freeFilep) econf_file *key_file = NULL; char *val; econf_err error; error = load_logindefs_config(&key_file); if (error != ECONF_SUCCESS) { fprintf(stderr, "Cannot parse login.defs: %s\n", econf_errString(error)); return strdup(def); } error = econf_getStringValueDef(key_file, NULL, key, &val, def); if (error != ECONF_SUCCESS) { if (error != ECONF_NOKEY) fprintf(stderr, "Error reading '%s': %s\n", key, econf_errString(error)); return strdup(def); } return val; } account-utils-1.3.0/libclient/get_logindefs.h000066400000000000000000000002741521474342200212430ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once extern long get_logindefs_num(const char *key, long def); extern char *get_logindefs_string(const char *key, const char *def); account-utils-1.3.0/libclient/get_value.c000066400000000000000000000033411521474342200203760ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include "basics.h" #include "get_value.h" /* prompt the user with the name of the field being changed and the current value. return value: 0 -> success < 0 -> error, -errno as code input argument: NULL -> Ctrl-D was pressed "" -> Field was cleard by user. User can enter a space or "none" to do this. def -> User entered only . input -> User entered something new . */ int get_value(const char *def, const char *prompt, char **input) { char buf[BUFSIZ]; char *cp; *input = NULL; printf("\t%s [%s]: ", prompt, strempty(def)); if (fgets(buf, sizeof(buf), stdin) != buf) { /* print newline to get defined output. */ printf("\n"); return 0; } if ((cp = strchr(buf, '\n')) != NULL) *cp = '\0'; if (buf[0]) /* something is entered */ { /* if none is entered, return an empty string. If somebody wishes to enter "none", he as to add a space. */ if(strcasecmp("none", buf) == 0) { *input = strdup(""); if (*input == NULL) return -ENOMEM; return 0; } /* Remove leading and trailing whitespace. This also makes it possible to change the field to empty or "none" by entering a space. */ /* cp should point to the trailing '\0'. */ cp = &buf[strlen(buf)]; while(--cp >= buf && isspace(*cp)) ; *++cp = '\0'; cp = buf; while (*cp && isspace(*cp)) cp++; *input = strdup(cp); if (*input == NULL) return -ENOMEM; return 0; } *input = strdup(strempty(def)); if (*input == NULL) return -ENOMEM; return 0; } account-utils-1.3.0/libclient/get_value.h000066400000000000000000000002021521474342200203740ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #pragma once extern int get_value(const char *def, const char *prompt, char **input); account-utils-1.3.0/libclient/varlink-client-common.c000066400000000000000000000117171521474342200226410ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-2.1-or-later #include "config.h" #include #include #include #include "basics.h" #include "varlink-client-common.h" struct pam_conv conv = { misc_conv, NULL }; struct result * struct_result_free(struct result *var) { var->error = mfree((char *)var->error); return NULL; } int connect_to_pwupdd(sd_varlink **ret, const char *socket, char **error) { _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; int r; r = sd_varlink_connect_address(&link, socket); if (r < 0) { if (error) if (asprintf (error, "Failed to connect to %s: %s", socket, strerror(-r)) < 0) { error = NULL; r = -ENOMEM; } return r; } /* Mark anything we get from the service as sensitive */ r = sd_varlink_set_input_sensitive(link); if (r < 0) { if (error) if (asprintf (error, "Failed to enable sensitive Varlink input: %s", strerror(-r)) < 0) { error = NULL; r = -ENOMEM; } return r; } *ret = TAKE_PTR(link); return 0; } static struct pam_message * pam_message_free(struct pam_message *var) { var->msg = mfree((char *)var->msg); return NULL; } int reply_callback(sd_varlink *link _unused_, sd_json_variant *parameters, const char *error, sd_varlink_reply_flags_t flags _unused_, void *userdata) { struct callback_data *cb_data = userdata; struct pam_response **resp = &cb_data->resp; _cleanup_(pam_message_free) struct pam_message pmsg = { .msg_style = -1, .msg = NULL }; static const sd_json_dispatch_field dispatch_pmsg_table[] = { { "msg_style", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, offsetof(struct pam_message, msg_style), SD_JSON_MANDATORY }, { "message", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct pam_message, msg), SD_JSON_NULLABLE }, {} }; _cleanup_(struct_result_free) struct result p = { .success = false, .error = NULL, }; static const sd_json_dispatch_field dispatch_result_table[] = { { "Success", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct result, success), SD_JSON_MANDATORY }, { "ErrorMsg", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct result, error), 0 }, {} }; int r; assert(*resp == NULL); if (error) { r = sd_json_dispatch(parameters, dispatch_result_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) { /* Mandatory field not found, so no pam_message but final end message */ fprintf(stderr, "Failed to parse JSON answer (result) for error '%s': %s\n", error, strerror(-r)); return r; } /* XXX Don't print error here, let the caller print it */ if (p.success || !p.error) /* Oops, something did go wrong */ fprintf(stderr, "Method call failed: %s\n", error); else fprintf(stderr, "%s\n", p.error); if (startswith(error, "org.openSUSE.")) { /* We need to our own errors ourself */ if (streq(error, "org.openSUSE.pwaccess.NoEntryFound") || streq(error, "org.openSUSE.pwupd.NoEntryFound")) r = -ENODATA; /* XXX more error codes */ else r = -EBADR; } else { /* If we can translate this to an errno, let's print that as errno and return it, otherwise, return a generic error code. */ r = sd_varlink_error_to_errno(error, parameters); } /* Store error code for caller to handle */ cb_data->error_code = -r; return 0; /* Don't abort the connection, let caller check error_code */ } //sd_json_variant_dump(parameters, SD_JSON_FORMAT_NEWLINE, stdout, NULL); r = sd_json_dispatch(parameters, dispatch_pmsg_table, SD_JSON_ALLOW_EXTENSIONS, &pmsg); if (r < 0) { /* Mandatory field not found, so no pam_message but final end message */ if (r != -ENXIO) { fprintf(stderr, "Failed to parse JSON answer (pam_message): %s\n", strerror(-r)); return r; } r = sd_json_dispatch(parameters, dispatch_result_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) { /* Mandatory field not found, so no pam_message but final end message */ fprintf(stderr, "Failed to parse JSON answer (result): %s\n", strerror(-r)); return r; } /* This should never happen */ if (!p.success) { if (p.error) fprintf(stderr, "%s\n", p.error); else fprintf(stderr, "Error while changing account data.\n"); return -EBADMSG; } } else /* got pam_message */ { const struct pam_message *arg = &pmsg; r = conv.conv(1, &arg, resp, conv.appdata_ptr); if (r != PAM_SUCCESS) { fprintf(stderr, "misc_conv() failed: %s\n", pam_strerror(NULL, r)); return -EBADMSG; } if (*resp && pmsg.msg_style != PAM_PROMPT_ECHO_OFF && pmsg.msg_style != PAM_PROMPT_ECHO_ON) { if ((*resp)->resp) free((*resp)->resp); *resp = mfree(*resp); } } return 0; } account-utils-1.3.0/libclient/varlink-client-common.h000066400000000000000000000010341521474342200226350ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include extern struct pam_conv conv; struct result { bool success; char *error; }; extern struct result *struct_result_free(struct result *var); struct callback_data { struct pam_response *resp; int error_code; }; extern int connect_to_pwupdd(sd_varlink **ret, const char *socket, char **error); extern int reply_callback(sd_varlink *link, sd_json_variant *parameters, const char *error, sd_varlink_reply_flags_t flags, void *userdata); account-utils-1.3.0/libcommon/000077500000000000000000000000001521474342200162705ustar00rootroot00000000000000account-utils-1.3.0/libcommon/check_caller_perms.c000066400000000000000000000013701521474342200222420ustar00rootroot00000000000000//SPDX-License-Identifier: LGPL-2.1-or-later #include "config.h" #include #include "check_caller_perms.h" /* Do not allow access if the query does not originate from root or the entry does not belong to the calling user. Exception: if the peer uid is in the list of exceptions. "Lex mariadb": user mysql/mariadb needs to authenticate as database user so that the database user can get access to the database. */ bool check_caller_perms(uid_t peer_uid, uid_t target_uid, uid_t *allowed) { if (peer_uid == 0) return true; if (peer_uid == target_uid) return true; if (!allowed) return false; for (size_t i = 0; allowed[i] != 0; i++) if (peer_uid == allowed[i]) return true; return false; } account-utils-1.3.0/libcommon/check_caller_perms.h000066400000000000000000000002471521474342200222510ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include extern bool check_caller_perms(uid_t peer_uid, uid_t target_uid, uid_t *allowed); account-utils-1.3.0/libcommon/chfn_checks.c000066400000000000000000000065411521474342200207000ustar00rootroot00000000000000//SPDX-License-Identifier: LGPL-2.1-or-later #include "config.h" #include #include #include #include #include #include "basics.h" #include "chfn_checks.h" static const char * get_chfn_restrict(char **ret_error) { static const char *value = NULL; _cleanup_(econf_freeFilep) econf_file *key_file = NULL; econf_err error; char *val = NULL; if (value) return value; error = econf_readConfig(&key_file, NULL /* project */, _PATH_VENDORDIR /* usr_conf_dir */, "login" /* config_name */, "defs" /* config_suffix */, "= \t" /* delim */, "#" /* comment */); if (error != ECONF_SUCCESS) { if (ret_error) { _cleanup_free_ char *errstr = NULL; if (asprintf(&errstr, "Cannot parse login.defs: %s", econf_errString(error)) < 0) *ret_error = NULL; else *ret_error = TAKE_PTR(errstr); } return ""; /* be very restrictive, allow nothing */ } error = econf_getStringValue (key_file, NULL, "CHFN_RESTRICT", &val); if (error != ECONF_SUCCESS) { if (ret_error) { _cleanup_free_ char *errstr = NULL; if (asprintf(&errstr, "Error reading CHFN_RESTRICT: %s", econf_errString(error)) < 0) *ret_error = NULL; else *ret_error = TAKE_PTR(errstr); } return ""; } else value = val; return value; } bool may_change_field(uid_t uid, char field, char **error) { const char *cp; /* root is always allowed to change everything. */ if (uid == 0) return true; /* CHFN_RESTRICT specifies exactly which fields may be changed by regular users. */ cp = get_chfn_restrict(error); if (error && *error) return false; if (strchr(cp, field)) return true; return false; } /* convert a multibye string to a wide character string, so that we can use iswprint. */ static int mbstowcs_alloc (const char *string, wchar_t **ret) { size_t size; _cleanup_free_ wchar_t *buf = NULL; if (!string) return -EINVAL; size = mbstowcs(NULL, string, 0); if (size == (size_t) -1) // equal to size == SIZE_MAX return -EINVAL; buf = calloc(size + 1, sizeof(wchar_t)); if (buf == NULL) return -ENOMEM; size = mbstowcs(buf, string, size + 1); if (size == (size_t) -1) return -EINVAL; *ret = TAKE_PTR(buf); return 0; } bool chfn_check_string(const char *string, const char *illegal, char **error) { _cleanup_free_ wchar_t *wstr = NULL; _cleanup_free_ wchar_t *willegal = NULL; int r; if (error) *error = NULL; r = mbstowcs_alloc(string, &wstr); if (r < 0) { if (error) *error = strdup(strerror(-r)); return false; } r = mbstowcs_alloc(illegal, &willegal); if (r < 0) { if (error) *error = strdup(strerror(-r)); return false; } for (size_t i = 0; i < wcslen(wstr); i++) { wchar_t c = wstr[i]; if (wcschr(willegal, c) != NULL || c == '\n') { if (error) if (asprintf(error, "The characters '%s\\n' are not allowed.", illegal) < 0) *error = NULL; return false; } if (iswcntrl (c)) { if (error) *error = strdup("Control characters are not allowed."); return false; } } return true; } account-utils-1.3.0/libcommon/chfn_checks.h000066400000000000000000000003461521474342200207020ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once extern bool may_change_field(uid_t uid, char field, char **error); extern bool chfn_check_string(const char *string, const char *illegal, char **error); account-utils-1.3.0/libcommon/context.h000066400000000000000000000002631521474342200201260ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include "read_config.h" struct context_t { struct config_t cfg; sd_event *loop; }; account-utils-1.3.0/libcommon/create_hash.c000066400000000000000000000024171521474342200207060ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include "config.h" #include #include #include #include #include "basics.h" #include "files.h" int create_hash(const char *password, const char *prefix, unsigned long count, char **hash, char **error) { /* Strings returned by crypt_gensalt_rn will be no longer than this. */ char salt[CRYPT_GENSALT_OUTPUT_SIZE]; _cleanup_free_ struct crypt_data *cdata = NULL; char *sp; assert(password); assert(hash); sp = crypt_gensalt_rn(prefix, count, NULL, 0, salt, sizeof(salt)); if (sp == NULL) return -errno; cdata = calloc(1, sizeof(*cdata)); if (cdata == NULL) return -ENOMEM; sp = crypt_r(password, salt, cdata); if (sp == NULL) return -errno; if (!strneq(sp, prefix, strlen(prefix))) { /* crypt doesn't know the algorithm, error out */ int r = -ENOSYS; if (error) { if (asprintf (error, "Algorithm with prefix '%s' is not supported by the crypto backend.", prefix) < 0) { *error = NULL; r = -ENOMEM; } } explicit_bzero(cdata, sizeof(struct crypt_data)); return r; } *hash = strdup(sp); explicit_bzero(cdata, sizeof(struct crypt_data)); if (*hash == NULL) return -ENOMEM; return 0; } account-utils-1.3.0/libcommon/files.c000066400000000000000000000162461521474342200175470ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include "config.h" #include #include #include #include #include #include #include #include #include #include "basics.h" #include "files.h" #ifdef WITH_SELINUX #include #define SELINUX_ENABLED (is_selinux_enabled()>0) #else #define SELINUX_ENABLED 0 #endif #define MAX_LOCK_RETRIES 300 /* How often should we try to lock password file */ typedef int (*update_account_file_cb)(FILE*, FILE*, void*); static int lock_db(void) { int retries = 0; int r; while((r = lckpwdf()) != 0 && retries < MAX_LOCK_RETRIES) { usleep(10000); /* 1/100 second */ ++retries; } if (r < 0) { if (retries == MAX_LOCK_RETRIES) return -ENOLCK; else return -errno; } return 0; } static void unlink_and_free_tempfilep(char **p) { if (p == NULL || *p == NULL) return; /* If the file is created with mkstemp(), it will (almost always) change the suffix. Treat this as a sign that the file was successfully created. We ignore both the rare case where the original suffix is used and unlink failures. */ if (!endswith(*p, ".XXXXXX")) (void) unlink(*p); *p = mfree(*p); } static inline void umaskp(mode_t *u) { umask(*u); } /* This is much like mkostemp() but is subject to umask(). */ static int mkostemp_safe(char *pattern) { _cleanup_(umaskp) mode_t _saved_umask_ = umask(0077); int r; r = mkostemp(pattern, O_CLOEXEC); if (r < 0) return -errno; else return r; } /* return value: < 0 : error 0 : found, nothing changed 1 : entry changed */ static int update_passwd_entry(FILE *oldf, FILE *newf, void *ctx) { struct passwd *pw; struct passwd *newpw = ctx; int gotit = 0; int r; /* Loop over all passwd entries */ while ((pw = fgetpwent(oldf)) != NULL) { if(!gotit && streq(newpw->pw_name, pw->pw_name)) { /* XXX we don't support changing uid/gid yet */ int changed = 0; if (newpw->pw_passwd != NULL && !streq(pw->pw_passwd, newpw->pw_passwd)) { pw->pw_passwd = newpw->pw_passwd; changed = 1; } if (newpw->pw_shell != NULL && !streq(pw->pw_shell, newpw->pw_shell)) { pw->pw_shell = newpw->pw_shell; changed = 1; } if (newpw->pw_gecos != NULL && !streq(pw->pw_gecos, newpw->pw_gecos)) { pw->pw_gecos = newpw->pw_gecos; changed = 1; } if (newpw->pw_dir != NULL && !streq(pw->pw_dir, newpw->pw_dir)) { pw->pw_dir = newpw->pw_dir; changed = 1; } if (!changed) /* nothing to change, change nothing */ return 0; gotit = 1; } /* write the passwd entry to tmp file */ r = putpwent(pw, newf); if (r < 0) return -errno; } if (gotit == 0) return -ENODATA; return gotit; } /* return value: < 0 : error 0 : found, nothing changed 1 : entry changed */ static int update_shadow_entry(FILE *oldf, FILE *newf, void *ctx) { struct spwd *sp; struct spwd *newsp = ctx; int gotit = 0; int r; /* Loop over all shadow entries */ while ((sp = fgetspent(oldf)) != NULL) { if(!gotit && streq(newsp->sp_namp, sp->sp_namp)) { /* write the new shadow entry to tmp file */ r = putspent(newsp, newf); if (r < 0) return -errno; gotit = 1; } else { /* write the shadow entry to tmp file */ r = putspent(sp, newf); if (r < 0) return -errno; } } if (gotit == 0) return -ENODATA; return gotit; } static int update_account_locked(const char *etcdir, const char *name, update_account_file_cb update_account_entry, void *ctx) { _cleanup_(unlink_and_free_tempfilep) char *tmpfn = NULL; _cleanup_free_ char *origfn = NULL; _cleanup_free_ char *oldfn = NULL; _cleanup_close_ int newfd = -EBADF; _cleanup_fclose_ FILE *oldf = NULL; _cleanup_fclose_ FILE *newf = NULL; struct stat st; int r; assert(etcdir); assert(name); assert(update_account_entry); assert(ctx); if (asprintf(&origfn, "%s/%s", etcdir, name) < 0) return -ENOMEM; if (asprintf(&oldfn, "%s/%s-", etcdir, name) < 0) return -ENOMEM; if (asprintf(&tmpfn, "%s/.%s.XXXXXX", etcdir, name) < 0) return -ENOMEM; if ((oldf = fopen(origfn, "r")) == NULL) return -errno; if (fstat(fileno(oldf), &st) < 0) return -errno; newfd = mkostemp_safe(tmpfn); if (newfd < 0) return newfd; /* newfd == -errno */ r = fchmod(newfd, st.st_mode); if (r < 0) return -errno; r = fchown(newfd, st.st_uid, st.st_gid); if (r < 0) return -errno; #if 0 /* XXX */ r = copy_xattr(passwd_orig, passwd_tmp); if (r > 0) return -r; #endif newf = fdopen(newfd, "w+"); if (newf == NULL) return -errno; /* make sure file descriptior does not get closed twice */ TAKE_FD(newfd); int gotit = update_account_entry(oldf, newf, ctx); if (gotit < 0) return gotit; r = fclose(oldf); oldf = NULL; if (r < 0) return -errno; r = fflush(newf); if (r < 0) return -errno; r = fsync(fileno(newf)); if (r < 0) return -errno; r = fclose(newf); newf = NULL; if (r < 0) return -errno; if (gotit == 0) { /* no changes */ unlink(tmpfn); return 0; } unlink(oldfn); r = link(origfn, oldfn); if (r < 0) return -errno; r = rename(tmpfn, origfn); if (r < 0) return -errno; return 0; } static int update_account(const char *etcdir, const char *name, update_account_file_cb update_account_entry, void *ctx) { _cleanup_free_ char *origfn = NULL; #ifdef WITH_SELINUX char *prev_context_raw = NULL; #endif int r; if (isempty(etcdir)) etcdir = "/etc"; /* XXX adjust lock if etcdir is not /etc */ if (streq(etcdir, "/etc")) { r = lock_db(); if (r < 0) return r; } /* XXX use old password to verify again, else some other process could * have already changed the password meanwhile */ if (asprintf(&origfn, "%s/%s", etcdir, name) < 0) return -ENOMEM; #ifdef WITH_SELINUX if (SELINUX_ENABLED) { char *context_raw = NULL; if (getfilecon_raw(origfn, &context_raw) < 0) return -errno; if (getfscreatecon_raw(&prev_context_raw) < 0) { int saved_errno = errno; freecon(context_raw); return -saved_errno; } if (setfscreatecon_raw(context_raw) < 0) { int saved_errno = errno; freecon(context_raw); freecon(prev_context_raw); return -saved_errno; } freecon(context_raw); } #endif r = update_account_locked(etcdir, name, update_account_entry, ctx); #ifdef WITH_SELINUX if (SELINUX_ENABLED) { if (setfscreatecon_raw(prev_context_raw) < 0) r = -errno; freecon(prev_context_raw); } #endif /* XXX adjust lock if etcdir is not /etc */ if (streq(etcdir, "/etc")) { if (ulckpwdf() != 0) return -errno; } return r; } int update_passwd(struct passwd *newpw, const char *etcdir) { if (!newpw) return -EINVAL; return update_account(etcdir, "passwd", update_passwd_entry, newpw); } int update_shadow(struct spwd *newsp, const char *etcdir) { if (!newsp) return -EINVAL; return update_account(etcdir, "shadow", update_shadow_entry, newsp); } account-utils-1.3.0/libcommon/files.h000066400000000000000000000005731521474342200175500ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #pragma once #include #include #include extern int update_passwd(struct passwd *newpw, const char *etcdir); extern int update_shadow(struct spwd *newsp, const char *etcdir); extern int create_hash(const char *password, const char *prefix, unsigned long count, char **hash, char **error); account-utils-1.3.0/libcommon/mkdir_p.c000066400000000000000000000017221521474342200200630ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include #include #include #include #include #include "mkdir_p.h" int mkdir_p(const char *path, mode_t mode) { if (path == NULL) return -EINVAL; if (mkdir(path, mode) == 0) return 0; if (errno == EEXIST) { struct stat st; /* Check if the existing path is a directory */ if (stat(path, &st) != 0) return -errno; /* If not, fail with ENOTDIR */ if (!S_ISDIR(st.st_mode)) return -ENOTDIR; /* if it is a directory, return */ return 0; } /* If it fails for any reason but ENOENT, fail */ if (errno != ENOENT) return -errno; char *buf = strdup(path); if (buf == NULL) return -ENOMEM; int r = mkdir_p(dirname(buf), mode); free(buf); /* if we couldn't create the parent, fail, too */ if (r < 0) return r; if (mkdir(path, mode) == -1) return -errno; return 0; } account-utils-1.3.0/libcommon/mkdir_p.h000066400000000000000000000026771521474342200201020ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-2-Clause Copyright (c) 2024, Thorsten Kukuk Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #pragma once #include extern int mkdir_p(const char *path, mode_t mode); account-utils-1.3.0/libcommon/no_new_privs.c000066400000000000000000000003721521474342200211460ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include #include "no_new_privs.h" bool no_new_privs_enabled(void) { /* The no_new_privs flag disables setuid at execve(2) time. */ return (prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) == 1); } account-utils-1.3.0/libcommon/no_new_privs.h000066400000000000000000000001501521474342200211450ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include extern bool no_new_privs_enabled(void); account-utils-1.3.0/libcommon/read_config.c000066400000000000000000000105651521474342200207030ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include "basics.h" #include "read_config.h" struct config_t * struct_config_free(struct config_t *cfg) { cfg->allow_get_user_record = mfree(cfg->allow_get_user_record); cfg->allow_verify_password = mfree(cfg->allow_verify_password); cfg->allow_expired_check = mfree(cfg->allow_expired_check); return NULL; } /* trim leading and trailing whitespaces */ static char * trim_whitespace(char *str) { char *end; while(isspace((unsigned char)*str)) str++; if(*str == '\0') return str; end = str + strlen(str) - 1; while(end > str && isspace((unsigned char)*end)) end--; *(end+1) = 0; return str; } /* function to resolve a single user string (name or UID) to a uid_t Returns 0 on success, -errno on failure */ static int parse_token(const char *token, uid_t *result_uid) { if (isempty(token)) return -EINVAL; /* if the first character is a number, it must be a UID */ if (isdigit(token[0])) { char *ep; long long ll; errno = 0; ll = strtol(token, &ep, 10); if (errno == ERANGE || ll < 0 || ll > UINT32_MAX || token == ep || *ep != '\0') { if (errno == 0) return -EINVAL; else return -errno; } *result_uid = (uid_t)ll; return 0; } else /* Try as username */ { struct passwd *pwd; errno = 0; pwd = getpwnam(token); if (pwd == NULL) { if (errno == 0) return -ENODATA; else return -errno; } *result_uid = pwd->pw_uid; } return 0; } static econf_err lookup_group(econf_file *key_file, const char *group, uid_t **list) { _cleanup_free_ char *value = NULL; _cleanup_free_ uid_t *uids = NULL; econf_err error; *list = NULL; /* look at first in method specific group */ error = econf_getStringValue(key_file, group, "allow", &value); if (error == ECONF_NOKEY || error == ECONF_NOGROUP) /* Fallback if key not found */ error = econf_getStringValue(key_file, "global", "allow", &value); if (error == ECONF_NOKEY || error == ECONF_NOGROUP) /* no data, done */ return ECONF_SUCCESS; /* error out in other cases */ if (error != ECONF_SUCCESS) return error; if (isempty(value)) return ECONF_SUCCESS; /* split value into tokens and parse them */ /* count number of tokens */ size_t count = 0; for (size_t i = 0; value[i] != '\0'; i++) if (value[i] == ',') count++; count++; /* allocate one more slot for the final "NULL" */ uids = calloc(count + 1, sizeof (uid_t)); if (uids == NULL) return ECONF_NOMEM; /* Split and store */ count = 0; char *token = strtok(value, ","); while (token != NULL) { uid_t uid = 0; int r; token = trim_whitespace(token); r = parse_token(token, &uid); token = strtok(NULL, ","); if (r < 0) { /* XXX we need a good warning */ continue; /* continue with the other user */ } if (uid == 0) /* root is allways allowed, ignore */ continue; uids[count++] = uid; } uids[count] = 0; *list = TAKE_PTR(uids); return ECONF_SUCCESS; } /* we will read everything and only report the firstx error */ econf_err read_config(struct config_t *cfg) { _cleanup_(econf_freeFilep) econf_file *key_file = NULL; econf_err error; econf_err retval = ECONF_SUCCESS; #ifdef TESTSDIR error = econf_newKeyFile_with_options(&key_file, "ROOT_PREFIX="TESTSDIR); if (error) return error; #endif /* This looks for pwaccessd.conf{.d} in /usr/share/account-utils/ or /etc/account-utils/ */ error = econf_readConfig(&key_file, "account-utils", /* project name */ "/usr/share", /* directory below /usr */ "pwaccessd", /* file name without extension */ "conf", /* suffix */ "=", /* delimiter */ "#"); /* comment */ if (error != ECONF_SUCCESS) return error; /* XXX read debug_level from [global] and set max_log_level */ error = lookup_group(key_file, "GetUserRecord", &(cfg->allow_get_user_record)); if (error && !retval) retval = error; error = lookup_group(key_file, "VerifyPassword", &(cfg->allow_verify_password)); if (error && !retval) retval = error; error = lookup_group(key_file, "ExpiredCheck", &(cfg->allow_expired_check)); if (error && !retval) return error; return retval; } account-utils-1.3.0/libcommon/read_config.h000066400000000000000000000004751521474342200207070ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include struct config_t { uid_t *allow_get_user_record; uid_t *allow_verify_password; uid_t *allow_expired_check; }; extern struct config_t *struct_config_free(struct config_t *cfg); extern econf_err read_config(struct config_t *cfg); account-utils-1.3.0/libcommon/string-util-fundamental.c000066400000000000000000000013641521474342200232150ustar00rootroot00000000000000/* SPDX-License-Identifier: LGPL-2.1-or-later */ // based on systemd v258 #include #include "basics.h" char *startswith(const char *s, const char *prefix) { size_t l; assert(s); assert(prefix); l = strlen(prefix); if (!strneq(s, prefix, l)) return NULL; return (char*) s + l; } char* endswith(const char *s, const char *suffix) { size_t sl, pl; assert(s); assert(suffix); sl = strlen(s); pl = strlen(suffix); if (pl == 0) return (char*) s + sl; if (sl < pl) return NULL; if (!streq(s + sl - pl, suffix)) return NULL; return (char*) s + sl - pl; } account-utils-1.3.0/libcommon/verify.c000066400000000000000000000107171521474342200177460ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include "config.h" #include #include #include #include #include "pwaccess.h" #include "basics.h" #include "verify.h" bool valid_name(const char *name) { /* This function tests if the name has invalid characters, not if the name is really valid. User/group names must match BRE regex: [a-zA-Z0-9_.][a-zA-Z0-9_.-]*$\? Reject every name containing additional characters. */ if (isempty(name)) return false; while (*name != '\0') { if (!((*name >= 'a' && *name <= 'z') || (*name >= 'A' && *name <= 'Z') || (*name >= '0' && *name <= '9') || *name == '_' || *name == '.' || *name == '-' || *name == '$') ) return false; ++name; } return true; } bool is_shadow(const struct passwd *pw) { assert(pw); if (isempty(pw->pw_passwd)) return false; if (streq(pw->pw_passwd, "x") || (pw->pw_passwd && strlen(pw->pw_passwd) > 2 && (pw->pw_passwd[0] == '#') && (pw->pw_passwd[1] == '#') && streq(pw->pw_name, pw->pw_passwd + 2))) return true; return false; } int expired_check(const struct spwd *sp, long *daysleft, bool *pwchangeable) { long int now, passed; assert(sp); if (daysleft) *daysleft = -1; if (pwchangeable) *pwchangeable = true; now = time(NULL) / (60 * 60 * 24); /* account expired */ if (sp->sp_expire > 0 && now >= sp->sp_expire) return PWA_EXPIRED_ACCT; /* new password required */ if (sp->sp_lstchg == 0) { if (daysleft) *daysleft = 0; return PWA_EXPIRED_CHANGE_PW; } /* password aging disabled */ /* The last and max fields must be present for an account to have an expired password. A maximum of >10000 days is considered to be infinite. */ if (sp->sp_lstchg == -1 || sp->sp_max == -1 || sp->sp_max >= 10000) return PWA_EXPIRED_NO; passed = now - sp->sp_lstchg; if (sp->sp_max >= 0) { if (sp->sp_inact >= 0) { long inact = sp->sp_max < LONG_MAX - sp->sp_inact ? sp->sp_max + sp->sp_inact : LONG_MAX; if (passed >= inact) { /* authtok expired */ if (daysleft) *daysleft = inact - passed; return PWA_EXPIRED_PW; } } /* needs a new password */ if (passed >= sp->sp_max) return PWA_EXPIRED_CHANGE_PW; if (sp->sp_warn > 0) { long warn = sp->sp_warn > sp->sp_max ? -1 : sp->sp_max - sp->sp_warn; if (passed >= warn && daysleft) /* warn before expire */ *daysleft = sp->sp_max - passed; } } if (sp->sp_min > 0 && passed < sp->sp_min && pwchangeable) /* The last password change was too recent. */ *pwchangeable = false; return PWA_EXPIRED_NO; } static inline int consttime_streq(const char *userinput, const char *secret) { volatile const char *u = userinput, *s = secret; volatile int ret = 0; do { ret |= *u ^ *s; s += !!*s; } while (*u++ != '\0'); return ret == 0; } int verify_password(const char *hash, const char *password, bool nullok) { _cleanup_free_ char *pp = NULL; if (isempty(password) && !nullok) return VERIFY_FAILED; else if (isempty(hash)) { if (isempty(password) && nullok) return VERIFY_OK; else return VERIFY_FAILED; } else if (!password || *hash == '*' || *hash == '!') return VERIFY_FAILED; else { /* Get the status of the hash from checksalt */ int retval_checksalt = crypt_checksalt(hash); /* * Check for hashing methods that are disabled by * libcrypt configuration and/or system preset. */ if (retval_checksalt == CRYPT_SALT_METHOD_DISABLED) return VERIFY_CRYPT_DISABLED; if (retval_checksalt == CRYPT_SALT_INVALID) return VERIFY_CRYPT_INVALID; struct crypt_data *cdata; cdata = calloc(1, sizeof(*cdata)); if (cdata != NULL) { char *cp = crypt_r(password, hash, cdata); if (cp) pp = strdup(cp); explicit_bzero(cdata, sizeof(struct crypt_data)); free(cdata); } } if (pp && consttime_streq(pp, hash)) return VERIFY_OK; return VERIFY_FAILED; } bool is_blank_password(const struct passwd *pw, const struct spwd *sp) { bool is_blank = false; assert(pw); if (is_shadow(pw)) { if (!sp) is_blank = false; else is_blank = (strlen(strempty(sp->sp_pwdp)) == 0); } else is_blank = (strlen(strempty(pw->pw_passwd)) == 0); return is_blank; } account-utils-1.3.0/libcommon/verify.h000066400000000000000000000012631521474342200177470ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #pragma once #define VERIFY_OK 0 /* password matches */ #define VERIFY_FAILED 1 /* password does not match */ #define VERIFY_CRYPT_DISABLED 2 /* salt got disabled in libcrypt */ #define VERIFY_CRYPT_INVALID 3 /* salt is not supported by libcrypt */ #include #include extern bool valid_name(const char *name); extern bool is_shadow(const struct passwd *pw); extern bool is_blank_password(const struct passwd *pw, const struct spwd *sp); extern int expired_check(const struct spwd *sp, long *daysleft, bool *pwchangeable); extern int verify_password(const char *hash, const char *password, bool nullok); account-utils-1.3.0/man/000077500000000000000000000000001521474342200150645ustar00rootroot00000000000000account-utils-1.3.0/man/chage.1.xml000066400000000000000000000160511521474342200170170ustar00rootroot00000000000000 chage 1 account-utils %version% chage chage change user password expiry information chage option user DESCRIPTION chage is used to list and change the password expiry information of a user. It allows the system administrator to change the number of days between allowed and required password changes and the date of the last password change. It allows also to define when an account will expire. The chage command is restricted to the system administrator, except for the option, which may be used by an user to determine when his password or account is due to expire. If no option is given, chage operates in an interactive mode, prompting the user with the current values for all of the fields. Enter the new value to change the field, or leave the line blank to use the current value. If the users exists in the local passwd5 file, but not in the local shadow5 file, chage will create a new entry in the shadow file. This implementation does not require the setuid bit set, instead it will communicate via the varlink protocol with pwaccessd8 and pwupd8 to read and modify the account data. OPTIONS This option will list the password expiry information in a human readable format. The user will see the date when he changed the password the last time, when the password will be expire, when the password will be locked and when the account will expire. #days With this option the minimum number of days between password changes is changed. A value of zero for this field indicates that the user may change her password at any time. Else the user will not be permitted to change the password until minimum number of days have elapsed. #days With this option the maximum number of days during which a password is valid is changed. When maxdays plus lastday is less than the current day, the user will be required to change his password before being able to use the account. date With this option the date when the password was last changed can be set to another value. lastday has to be specified as number of days since January 1st, 1970. The date may also be expressed in the format YYYY-MM-DD. If supported by the system, a value of zero forces the user to change the password at next login. expiredate With this option the date when the account will be expired can be changed. The expire date has to be specified as number of days since January 1st, 1970. The date may also be expressed in the format YYYY-MM-DD. #days This option is used to set the number of days of inactivity after a password has expired before the account is locked. A user whose account is locked must contact the system administrator before being able to use the account again. A value of -1 disables this feature. #days With this option the number of days of warning before a password change is required can be changed. This option is the number of days prior to the password expiring that a user will be warned the password is about to expire. FILES /etc/passwd user account information /etc/shadow shadow user account information SEE ALSO pwaccessd8 , pwupd8 , passwd1 , passwd5 , shadow5 account-utils-1.3.0/man/chfn.1.xml000066400000000000000000000143561521474342200166740ustar00rootroot00000000000000 chfn 1 account-utils %version% chfn chfn change finger information chfn option user DESCRIPTION chfn is used to change the user finger information. This are the users fullname, office room number, office phone number and home phone number. This information is stored in the /etc/passwd file and typically printed by tools like finger1 or pinky1 and similiar programs. A normal user may only change the fields for their own account, the super user may change the fields for any account. Also, only the super user may use the option to change the other portions of the GECOS field. If no information is given on the command line, chfn operates in an interactive fashion, prompting the user for each field. Enter the new value to change the field, or leave the line blank to use the current value. Enter none or a blank only to remove the old value. The current value is displayed between a pair of [...] marks. The only restrictions placed on the contents of the fields is that no control characters may be present, nor any of comma, colon, or equal sign. The other field does not have this restriction. This implementation does not require the setuid bit set, instead it will communicate via varlink protocol with pwaccessd8 and pwupd8 to read and modify the account data. OPTIONS Specify the user's real name. Specify the user's office room number. Specify the user's office phone number. Specify the user's home phone number. Specify the other portion of the GECOS field. Print a more verbose help text and exit. Print version information and exit. CONFIGURATION The following configuration variables provided by login.defs5 define the behavior of the chfn command: This parameter specifies which values in the GECOS field may be changed by regular users using the chfn program. It can be any combination of letters , , , and for Full name, Room number, Work phone, Home phone and Other. Only the superuser can make changes if not specified. FILES /etc/passwd user account information SEE ALSO pwaccessd8 , pwupd8 , finger1 , pinky1 , passwd5 , login.defs5 account-utils-1.3.0/man/chsh.1.xml000066400000000000000000000077711521474342200167060ustar00rootroot00000000000000 chsh 1 account-utils %version% chsh chsh change login shell chsh option user DESCRIPTION chsh is used to change the user login shell. A normal user may only change the login shell for their own account, the super user may change the login shell for any account. If a shell is not given on the command line, chsh operates in an interactive fashion, prompting the user with the current login shell. Enter the new value to change the field, or leave the line blank to use the current value. Enter a space or none to remove the old content. The current value is displayed between a pair of [...] marks. The only restrictions placed on the login shell is that the command name must be listed in /etc/shells, unless the invoker is the super-user, and then any value may be added. An account with a restricted login shell may not change their login shell. This implementation does not require the setuid bit set, instead it will communicate via varlink protocol with pwaccessd8 and pwupd8 to read and modify the account data. OPTIONS Specify the login shell. Print the list of allowed shells5 and exit. Print a more verbose help text and exit. Print version information and exit. FILES /etc/passwd user account information SEE ALSO pwaccessd8 , pwupd8 , login1 , passwd5 , shells5 account-utils-1.3.0/man/custom-man.xsl000066400000000000000000000007001521474342200176740ustar00rootroot00000000000000 account-utils-1.3.0/man/expiry.1.xml000066400000000000000000000060741521474342200172740ustar00rootroot00000000000000 expiry 1 account-utils %version% expiry expiry check password expiration and enforce password change expiry -c|-f user DESCRIPTION expiry checks the current password expiration and prints a warning if the password is expiring or enforces a change when required. This implementation does not require the setuid bit set, instead it will communicate via varlink protocol with pwaccessd8 and pwupd8 to read and modify the account data. OPTIONS Print number of days when password expires for the caller or the specified . The caller or is forced to change the password if it is expired but not inactive. FILES /etc/passwd user account information /etc/shadow shadow user account information SEE ALSO pwaccessd8 , pwupd8 , passwd1 , passwd5 , shadow5 account-utils-1.3.0/man/meson.build000066400000000000000000000052771521474342200172410ustar00rootroot00000000000000xsltproc_exe = find_program('xsltproc', required : get_option('man')) want_man = (get_option('man').enabled() or get_option('man').auto()) and xsltproc_exe.found() xsltproc_flags = [ '--nonet', '--xinclude', '--stringparam', 'version', '@0@'.format(meson.project_version()), '--path', '@0@:@1@'.format(meson.current_build_dir(), meson.current_source_dir())] custom_man_xsl = files('custom-man.xsl') xslt_cmd = [xsltproc_exe, '-o', '@OUTPUT0@'] + xsltproc_flags mandir1 = get_option('mandir') / 'man1' mandir8 = get_option('mandir') / 'man8' if xsltproc_exe.found() custom_target('chage.1', input : 'chage.1.xml', output : 'chage.1', command : xslt_cmd + [custom_man_xsl, '@INPUT@'], install : want_man, install_dir : mandir1) custom_target('chfn.1', input : 'chfn.1.xml', output : 'chfn.1', command : xslt_cmd + [custom_man_xsl, '@INPUT@'], install : want_man, install_dir : mandir1) custom_target('chsh.1', input : 'chsh.1.xml', output : 'chsh.1', command : xslt_cmd + [custom_man_xsl, '@INPUT@'], install : want_man, install_dir : mandir1) custom_target('expiry.1', input : 'expiry.1.xml', output : 'expiry.1', command : xslt_cmd + [custom_man_xsl, '@INPUT@'], install : want_man, install_dir : mandir1) custom_target('passwd.1', input : 'passwd.1.xml', output : 'passwd.1', command : xslt_cmd + [custom_man_xsl, '@INPUT@'], install : want_man, install_dir : mandir1) custom_target('pam_unix_ng.8', input : 'pam_unix_ng.8.xml', output : 'pam_unix_ng.8', command : xslt_cmd + [custom_man_xsl, '@INPUT@'], install : want_man, install_dir : mandir8) custom_target('pam_debuginfo.8', input : 'pam_debuginfo.8.xml', output : 'pam_debuginfo.8', command : xslt_cmd + [custom_man_xsl, '@INPUT@'], install : want_man, install_dir : mandir8) custom_target('pwaccessd.8', input : 'pwaccessd.8.xml', output : 'pwaccessd.8', command : xslt_cmd + [custom_man_xsl, '@INPUT@'], install : want_man, install_dir : mandir8) custom_target('pwupdd.8', input : 'pwupdd.8.xml', output : 'pwupdd.8', command : xslt_cmd + [custom_man_xsl, '@INPUT@'], install : want_man, install_dir : mandir8) endif account-utils-1.3.0/man/pam_debuginfo.8.xml000066400000000000000000000067511521474342200205640ustar00rootroot00000000000000 pam_debuginfo 8 account-utils %version% pam_debuginfo pam_debuginfo PAM module logging information for debugging pam_debuginfo.so ... DESCRIPTION This is a PAM module which logs a lot of useful information for debugging. It logs the following information: Service name PAM type PAM flags UID EUID PAM items SELinux status NoNewPrivs status OPTIONS level Define with which syslog3 level the information should be printed. Valid values are debug, info, notice, warning, error, critical, alert and emerg. MODULE TYPES PROVIDED All module types (, , , ) are provided. RETURN VALUES PAM_IGNORE Returned by all service types. EXAMPLES Add the following line to e.g. /etc/pam.d/login to log all information via syslog8 : auth optional pam_debuginfo.so account optional pam_debuginfo.so password optional pam_debuginfo.so session optional pam_debuginfo.so SEE ALSO pam.conf5 , pam.d5 , pam8 AUTHOR pam_debuginfo was written by Thorsten Kukuk <kukuk@suse.com>. account-utils-1.3.0/man/pam_unix_ng.8.xml000066400000000000000000000167101521474342200202650ustar00rootroot00000000000000 pam_unix_ng 8 account-utils %version% pam_unix_ng pam_unix_ng PAM module for traditional password authentication pam_unix_ng.so ... DESCRIPTION This is a standard UNIX authentication PAM module which delegates tasks requiring access to /etc/shadow to pwaccessd8, which allows one to use this module in environments without setuid binaries. If pwaccessd is not running, it tries at first to start it via D-Bus. If this fails, it tries to read the local files as fallback itself. OPTIONS debug Print debug information via syslog3 . quiet Avoid all messages except errors. nullok The default action of this module is to not permit the user access to a service if their official password is blank. The argument overrides this default. If the application sets the PAM_DISALLOW_NULL_AUTHTOK flag, is ignored in the auth module type. try_first_pass The module first attempts to use the password from the previously stacked modules to see if it is also suitable for this module before prompting the user to enter their password again. use_first_pass The module only attempts to use the password from the previously stacked modules and never prompts the user for input. If no password is available or the password does not match, the user is denied access. use_authtok When a password is changed, the module will set the new password to the one provided by a previously stacked module. authtok_type=type The default action is for the module to use the following prompts when requesting passwords: "New UNIX password: " and "Retype UNIX password: ". The example word UNIX can be replaced with this option, by default it is empty. minlen=<number> Minimal length of new password. The default is 8 characters. crypt_prefix=<prefix> Prefix of the hash algorithm to use. See crypt5 for valid values. crypt_count=<number> This option controls the processing cost of the hash. See crypt5 for valid values. fail_delay=<milliseconds> The module requests by default a delay of 2000 milliseconds should the authentication as a whole fail. This argument can be used to adjust the delay or disable it (fail_delay=0). MODULE TYPES PROVIDED All module types (, , , ) are provided. RETURN VALUES PAM_SUCCESS Everything was successful. PAM_SERVICE_ERR Internal service module error. PAM_USER_UNKNOWN User not known. PAM_IGNORE Returned by service types which do nothing. EXAMPLES Add the following line to e.g. /etc/pam.d/login to log when a user logs in and out to syslog8 : session required pam_unix_ng.so SEE ALSO pwaccessd8 , pam.conf5 , pam.d5 , pam8 AUTHOR pam_unix_ng was written by Thorsten Kukuk <kukuk@suse.com>. account-utils-1.3.0/man/passwd.1.xml000066400000000000000000000201011521474342200172400ustar00rootroot00000000000000 passwd 1 account-utils %version% passwd passwd change user password passwd option user DESCRIPTION The command changes passwords for user accounts. While an administrator may change the password for any account, a normal user is only allowed to change the password for their own account. can also change account information, such as the full name of the user, their login shell and password expiry dates or disable an account. This implementation does not require the setuid bit set, instead it will communicate via the varlink protocol with pwaccessd8 and pwupd8 to read and modify the account data. OPTIONS The password of the given account can be deleted by the system administrator. If the PAM stack is configured accordingly, the user can log in without entering a password. Immediately expire the password. The user will be forced to change the password at next login. -h, --help Print a verbose help text and exit. days This option is used to set the number of days of inactivity after a password has expired before the account is locked. A user whose account is locked must contact the system administrator before being able to use the account again. A value of -1 disables this feature. Keep non-expired authentication tokens. The password will only be changed if it is expired. This functionality depends on the used PAM modules to change the password. A system administrator can lock the account of the specified user by adding a ! in front of the password, so that it cannot match anything. #days With this option the minimum number of days between password changes is changed. A value of zero for this field indicates that the user may change her password at any time. Else the user will not be permitted to change the password until minimum number of days have elapsed. #days With this option the maximum number of days during which a password is valid is changed. When maxdays plus lastday is less than the current day, the user will be required to change his password before being able to use the account. Suppress informal messages. This mainly depends on the used PAM modules. Read the password from stdin, which could also be a pipe. Other input requested from a PAM module will lead to an error. Report password status on the named account. The first part indicates if the user account is locked (LK), has no password (NP), or has an existing or locked password (PS). The second part gives the date of the last password change. The next parts are the minimum age, maximum age, warning period, and inactivity period for the password. A system administrator can unlock the specified account by removing the ! in front of the password again. This can lead to a password less account, if it was password less before, too. Print version information and exit. #days With this option the number of days of warning before a password change is required can be changed. This option is the number of days prior to the password expiring that a user will be warned the password is about to expire. FILES /etc/passwd user account information /etc/shadow shadow user account information SEE ALSO pwaccessd8 , pwupd8 , passwd5 , shadow5 account-utils-1.3.0/man/pwaccessd.8.xml000066400000000000000000000156141521474342200177370ustar00rootroot00000000000000 pwaccessd 8 account-utils %version% pwaccessd pwaccessd pwaccessd.service pwaccessd.socket manage passwd and shadow information pwaccessd.service pwaccessd.socket /usr/libexec/pwaccessd OPTIONS Description pwaccessd is a systemd1 socket-activated service which provides account information in struct passwd and struct shadow format. It is capable of checking if a password or account has expired and verifies passwords. By default, normal users only have access to their own passwd and shadow entries. The root user has access to all accounts. Specific users can be granted extended access via configuration. Options Activation through socket. This is the standard mode when running under systemd. Enable debug mode. Enable verbose logging. Give the help list. Print program version. Varlink Interfaces The pwaccessd daemon exposes the following functionality via Varlink interfaces: GetAccountName Provides the user name corresponding to a given UID. GetUserRecord Provides the passwd and shadow entry for a given UID or account name. GetGroupRecord Provides the group entry for a given GID or group name. VerifyPassword Validates a password for a specific user. ExpiredCheck Checks if a user account or password is expired. Configuration pwaccessd reads its configuration from pwaccessd.conf. It follows the UAPI Configuration Files Specification, meaning it searches for configuration files in directories such as /usr/share/account-utils/, /run/account-utils/, and /etc/account-utils/. Files in /etc/account-utils/ take precedence. The configuration format is INI-style. The primary configuration key is allow. This key accepts a list of user accounts that are allowed to read all passwd and shadow entries, in addition to root. The allow key can be defined within specific sections (groups) corresponding to the Varlink interface methods: [GetUserRecord] [VerifyPassword] [ExpiredCheck] If the key is not found in the specific section, pwaccessd will fall back to looking in the [global] section. Example pwaccessd.conf [global] # Allow user 'admin' to perform all actions allow = admin [VerifyPassword] # Allow 'auth-service' to verify passwords, overriding global allow = auth-service Files /usr/libexec/pwaccessd The daemon binary. /etc/account-utils/pwaccessd.conf The main configuration file. See Also systemd1, expiry1, passwd1, passwd5, shadow5, pam_unix_ng8 account-utils-1.3.0/man/pwupdd.8.xml000066400000000000000000000112741521474342200172640ustar00rootroot00000000000000 pwupdd 8 account-utils %version% pwupdd pwupdd pwupdd.service pwupdd.socket update passwd and shadow entries pwupdd.service pwupdd.socket /usr/libexec/pwupdd OPTIONS Description pwupdd is an inetd8 style socket-activated service. A new instance of the daemon is started for every incoming request. It exposes a Varlink interface to allow authorized users to modify their own account data, including passwords, login shells, and GECOS field information. Authentication is handled via PAM8. Additionally, the root user can utilize specific methods to update any /etc/passwd or /etc/shadow entry. Options , Enable debug mode. , Enable verbose logging. , Give the help list. Print program version. Varlink Interfaces The service exposes the following methods via Varlink: Chauthtok Changes the password for a provided user. Authentication is performed via PAM using the configuration pwupd-passwd. This method may be called by the root user or the user owning the record. Chfn Changes the finger (GECOS) information of a user. Authentication is performed via PAM using the configuration pwupd-chfn. This method may be called by the root user or the user owning the record. Chsh Changes the login shell of an account. Authentication is performed via PAM using the configuration pwupd-chsh. This method may be called by the root user or the user owning the record. UpdatePasswdShadow Updates the passwd and shadow entry of a specified user. Only root is allowed to call this method. See Also passwd1, chfn1, chsh1, pam8 account-utils-1.3.0/meson.build000066400000000000000000000232671521474342200164650ustar00rootroot00000000000000project( 'account-utils', 'c', meson_version : '>= 0.61.0', default_options : [ 'prefix=/usr', 'sysconfdir=/etc', 'localstatedir=/var', 'buildtype=debugoptimized', 'default_library=shared', 'b_pie=true', 'b_lto=true', 'warning_level=2'], license : ['GPL-2.0-or-later', 'LGPL-2.1-or-later'], version : '1.3.0', ) distribution = get_option('distribution') if distribution == '' fs = import('fs') # Auto-detect distribution from /etc/os-release osrelease = '/etc/os-release' if fs.is_file(osrelease) osrelease_content = run_command('sh', '-c', 'grep "^ID=" ' + osrelease + ' | cut -d= -f2 | tr -d \'"\'', check: false) if osrelease_content.returncode() == 0 detected_id = osrelease_content.stdout().strip() if detected_id == 'opensuse' or detected_id == 'opensuse-leap' or detected_id == 'opensuse-tumbleweed' or detected_id == 'opensuse-microos' or detected_id == 'sles' distribution = 'suse' else distribution = 'example' endif else distribution = 'example' endif else distribution = 'example' endif endif message('Distribution: ' + distribution) conf = configuration_data() conf.set_quoted('VERSION', meson.project_version()) conf.set_quoted('PACKAGE', meson.project_name()) conf.set_quoted('_VARLINK_PWACCESS_SOCKET_DIR', '/run/account') conf.set_quoted('_VARLINK_PWACCESS_SOCKET', '/run/account/pwaccess-socket') conf.set_quoted('_VARLINK_PWUPD_SOCKET_DIR', '/run/account') conf.set_quoted('_VARLINK_PWUPD_SOCKET', '/run/account/pwupd-socket') conf.set_quoted('_VARLINK_NEWIDMAPD_SOCKET_DIR', '/run/account') conf.set_quoted('_VARLINK_NEWIDMAPD_SOCKET', '/run/account/newidmapd-socket') conf.set_quoted('_PATH_VENDORDIR', get_option('vendordir')) cc = meson.get_compiler('c') pkg = import('pkgconfig') inc = include_directories(['include','libcommon','libclient']) add_project_arguments(['-D_GNU_SOURCE=1', '-DXTSTRINGDEFINES', '-D_FORTIFY_SOURCE=2', '-D_FILE_OFFSET_BITS=64', '-D_TIME_BITS=64'], language : 'c') possible_cc_flags = [ '-fstack-protector-strong', '-funwind-tables', '-fasynchronous-unwind-tables', '-fstack-clash-protection', '-Werror=return-type', '-Wbad-function-cast', '-Wcast-align', '-Wformat-security', '-Winline', '-Wmissing-declarations', '-Wmissing-prototypes', '-Wnested-externs', '-Wshadow', '-Wstrict-prototypes', '-Wundef', ] add_project_arguments(cc.get_supported_arguments(possible_cc_flags), language : 'c') prefixdir = get_option('prefix') if not prefixdir.startswith('/') error('Prefix is not absolute: "@0@"'.format(prefixdir)) endif libexecdir = join_paths(prefixdir, get_option('libexecdir')) systemunitdir = prefixdir / 'lib/systemd/system' tmpfilesdir = prefixdir / 'lib/tmpfiles.d' pamlibdir = get_option('pamlibdir') if pamlibdir == '' pamlibdir = get_option('libdir') / 'security' endif pamconfdir = get_option('pamconfdir') if pamconfdir == '' pamconfdir = prefixdir / 'lib/pam.d' endif libpam = dependency('pam', required: true) libpam_misc = dependency('pam_misc', required: true) libsystemd = dependency('libsystemd', version: '>= 257', required: true) libcrypt = dependency('libxcrypt', version: '>= 4.4.27', required: true) libeconf = dependency('libeconf', version : '>=0.7.5', required : true) libcap = dependency('libcap', required: get_option('tools')) libselinux = dependency('libselinux', required: get_option('selinux')) if libselinux.found() conf.set('WITH_SELINUX', 1) endif libpwaccess_c = files('lib/varlink.c') libpwaccess_map = 'lib/libpwaccess.map' libpwaccess_map_version = '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), libpwaccess_map) libpwaccess = shared_library( 'pwaccess', libpwaccess_c, include_directories : inc, link_args : ['-shared', libpwaccess_map_version], link_depends : libpwaccess_map, dependencies : [libsystemd], install : true, version : meson.project_version(), soversion : '0' ) install_headers('include/pwaccess.h') pkg.generate( libpwaccess, name : 'libpwaccess', description : 'library to read passwd and shadow entries via varlink daemon', version : meson.project_version(), ) libcommon_c = files('libcommon/check_caller_perms.c', 'libcommon/chfn_checks.c', 'libcommon/create_hash.c', 'libcommon/files.c', 'libcommon/mkdir_p.c', 'libcommon/no_new_privs.c', 'libcommon/read_config.c', 'libcommon/string-util-fundamental.c', 'libcommon/verify.c') libcommon = static_library( 'common', libcommon_c, include_directories : inc, install : false ) libclient_c = files('libclient/chauthtok.c', 'libclient/drop_privs.c', 'libclient/get_logindefs.c', 'libclient/get_value.c', 'libclient/varlink-client-common.c') libclient = static_library( 'client', libclient_c, include_directories : inc, install : false ) pwaccessd_c = ['src/pwaccessd.c', 'src/varlink-org.openSUSE.pwaccess.c', 'src/varlink-service-common.c'] pwupdd_c = ['src/pwupdd.c', 'src/varlink-org.openSUSE.pwupd.c', 'src/varlink-service-common.c'] newidmapd_c = ['src/newidmapd.c', 'src/varlink-org.openSUSE.newidmapd.c', 'libcommon/mkdir_p.c', 'src/map_range.c', 'src/varlink-service-common.c'] executable('pwaccessd', pwaccessd_c, include_directories : inc, link_with : [libcommon], dependencies : [libsystemd, libcrypt, libeconf], install_dir : libexecdir, install : true) executable('pwupdd', pwupdd_c, include_directories : inc, link_with : [libcommon, libpwaccess], dependencies : [libpam, libsystemd, libeconf, libselinux], install_dir : libexecdir, install : true) executable('newidmapd', newidmapd_c, include_directories : inc, link_with : [libpwaccess], dependencies : [libsystemd, libeconf], install_dir : libexecdir, install : true) executable('newuidmap', 'src/newxidmap.c', 'src/map_range.c', c_args : '-DXID="uid"', include_directories : inc, link_with : [libclient, libcommon], dependencies : [libsystemd], install : true) executable('newgidmap', 'src/newxidmap.c', 'src/map_range.c', c_args : '-DXID="gid"', include_directories : inc, link_with : [libclient, libcommon], dependencies : [libsystemd], install : true) executable('chage', 'src/chage.c', include_directories : inc, link_with : [libclient, libpwaccess], dependencies : [libsystemd, libpam, libpam_misc, libeconf], install : true) executable('chfn', 'src/chfn.c', include_directories : inc, link_with : [libclient, libcommon, libpwaccess], dependencies : [libsystemd, libpam, libpam_misc, libeconf], install : true) executable('chsh', 'src/chsh.c', include_directories : inc, link_with : [libclient, libcommon, libpwaccess], dependencies : [libsystemd, libpam, libpam_misc, libeconf], install : true) executable('passwd', 'src/passwd.c', include_directories : inc, link_with : [libclient, libcommon, libpwaccess], dependencies : [libsystemd, libpam, libpam_misc, libeconf], install : true) executable('expiry', 'src/expiry.c', include_directories : inc, link_with : [libclient, libcommon, libpwaccess], dependencies : [libsystemd, libpam, libpam_misc], install : true) pam_unix_ng_c = files('src/pam_unix_ng-common.c', 'src/pam_unix_ng-session.c', 'src/pam_unix_ng-acct.c', 'src/pam_unix_ng-auth.c', 'src/pam_unix_ng-passwd.c', 'src/pam_unix_ng-spawn.c') pam_unix_ng_map = 'src/pam_unix_ng.map' pam_unix_ng_map_version = '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), pam_unix_ng_map) pam_unix_ng = shared_library( 'pam_unix_ng', pam_unix_ng_c, name_prefix : '', include_directories : inc, link_args : ['-shared', pam_unix_ng_map_version], link_depends : pam_unix_ng_map, link_with : [libclient, libcommon, libpwaccess], dependencies : [libsystemd, libcrypt, libeconf, libpam, libselinux], install : true, install_dir : pamlibdir ) pam_debuginfo_c = ['src/pam_debuginfo.c', 'libcommon/no_new_privs.c', 'libcommon/string-util-fundamental.c'] pam_debuginfo = shared_library( 'pam_debuginfo', pam_debuginfo_c, name_prefix : '', include_directories : inc, link_args : ['-shared', pam_unix_ng_map_version], link_depends : pam_unix_ng_map, dependencies : [libpam, libselinux], install : true, install_dir : pamlibdir ) # create config.h subdir('include') # systemd units subdir('units') if get_option('tools') subdir('tools') endif # example pam configuration files pamdpwupdchfnsrc = 'pam.d' / distribution / 'pwupd-chfn' pamdpwupdchshsrc = 'pam.d' / distribution / 'pwupd-chsh' pamdpwupdpasswdsrc = 'pam.d' / distribution / 'pwupd-passwd' pamdpasswdsrc = 'pam.d' / distribution / 'passwd' install_data(pamdpwupdchfnsrc, install_dir : pamconfdir) install_data(pamdpwupdchshsrc, install_dir : pamconfdir) install_data(pamdpwupdpasswdsrc, install_dir : pamconfdir) install_data(pamdpasswdsrc, install_dir : pamconfdir) # Unit tests subdir('tests') # Manual pages subdir('man') subdir('example') account-utils-1.3.0/meson_options.txt000066400000000000000000000013211521474342200177430ustar00rootroot00000000000000option('man', type : 'feature', value : 'auto', description : 'build and install man pages') option('pamlibdir', type : 'string', description : 'directory for PAM modules') option('pamconfdir', type : 'string', description : 'directory for PAM configuration') option('selinux', type: 'feature', value: 'auto', description: 'SELinux support') option('vendordir', type: 'string', value: '/usr/etc', description : 'directory for distribution provided config files (e.g. login.defs)') option('distribution', type : 'string', description : 'Name of used distribution: example, suse') option('tools', type: 'boolean', value: true, description: 'Build additional utilities') account-utils-1.3.0/pam.d/000077500000000000000000000000001521474342200153105ustar00rootroot00000000000000account-utils-1.3.0/pam.d/example/000077500000000000000000000000001521474342200167435ustar00rootroot00000000000000account-utils-1.3.0/pam.d/example/passwd000066400000000000000000000003041521474342200201640ustar00rootroot00000000000000#%PAM-1.0 auth required pam_unix_ng.so account required pam_unix_ng.so password requisite pam_pwquality.so password required pam_unix_ng.so use_authtok session required pam_unix_ng.so account-utils-1.3.0/pam.d/example/pwupd-chfn000066400000000000000000000003431521474342200207410ustar00rootroot00000000000000#%PAM-1.0 auth sufficient pam_rootok.so auth required pam_unix_ng.so account required pam_unix_ng.so password requisite pam_pwquality.so password required pam_unix_ng.so use_authtok session required pam_unix_ng.so account-utils-1.3.0/pam.d/example/pwupd-chsh000066400000000000000000000003431521474342200207500ustar00rootroot00000000000000#%PAM-1.0 auth sufficient pam_rootok.so auth required pam_unix_ng.so account required pam_unix_ng.so password requisite pam_pwquality.so password required pam_unix_ng.so use_authtok session required pam_unix_ng.so account-utils-1.3.0/pam.d/example/pwupd-passwd000066400000000000000000000003041521474342200213210ustar00rootroot00000000000000#%PAM-1.0 auth required pam_unix_ng.so account required pam_unix_ng.so password requisite pam_pwquality.so password required pam_unix_ng.so use_authtok session required pam_unix_ng.so account-utils-1.3.0/pam.d/suse/000077500000000000000000000000001521474342200162675ustar00rootroot00000000000000account-utils-1.3.0/pam.d/suse/passwd000066400000000000000000000002111521474342200175050ustar00rootroot00000000000000#%PAM-1.0 auth include common-auth account include common-account session include common-session password include common-password account-utils-1.3.0/pam.d/suse/pwupd-chfn000066400000000000000000000002501521474342200202620ustar00rootroot00000000000000#%PAM-1.0 auth sufficient pam_rootok.so auth include common-auth account include common-account session include common-session password include common-password account-utils-1.3.0/pam.d/suse/pwupd-chsh000066400000000000000000000002501521474342200202710ustar00rootroot00000000000000#%PAM-1.0 auth sufficient pam_rootok.so auth include common-auth account include common-account session include common-session password include common-password account-utils-1.3.0/pam.d/suse/pwupd-passwd000066400000000000000000000002111521474342200206420ustar00rootroot00000000000000#%PAM-1.0 auth include common-auth account include common-account session include common-session password include common-password account-utils-1.3.0/src/000077500000000000000000000000001521474342200151005ustar00rootroot00000000000000account-utils-1.3.0/src/chage.c000066400000000000000000000364601521474342200163240ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include #include #include #include #include "basics.h" #include "pwaccess.h" #include "varlink-client-common.h" #include "get_value.h" #include "get_logindefs.h" #include "drop_privs.h" #define DAY (24L*3600L) #define SCALE DAY static int oom(void) { fprintf(stderr, "Out of memory!\n"); return ENOMEM; } /* convert a string to a time_t value and return it as number of days since 1.1.1970. */ static long int str2date(const char *str) { struct tm tp; char *cp; time_t result; if (streq(str, "1969-12-31")) return -1; memset(&tp, 0, sizeof tp); cp = strptime(str, "%Y-%m-%d", &tp); if (!cp || *cp != '\0') return -1; result = mktime(&tp); if (result == (time_t) -1) return -1; return (result + (DAY/2)) / DAY; } /* convert time_t into a readable date string. */ static char * date2str(time_t date) { struct tm *tp; char buf[20]; tp = gmtime(&date); if (strftime(buf, sizeof(buf), "%Y-%m-%d", tp) == 0) { fprintf(stderr, "strftime failed!\n"); return NULL; } return strdup(buf); } static void format_date_buf(char *buf, size_t buf_size, long date_val) { if (date_val == -1) strlcpy(buf, "-1", buf_size); else { _cleanup_free_ char *p = date2str(date_val * SCALE); if (p != NULL) strlcpy(buf, p, buf_size); else strlcpy(buf, "-1", buf_size); } } static int prompt_and_check(char *buf, const char *prompt, char **result_ptr) { int r = get_value(buf, prompt, result_ptr); if (r < 0) return -r; if (*result_ptr == NULL) { fprintf(stderr, "chage aborted.\n"); return ENODATA; } return 0; } /* Print the time in a human readable format. */ static void print_date(time_t date, bool iso8601) { struct tm *tp; char buf[40]; tp = gmtime(&date); if (strftime(buf, sizeof buf, iso8601?"%F":"%b %d, %Y", tp) == 0) { fprintf(stderr, "strftime failed!\n"); return; } puts(buf); } /* Print the current values of the expiration fields. */ static int print_shadow_info (const char *user, struct spwd *sp, bool iso8601) { if (sp == NULL) { fprintf(stderr, "ERROR: No shadow entry for user '%s' found.\n", user); return ENODATA; } printf ("Last password change:\t\t"); if (sp->sp_lstchg == 0) printf("password change enforced\n"); else if (sp->sp_lstchg < 0) printf("never\n"); else print_date(sp->sp_lstchg * SCALE, iso8601); printf("Password expires:\t\t"); if (sp->sp_lstchg < 0 || sp->sp_max >= 10000 * (DAY / SCALE) || sp->sp_max < 0) printf("never\n"); else print_date(sp->sp_lstchg * SCALE + sp->sp_max * SCALE, iso8601); printf("Password inactive:\t\t"); if (sp->sp_lstchg < 0 || sp->sp_inact < 0 || sp->sp_max >= 10000 * (DAY / SCALE) || sp->sp_max < 0) printf("never\n"); else print_date(sp->sp_lstchg * SCALE + (sp->sp_max + sp->sp_inact) * SCALE, iso8601); printf("Account expires:\t\t"); if (sp->sp_expire < 0) printf("never\n"); else print_date(sp->sp_expire * SCALE, iso8601); printf("Minimum password age:\t\t"); if (sp->sp_min <= 0) printf("disabled\n"); else printf("%ld days\n", sp->sp_min); printf("Maximum password age:\t\t"); if (sp->sp_max <= 0) printf("disabled\n"); else printf("%ld days\n", sp->sp_max); printf("Password warning period:\t"); if (sp->sp_warn <= 0) printf("disabled\n"); else printf("%ld days\n", sp->sp_warn); printf("Password inactivity period:\t"); if (sp->sp_inact < 0) printf("disabled\n"); else printf("%ld days\n", sp->sp_inact); return 0; } static int update_account(const struct passwd *pw, const struct spwd *sp) { _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *passwd = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *shadow = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL; _cleanup_free_ char *error = NULL; _cleanup_(struct_result_free) struct result p = { .success = false, .error = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "Success", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct result, success), 0 }, { "ErrorMsg", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct result, error), 0 }, {} }; const char *error_id = NULL; int r; r = connect_to_pwupdd(&link, _VARLINK_PWUPD_SOCKET, &error); if (r < 0) { if (error) fprintf(stderr, "%s\n", error); else fprintf(stderr, "Cannot connect to pwupd! (%s)\n", strerror(-r)); return -r; } if (pw) { r = sd_json_variant_merge_objectbo(&passwd, SD_JSON_BUILD_PAIR_STRING("name", pw->pw_name), SD_JSON_BUILD_PAIR_STRING("passwd", pw->pw_passwd), SD_JSON_BUILD_PAIR_INTEGER("UID", pw->pw_uid), SD_JSON_BUILD_PAIR_INTEGER("GID", pw->pw_gid), SD_JSON_BUILD_PAIR_STRING("GECOS", pw->pw_gecos), SD_JSON_BUILD_PAIR_STRING("dir", pw->pw_dir), SD_JSON_BUILD_PAIR_STRING("shell", pw->pw_shell)); if (r < 0) { fprintf(stderr, "Error building passwd data: %s\n", strerror(-r)); return -r; } } r = sd_json_variant_merge_objectbo(&shadow, SD_JSON_BUILD_PAIR_STRING("name", sp->sp_namp), SD_JSON_BUILD_PAIR_STRING("passwd", sp->sp_pwdp), SD_JSON_BUILD_PAIR_INTEGER("lstchg", sp->sp_lstchg), SD_JSON_BUILD_PAIR_INTEGER("min", sp->sp_min), SD_JSON_BUILD_PAIR_INTEGER("max", sp->sp_max), SD_JSON_BUILD_PAIR_INTEGER("warn", sp->sp_warn), SD_JSON_BUILD_PAIR_INTEGER("inact", sp->sp_inact), SD_JSON_BUILD_PAIR_INTEGER("expire", sp->sp_expire), SD_JSON_BUILD_PAIR_INTEGER("flag", sp->sp_flag)); if (r < 0) { fprintf(stderr, "Error building shadow data: %s\n", strerror(-r)); return -r; } r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_VARIANT("shadow", shadow)); if (r >= 0 && passwd) r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_VARIANT("passwd", passwd)); if (r < 0) { fprintf(stderr, "JSON merge result object failed: %s", strerror(-r)); return -r; } r = sd_varlink_call(link, "org.openSUSE.pwupd.UpdatePasswdShadow", params, &result, &error_id); if (r < 0) { fprintf(stderr, "Failed to call UpdatePasswdShadow method: %s\n", strerror(-r)); return r; } /* dispatch before checking error_id, we may need the result for the error message */ r = sd_json_dispatch(result, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) { fprintf(stderr, "Failed to parse JSON answer: %s\n", strerror(-r)); return r; } if (error_id && strlen(error_id) > 0) { if (p.error) fprintf(stderr, "Error updating account information:\n%s\n", p.error); else fprintf(stderr, "Error updating account information:\n%s\n", error_id); return -EIO; } printf("Account information changed.\n"); return 0; } static void print_usage(FILE *stream) { fprintf(stream, "Usage: chage [options] [--help] [--version] [user]\n"); } static void print_help(void) { fprintf(stdout, "chage - change and list user expiry data\n\n"); print_usage(stdout); fputs(" -d, --lastday Set date of last password change\n", stdout); fputs(" -E, --expiredate Date on which user's password expires\n", stdout); fputs(" -i, --iso8601 Print dates as YYYY-MM-DD\n", stdout); fputs(" -I, --inactive Lock expired account after inactive days\n", stdout); fputs(" -l, --list List account aging information\n", stdout); fputs(" -m, --mindays Minimum # of days before password can be changed\n", stdout); fputs(" -M, --maxdays Maximum # of days before password can be canged\n", stdout); fputs(" -h, --help Give this help list\n", stdout); fputs(" -v, --version Print program version\n", stdout); fputs(" -W, --warndays # days of warning before password expires\n", stdout); fputs(" must be in the form of \"YYYY-MM-DD\"\n", stdout); } static void print_error(void) { fprintf(stderr, "Try `chage --help' for more information.\n"); } int main(int argc, char **argv) { _cleanup_(struct_passwd_freep) struct passwd *pw = NULL; _cleanup_(struct_shadow_freep) struct spwd *sp = NULL; _cleanup_free_ char *error = NULL; bool complete = false; char *user = NULL; char *expiredate = NULL; char *inactive = NULL; char *lastday = NULL; char *maxdays = NULL; char *mindays = NULL; char *warndays = NULL; int i_flag = 0; int l_flag = 0; int r; setlocale(LC_ALL, ""); while (1) { int c; int option_index = 0; static struct option long_options[] = { {"expiredate", required_argument, NULL, 'E' }, {"help", no_argument, NULL, 'h' }, {"inactive", required_argument, NULL, 'I' }, {"iso8601", no_argument, NULL, 'i' }, {"lastday", required_argument, NULL, 'd' }, {"list", no_argument, NULL, 'l' }, {"maxdays", required_argument, NULL, 'M' }, {"mindays", required_argument, NULL, 'm' }, {"version", no_argument, NULL, 'v' }, {"warndays", required_argument, NULL, 'W' }, {NULL, 0, NULL, '\0'} }; c = getopt_long(argc, argv, "E:hI:id:lM:m:vW:", long_options, &option_index); if (c == (-1)) break; switch (c) { case 'E': expiredate = optarg; break; case 'I': inactive = optarg; break; case 'i': i_flag = 1; break; case 'd': lastday = optarg; break; case 'M': maxdays = optarg; break; case 'm': mindays = optarg; break; case 'W': warndays = optarg; break; case 'l': l_flag = 1; break; case 'h': print_help(); return 0; case 'v': printf("chage (%s) %s\n", PACKAGE, VERSION); return 0; default: print_error(); return EINVAL; } } argc -= optind; argv += optind; if (argc == 1) user = argv[0]; if (argc > 1) { fprintf(stderr, "chage: Too many arguments.\n"); print_error(); return EINVAL; } if (l_flag && (expiredate || inactive || lastday || maxdays || mindays || warndays)) { fprintf(stderr, "The --list option cannot be combined with other options.\n"); print_error(); return EINVAL; } r = check_and_drop_privs(); if (r < 0) return -r; /* get user account data */ r = pwaccess_get_user_record(user?-1:(int64_t)getuid(), user?user:NULL, &pw, &sp, &complete, &error); if (r < 0) { fprintf(stderr, "get_user_record failed: %s\n", error?error:strerror(-r)); return -r; } if (pw == NULL) { fprintf(stderr, "ERROR: Unknown user '%s'.\n", user); return ENODATA; } if (!complete) { fprintf(stderr, "Permission denied.\n"); return EPERM; } if (!user) user = pw->pw_name; /* execute options */ if (l_flag) return print_shadow_info(user, sp, i_flag); if (getuid() != 0) { fprintf(stderr, "Permission denied.\n"); return EPERM; } /* create default shadow entry if there is none */ bool pw_changed = false; char *ep; if (!sp) { sp = calloc(1, sizeof(struct spwd)); if (!sp) return oom(); sp->sp_namp = strdup(pw->pw_name); if (!sp->sp_namp) return oom(); sp->sp_pwdp = pw->pw_passwd; pw->pw_passwd = strdup("x"); if (!pw->pw_passwd) return oom(); pw_changed = true; sp->sp_lstchg = time(NULL) / DAY; /* disable instead of requesting password change */ if (!sp->sp_lstchg) sp->sp_lstchg = -1; sp->sp_min = get_logindefs_num("PASS_MIN_DAYS", -1); sp->sp_max = get_logindefs_num("PASS_MAX_DAYS", -1); sp->sp_warn = get_logindefs_num("PASS_WARN_AGE", -1); sp->sp_inact = -1; sp->sp_expire = -1; } /* Use user provided values */ if (!(expiredate || inactive || lastday || maxdays || mindays || warndays)) { char buf[80]; snprintf(buf, sizeof(buf), "%ld", sp->sp_min); r = prompt_and_check(buf, "Minimum password age", &mindays); if (r != 0) return r; snprintf(buf, sizeof(buf), "%ld", sp->sp_max); r = prompt_and_check(buf, "Maximum password age", &maxdays); if (r != 0) return r; format_date_buf(buf, sizeof(buf), sp->sp_lstchg); r = prompt_and_check(buf, "Last password change (YYYY-MM-DD)", &lastday); if (r != 0) return r; snprintf(buf, sizeof(buf), "%ld", sp->sp_warn); r = prompt_and_check(buf, "Password warning period", &warndays); if (r != 0) return r; snprintf(buf, sizeof(buf), "%ld", sp->sp_inact); r = prompt_and_check(buf, "Password inactivity period", &inactive); if (r != 0) return r; format_date_buf(buf, sizeof(buf), sp->sp_expire); r = prompt_and_check(buf, "Account expires (YYYY-MM-DD)", &expiredate); if (r != 0) return r; } /* values are provided as option or asked for */ if (mindays) { long l; errno = 0; l = strtol(mindays, &ep, 10); if (errno == ERANGE || l < -1 || mindays == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'mindays=%s'\n", mindays); return EINVAL; } sp->sp_min = l; } if (maxdays) { long l; errno = 0; l = strtol(maxdays, &ep, 10); if (errno == ERANGE || l < -1 || maxdays == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'maxdays=%s'\n", maxdays); return EINVAL; } sp->sp_max = l; } if (warndays) { long l; errno = 0; l = strtol(warndays, &ep, 10); if (errno == ERANGE || l < -1 || warndays == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'warndays=%s'\n", warndays); return EINVAL; } sp->sp_warn = l; } if (inactive) { long l; errno = 0; l = strtol(inactive, &ep, 10); if (errno == ERANGE || l < -1 || inactive == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'inactive=%s'\n", inactive); return EINVAL; } sp->sp_inact = l; } if (lastday) { if (streq(lastday, "1969-12-31")) sp->sp_lstchg = -1; else { sp->sp_lstchg = str2date(lastday); if (sp->sp_lstchg == -1) { long l; errno = 0; l = strtol(lastday, &ep, 10); if (errno == ERANGE || l < -1 || lastday == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'lastday=%s'\n", lastday); return EINVAL; } sp->sp_lstchg = l; } } } if (expiredate) { if (streq(expiredate, "1969-12-31")) sp->sp_expire = -1; else { sp->sp_expire = str2date(expiredate); if (sp->sp_expire == -1) { long l; errno = 0; l = strtol(expiredate, &ep, 10); if (errno == ERANGE || l < -1 || expiredate == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'expiredate=%s'\n", expiredate); return EINVAL; } sp->sp_expire = l; } } } return update_account(pw_changed?pw:NULL, sp); } account-utils-1.3.0/src/chfn.c000066400000000000000000000230041521474342200161610ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include #include #include #include #include "basics.h" #include "pwaccess.h" #include "varlink-client-common.h" #include "get_value.h" #include "chfn_checks.h" #include "drop_privs.h" #define USEC_INFINITY ((uint64_t) UINT64_MAX) static int ask_or_print(const char *old, const char *prompt, char **input, char field) { _cleanup_free_ char *error = NULL; bool allowed = true; int r; allowed = may_change_field(getuid(), field, &error); if (error) { fprintf(stderr, "%s\n", error); return -EPERM; } if (allowed) { r = get_value(old, prompt, input); if (r < 0) return r; if (*input == NULL) { fprintf(stderr, "chfn aborted.\n"); return -ENODATA; } /* don't change string if equal */ if (streq(strempty(old), *input)) *input = mfree(*input); else { /* field "other" allows ',' and '=' */ if (!chfn_check_string(*input, field=='o'?":":":,=", &error)) { *input = mfree(*input); if (error) fprintf(stderr, "%s: %s\n", prompt, error); return -EINVAL; } } } else printf("%s: '%s'\n", prompt, strempty(old)); return 0; } static void print_usage(FILE *stream) { fprintf(stream, "Usage: chfn [options] [user]\n"); } static void print_help(void) { fprintf(stdout, "chfn - change user information\n\n"); print_usage(stdout); fputs(" -f, --full-name Change full name\n", stdout); fputs(" -h, --home-phone Change home phone number\n", stdout); fputs(" -o, --other Change other GECOS information\n", stdout); fputs(" -r, --room Change room number\n", stdout); fputs(" -w, --work-phone Change work phone number\n", stdout); fputs(" -u, --help Give this help list\n", stdout); fputs(" -v, --version Print program version\n", stdout); } static void print_error(void) { fprintf (stderr, "Try `chfn --help' for more information.\n"); } int main(int argc, char **argv) { _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; _cleanup_free_ char *new_full_name = NULL; _cleanup_free_ char *new_home_phone = NULL; _cleanup_free_ char *new_other = NULL; _cleanup_free_ char *new_room = NULL; _cleanup_free_ char *new_work_phone = NULL; _cleanup_free_ char *user_copy = NULL; const char *old_full_name = NULL; const char *old_home_phone = NULL; const char *old_other = NULL; const char *old_room = NULL; const char *old_work_phone = NULL; const char *user = NULL; _cleanup_free_ char *error = NULL; struct callback_data cb_data = { .resp = NULL, .error_code = 0 }; int r; setlocale(LC_ALL, ""); while (1) { int c; int option_index = 0; static struct option long_options[] = { {"full-name", required_argument, NULL, 'f' }, {"home-phone", required_argument, NULL, 'h' }, {"other", required_argument, NULL, 'o' }, {"room", required_argument, NULL, 'r' }, {"work-phone", required_argument, NULL, 'w' }, {"version", no_argument, NULL, 'v' }, {"help", no_argument, NULL, 'u' }, {NULL, 0, NULL, '\0'} }; c = getopt_long (argc, argv, "f:h:o:r:uvw:", long_options, &option_index); if (c == (-1)) break; switch (c) { case 'f': new_full_name = strdup(optarg); if (new_full_name == NULL) return ENOMEM; break; case 'h': new_home_phone = strdup(optarg); if (new_home_phone == NULL) return ENOMEM; break; case 'o': new_other = strdup(optarg); if (new_other == NULL) return ENOMEM; break; case 'r': new_room = strdup(optarg); if (new_room == NULL) return ENOMEM; break; case 'w': new_work_phone = strdup(optarg); if (new_work_phone == NULL) return ENOMEM; break; case 'u': print_help(); return 0; case 'v': printf("chfn (%s) %s\n", PACKAGE, VERSION); return 0; default: print_error(); return 1; } } argc -= optind; argv += optind; if (argc > 1) { fprintf(stderr, "chfn: Too many arguments.\n"); print_error(); return 1; } r = check_and_drop_privs(); if (r < 0) return -r; if (argc == 1) user = argv[0]; else { _cleanup_free_ char *name = NULL; r = pwaccess_get_account_name(getuid(), &name, &error); if (r < 0) { fprintf(stderr, "Get account name failed: %s\n", error?error:strerror(-r)); return -r; } user_copy = strdup(name); if (user_copy == NULL) { fprintf(stderr, "Out of memory!\n"); return ENOMEM; } user = user_copy; } /* no new values as argument provided, ask for them */ if (!new_full_name && !new_home_phone && !new_other && !new_room && !new_work_phone) { char *p; const char *f; struct passwd *pw = getpwnam(user); if (pw == NULL) { fprintf(stderr, "User (%s) not found!\n", user); return ENODATA; } /* set old values */ p = pw->pw_gecos; f = strsep(&p, ","); old_full_name = f; f = strsep(&p, ","); old_room = f; f = strsep(&p, ","); old_work_phone = f; f = strsep(&p, ","); old_home_phone = f; /* Anything left over is "other". */ old_other = p; printf("Enter the new value, or press return for the default.\n"); r = ask_or_print(old_full_name, "Full Name", &new_full_name, 'f'); if (r < 0) return -r; r = ask_or_print(old_room, "Room Number", &new_room, 'r'); if (r < 0) return -r; r = ask_or_print(old_work_phone, "Work Phone", &new_work_phone, 'w'); if (r < 0) return -r; r = ask_or_print(old_home_phone, "Home Phone", &new_home_phone, 'h'); if (r < 0) return -r; r = ask_or_print(old_other, "Other", &new_other, 'o'); if (r < 0) return -r; } /* abort if there is nothing to change */ if (!new_full_name && !new_home_phone && !new_other && !new_room && !new_work_phone) { printf("Nothing to change.\n"); return 0; } r = connect_to_pwupdd(&link, _VARLINK_PWUPD_SOCKET, &error); if (r < 0) { if (error) fprintf(stderr, "%s\n", error); else fprintf(stderr, "Cannot connect to pwupd! (%s)\n", strerror(-r)); return -r; } sd_varlink_set_userdata(link, &cb_data); r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_STRING("userName", user)); if (r >= 0 && new_full_name) r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_STRING("fullName", new_full_name)); if (r >= 0 && new_room) r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_STRING("room", new_room)); if (r >= 0 && new_work_phone) r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_STRING("workPhone", new_work_phone)); if (r >= 0 && new_home_phone) r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_STRING("homePhone", new_home_phone)); if (r >= 0 && new_other) r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_STRING("other", new_other)); if (r < 0) { fprintf(stderr, "Failed to build param list: %s\n", strerror(-r)); return -r; } r = sd_varlink_bind_reply(link, reply_callback); if (r < 0) { fprintf(stderr, "Failed to bind reply callback: %s\n", strerror(-r)); return -r; } r = sd_varlink_observe(link, "org.openSUSE.pwupd.Chfn", params); if (r < 0) { fprintf(stderr, "Failed to call chfn method: %s\n", strerror(-r)); return -r; } loop: for (;;) { r = sd_varlink_is_idle(link); if (r < 0) { fprintf(stderr, "Failed to check if varlink connection is idle: %s\n", strerror(-r)); return -r; } if (r > 0) break; r = sd_varlink_process(link); if (r < 0) { fprintf(stderr, "Failed to process varlink connection: %s\n", strerror(-r)); return -r; } if (r != 0) continue; r = sd_varlink_wait(link, USEC_INFINITY); if (r < 0) { fprintf(stderr, "Failed to wait for varlink connection events: %s\n", strerror(-r)); return -r; } } /* Check if an error occurred in the callback */ if (cb_data.error_code != 0) { if (cb_data.error_code == ENODATA) fprintf(stderr, "chfn: user '%s' does not exist\n", user); else fprintf(stderr, "chfn: %s\n", strerror(cb_data.error_code)); return cb_data.error_code; } if (cb_data.resp) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *answer = NULL; r = sd_json_buildo(&answer, SD_JSON_BUILD_PAIR("response", SD_JSON_BUILD_STRING(strempty(cb_data.resp->resp)))); if (r < 0) { fprintf(stderr, "Failed to build response list: %s\n", strerror(-r)); return -r; } free(cb_data.resp->resp); cb_data.resp = mfree(cb_data.resp); sd_json_variant_sensitive(answer); /* password is sensitive */ r = sd_varlink_observe(link, "org.openSUSE.pwupd.Conv", answer); if (r < 0) { fprintf(stderr, "Failed to call conv method: %s\n", strerror(-r)); return -r; } goto loop; } return cb_data.error_code; } account-utils-1.3.0/src/chsh.c000066400000000000000000000161461521474342200162010ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include #include #include #include #include "basics.h" #include "pwaccess.h" #include "varlink-client-common.h" #include "get_value.h" #include "drop_privs.h" #define USEC_INFINITY ((uint64_t) UINT64_MAX) static int get_shell_list(void) { _cleanup_(econf_freeFilep) econf_file *key_file = NULL; _cleanup_(econf_freeArrayp) char **keys = NULL; size_t size = 0; econf_err error; error = econf_readConfig(&key_file, NULL /* project */, _PATH_VENDORDIR /* usr_conf_dir */, "shells" /* config_name */, NULL /* config_suffix */, "" /* delim, key only */, "#" /* comment */); if (error != ECONF_SUCCESS) { fprintf(stderr, "Cannot parse shell files: %s", econf_errString(error)); return 1; } error = econf_getKeys(key_file, NULL, &size, &keys); if (error) { fprintf(stderr, "Cannot evaluate entries in shell files: %s", econf_errString(error)); return 1; } for (size_t i = 0; i < size; i++) printf("%s\n", keys[i]); return 0; } static void print_usage(FILE *stream) { fprintf(stream, "Usage: chsh [-s shell] [-l] [--help] [--version] [user]\n"); } static void print_help(void) { fprintf(stdout, "chsh - change login shell\n\n"); print_usage(stdout); fputs(" -l, --list-shells List allowed shells from /etc/shells\n", stdout); fputs(" -s, --shell Use 'shell' as new login shell\n", stdout); fputs(" -h, --help Give this help list\n", stdout); fputs(" -v, --version Print program version\n", stdout); } static void print_error(void) { fprintf (stderr, "Try `chsh --help' for more information.\n"); } int main(int argc, char **argv) { struct callback_data cb_data = { .resp = NULL, .error_code = 0 }; char *new_shell = NULL; int l_flag = 0; int r; setlocale(LC_ALL, ""); while (1) { int c; int option_index = 0; static struct option long_options[] = { {"shell", required_argument, NULL, 's' }, {"list-shells", no_argument, NULL, 'l' }, {"version", no_argument, NULL, 'v' }, {"help", no_argument, NULL, 'h' }, {NULL, 0, NULL, '\0'} }; c = getopt_long (argc, argv, "s:lvh", long_options, &option_index); if (c == (-1)) break; switch (c) { case 'l': l_flag = 1; break; case 's': if (!optarg) { print_usage(stderr); return 1; } new_shell = optarg; break; case 'h': print_help(); return 0; case 'v': printf("chsh (%s) %s\n", PACKAGE, VERSION); return 0; default: print_error(); return 1; } } argc -= optind; argv += optind; if (argc > 1 || (l_flag && argc > 0)) { fprintf(stderr, "chsh: Too many arguments.\n"); print_error(); return 1; } if (l_flag && new_shell) { fprintf(stderr, "chsh: Too many arguments.\n"); print_error(); return 1; } r = check_and_drop_privs(); if (r < 0) return -r; if (l_flag) return get_shell_list(); else { _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; _cleanup_(struct_passwd_freep) struct passwd *pw = NULL; _cleanup_free_ char *error = NULL; const char *user = NULL; const char *old_shell = NULL; if (argc == 1) r = pwaccess_get_user_record(-1, argv[0], &pw, NULL, NULL, &error); else r = pwaccess_get_user_record(getuid(), NULL, &pw, NULL, NULL, &error); if (r < 0) { if (argc == 1 && streq(error, "org.openSUSE.pwaccess.NoEntryFound")) fprintf(stderr, "chsh: user '%s' does not exist\n", argv[0]); else fprintf (stderr, "get_user_record failed: %s\n", error?error:strerror(-r)); return -r; } user = pw->pw_name; old_shell = pw->pw_shell; if (new_shell == NULL) { printf("Enter the new value, or press return for the default.\n"); r = get_value(old_shell, "Login Shell", &new_shell); if (r < 0) return -r; } /* we don't need to change the shell if here is no change */ if (new_shell == NULL || streq(old_shell, new_shell)) { printf("Shell not changed.\n"); return 0; } r = connect_to_pwupdd(&link, _VARLINK_PWUPD_SOCKET, &error); if (r < 0) { if (error) fprintf(stderr, "%s\n", error); else fprintf(stderr, "Cannot connect to pwupd! (%s)\n", strerror(-r)); return -r; } sd_varlink_set_userdata(link, &cb_data); r = sd_json_buildo(¶ms, SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(user)), SD_JSON_BUILD_PAIR("shell", SD_JSON_BUILD_STRING(strempty(new_shell)))); if (r < 0) { fprintf(stderr, "Failed to build param list: %s\n", strerror(-r)); return -r; } r = sd_varlink_bind_reply(link, reply_callback); if (r < 0) { fprintf(stderr, "Failed to bind reply callback: %s\n", strerror(-r)); return -r; } r = sd_varlink_observe(link, "org.openSUSE.pwupd.Chsh", params); if (r < 0) { fprintf(stderr, "Failed to call chsh method: %s\n", strerror(-r)); return -r; } loop: for (;;) { r = sd_varlink_is_idle(link); if (r < 0) { fprintf(stderr, "Failed to check if varlink connection is idle: %s\n", strerror(-r)); return -r; } if (r > 0) break; r = sd_varlink_process(link); if (r < 0) { fprintf(stderr, "Failed to process varlink connection: %s\n", strerror(-r)); return -r; } if (r != 0) continue; r = sd_varlink_wait(link, USEC_INFINITY); if (r < 0) { fprintf(stderr, "Failed to wait for varlink connection events: %s\n", strerror(-r)); return -r; } } /* Check if an error occurred in the callback */ if (cb_data.error_code != 0) { if (cb_data.error_code == ENODATA) fprintf(stderr, "chsh: user '%s' does not exist\n", user); else fprintf(stderr, "chsh: %s\n", strerror(cb_data.error_code)); return cb_data.error_code; } if (cb_data.resp) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *answer = NULL; r = sd_json_buildo(&answer, SD_JSON_BUILD_PAIR("response", SD_JSON_BUILD_STRING(strempty(cb_data.resp->resp)))); if (r < 0) { fprintf(stderr, "Failed to build response list: %s\n", strerror(-r)); return -r; } free(cb_data.resp->resp); cb_data.resp = mfree(cb_data.resp); sd_json_variant_sensitive(answer); /* password is sensitive */ r = sd_varlink_observe(link, "org.openSUSE.pwupd.Conv", answer); if (r < 0) { fprintf(stderr, "Failed to call conv method: %s\n", strerror(-r)); return -r; } goto loop; } } return cb_data.error_code; } account-utils-1.3.0/src/expiry.c000066400000000000000000000075021521474342200165700ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include "basics.h" #include "pwaccess.h" #include "chauthtok.h" #include "drop_privs.h" static void print_usage(FILE *stream) { fprintf(stream, "Usage: expiry [-c|-f] [user] [--help] [--version]\n"); } static void print_help(void) { fprintf(stdout, "expiry - check password expiration and force password change\n\n"); print_usage(stdout); fputs(" -c, --check Print number of days when password expires\n", stdout); fputs(" -f, --force Force password change if password is expired\n", stdout); fputs(" -h, --help Give this help list\n", stdout); fputs(" -v, --version Print program version\n", stdout); } static void print_error(void) { fprintf (stderr, "Try `expiry --help' for more information.\n"); } int main(int argc, char **argv) { _cleanup_free_ char *error = NULL; _cleanup_free_ char *user = NULL; long daysleft = -1; int cflg = 0; int fflg = 0; int r; setlocale(LC_ALL, ""); while (1) { int c; int option_index = 0; static struct option long_options[] = { {"check", no_argument, NULL, 'c' }, {"force", no_argument, NULL, 'f' }, {"help", no_argument, NULL, 'h' }, {"version", no_argument, NULL, 'v' }, {NULL, 0, NULL, '\0'} }; c = getopt_long (argc, argv, "cfhv", long_options, &option_index); if (c == (-1)) break; switch (c) { case 'c': cflg = 1; break; case 'f': fflg = 1; break; case 'h': print_help(); return 0; case 'v': printf("expiry (%s) %s\n", PACKAGE, VERSION); return 0; default: print_error(); return 1; } } argc -= optind; argv += optind; if (argc > 1) { fprintf(stderr, "expiry: too many arguments.\n"); print_error(); return EINVAL; } if (cflg+fflg > 1) { fprintf(stderr, "expiry: options -c and -f conflict.\n"); print_error(); return EINVAL; } r = check_and_drop_privs(); if (r < 0) return -r; /* common for -c and -f */ if (argc == 1) { user = strdup(argv[0]); if (!user) { fprintf(stderr, "Out of memory!\n"); return ENOMEM; } } else { r = pwaccess_get_account_name(getuid(), &user, &error); if (r < 0) { fprintf(stderr, "Get account name failed: %s\n", error?error:strerror(-r)); return -r; } } r = pwaccess_check_expired(user, &daysleft, NULL /* pwchangeable */, &error); if (r < 0) { fprintf(stderr, "Calling pwaccess check expired failed: %s\n", error?error:strerror(-r)); return -r; } if (cflg) { if (daysleft >= 0) printf("Your password will expire in %ld %s.\n", daysleft, (daysleft == 1)?"day":"days"); /* return expire status as return value */ return r; } else if (fflg) { switch (r) { case PWA_EXPIRED_NO: return 0; break; case PWA_EXPIRED_ACCT: printf("Your account has expired; please contact your system administrator.\n"); return EPERM; break; case PWA_EXPIRED_CHANGE_PW: printf("Your password has expired.\n"); break; case PWA_EXPIRED_PW: printf("Your password is inactive; please contact your system administrator.\n"); return EPERM; break; default: fprintf(stderr, "Unexpected expire value: %i\n", r); return EINVAL; break; } return chauthtok(user, PAM_CHANGE_EXPIRED_AUTHTOK); } else { fprintf(stderr, "expiry: no arguments provided.\n"); print_error(); return 1; } return 0; } account-utils-1.3.0/src/map_range.c000066400000000000000000000003041521474342200171720ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "basics.h" #include "map_range.h" void map_range_freep(struct map_range **var) { if (!var || !*var) return; *var = mfree(*var); } account-utils-1.3.0/src/map_range.h000066400000000000000000000005071521474342200172040ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include struct map_range { int64_t upper; /* first ID inside the namespace */ int64_t lower; /* first ID outside the namespace */ int64_t count; /* Length of the inside and outside ranges */ }; extern void map_range_freep(struct map_range **var); account-utils-1.3.0/src/newidmapd.c000066400000000000000000000437311521474342200172240ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include #include "basics.h" #include "mkdir_p.h" #include "varlink-service-common.h" #include "pwaccess.h" #include "map_range.h" #include "varlink-org.openSUSE.newidmapd.h" #define USEC_PER_SEC ((uint64_t) 1000000ULL) /* event loop which quits after 30 seconds idle time */ #define DEFAULT_EXIT_USEC (30*USEC_PER_SEC) #define UID_MAX ((uid_t)-1) struct parameters { pid_t pid; char *map; /* see varlink interface definition for valid names */ int nranges; struct map_range *mappings; sd_json_variant *content_map_ranges; }; static void parameters_free(struct parameters *var) { var->map = mfree(var->map); var->nranges = 0; map_range_freep(&(var->mappings)); var->content_map_ranges = sd_json_variant_unref(var->content_map_ranges); } static int open_pidfd(pid_t pid) { _cleanup_free_ char *proc_dir = NULL; int proc_dir_fd; if (asprintf(&proc_dir, "/proc/%u/", pid) == -1) { log_msg(LOG_ERR, "Out of memory!"); return -ENOMEM; } proc_dir_fd = open(proc_dir, O_DIRECTORY); if (proc_dir_fd < 0) { log_msg(LOG_ERR, "Open proc directory (%s) failed: %m\n", proc_dir); return -errno; } return proc_dir_fd; } static bool verify_range(uid_t uid, int64_t start, int64_t count, const struct map_range mapping) { if (mapping.count == 0) return false; /* Allow a process to map its own uid */ if ((mapping.count == 1) && (uid == mapping.lower)) return true; /* first ID outside namespace must be between start and start+count */ if (mapping.lower < start || mapping.lower >= start+count) return false; /* last ID outside must be smaller than start+count. -1 because lower is already the first ID. */ if ((mapping.lower+mapping.count-1) < (start+count)) return true; return false; } /* result < 0: error (-errno) 0 : range is valid 1 : range is invalid */ static int verify_ranges(uid_t uid, int nranges, const struct map_range *mappings, const char *map) { const char *subid_file = NULL; _cleanup_(econf_freeFilep) econf_file *econf = NULL; econf_err error; char *user; _cleanup_free_ char *pwerror = NULL; _cleanup_free_ char *val = NULL; long start, count; int r; r = pwaccess_get_account_name(uid, &user, &pwerror); if (r < 0) { log_msg(LOG_ERR, "Cannot get account data for uid '%u': %s", uid, pwerror?pwerror:strerror(-r)); return r; } if (streq(map, "uid_map")) subid_file = "/etc/subuid"; else if (streq(map, "gid_map")) subid_file = "/etc/subgid"; else { log_msg(LOG_ERR, "Unknown map name: '%s'", map); return -EINVAL; } error = econf_readFile(&econf, subid_file, ":", "#"); if (error != ECONF_SUCCESS) { log_msg(LOG_ERR, "Cannot open %s: %s", subid_file, econf_errString(error)); if (error == ECONF_NOFILE) return -ENOENT; else return -EIO; } error = econf_getStringValue(econf, NULL, user, &val); if (error != ECONF_SUCCESS) { if (error == ECONF_NOKEY) log_msg(LOG_ERR, "Mapping range for user '%s' not found in %s", user, subid_file); else log_msg(LOG_ERR, "Error retrieving key '%s': %s", user, econf_errString(error)); return -ENODATA; } char *cp = strchr(val, ':'); if (cp == NULL) { log_msg(LOG_ERR, "Invalid format for user %s in %s: %s", user, subid_file, val); return -EINVAL; } *cp++='\0'; char *ep = NULL; errno = 0; start = strtol(val, &ep, 10); if (errno == ERANGE || start < -1 || start > UID_MAX || val == ep || *ep != '\0') { log_msg(LOG_ERR, "Cannot parse 'start' value (%s,%s,%s)", subid_file, user, val); return -EINVAL; } errno = 0; count = strtol(cp, &ep, 10); if (errno == ERANGE || count < -1 || count >= (UID_MAX - start) || cp == ep || *ep != '\0') { log_msg(LOG_ERR, "Cannot parse 'count' value (%s,%s,%s)", subid_file, user, cp); return -EINVAL; } log_msg(LOG_DEBUG, "%s: user=%s, start=%li, count=%li", subid_file, user, start, count); for (int i = 0; i < nranges; i++) { if (!verify_range(uid, start, count, mappings[i])) return 1; } return 0; } static int write_mapping(int proc_dir_fd, int nranges, const struct map_range *mappings, const char *map) { _cleanup_free_ char *res = NULL; _cleanup_close_ int fd = -EBADF; int r; res = strdup(""); if (res == NULL) { log_msg(LOG_ERR, "Out of memory!"); return -ENOMEM; } for (int i = 0; i < nranges; i++) { _cleanup_free_ char *old_res = res; if (asprintf(&res, "%s%lu %lu %lu\n", old_res, mappings[i].upper, mappings[i].lower, mappings[i].count) == -1) { log_msg(LOG_ERR, "Out of memory!"); return -ENOMEM; } } log_msg(LOG_DEBUG, "mapping string: '%s'", res); /* Write the mapping to the mapping file */ fd = openat(proc_dir_fd, map, O_WRONLY|O_NOFOLLOW|O_CLOEXEC); if (fd < 0) { r = -errno; log_msg(LOG_ERR, "Failed to open '%s': %s", map, strerror(-r)); return r; } if (write(fd, res, strlen(res)) == -1) { r = -errno; log_msg(LOG_ERR, "Failed to write to '%s': %s", map, strerror(-r)); return r; } if (close(fd) != 0 && errno != EINTR) { r = -errno; log_msg(LOG_ERR, "Failed to close '%s': %s", map, strerror(-r)); return r; } return 0; } static int vl_method_write_mappings(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL; _cleanup_(parameters_free) struct parameters p = { .pid = 0, .map = NULL, .nranges = 0, .mappings = NULL, .content_map_ranges = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "PID", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, offsetof(struct parameters, pid), SD_JSON_MANDATORY}, { "Map", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, map), SD_JSON_NULLABLE}, { "MapRanges", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant, offsetof(struct parameters, content_map_ranges), SD_JSON_MANDATORY}, {} }; _cleanup_close_ int proc_dir_fd = -EBADF; uid_t peer_uid; gid_t peer_gid; int r; log_msg(LOG_INFO, "Varlink method \"WriteMappings\" called..."); r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); if (r < 0) { log_msg(LOG_ERR, "WriteMappings request: varlink dispatch failed: %s", strerror(-r)); return r; } if (isempty(p.map)) { log_msg(LOG_ERR, "No map name provided."); return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "No 'Map' entry provided.")); } if (!streq(p.map, "uid_map") && !streq(p.map, "gid_map")) { log_msg(LOG_ERR, "Map name is neither 'uid_map' nor 'gid_map'."); return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Unknown map name provided.")); } if (!sd_json_variant_is_array(p.content_map_ranges)) { fprintf(stderr, "JSON 'MapRanges' is no array!\n"); return -EINVAL; } size_t nranges = sd_json_variant_elements(p.content_map_ranges); /* 340 entries is the kernel limit since 4.16 */ if (nranges > 340) { log_msg(LOG_ERR, "Too many MapRanges entries: %i, limit is 340", p.nranges); return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Entry 'MapRanges' has too many entries (>340)")); } p.nranges = nranges; p.mappings = calloc(p.nranges, sizeof(struct map_range)); if (p.mappings == NULL) { log_msg(LOG_ERR, "Out of memory!"); return -ENOMEM; } for (int i = 0; i < p.nranges; i++) { struct map_range e = { .upper = -1, .lower = -1, .count = -1, }; static const sd_json_dispatch_field dispatch_entry_table[] = { { "upper", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_int64, offsetof(struct map_range, upper), SD_JSON_MANDATORY }, { "lower", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_int64, offsetof(struct map_range, lower), SD_JSON_MANDATORY }, { "count", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_int64, offsetof(struct map_range, count), SD_JSON_MANDATORY }, {} }; sd_json_variant *entry = sd_json_variant_by_index(p.content_map_ranges, i); if (!sd_json_variant_is_object(entry)) { log_msg(LOG_ERR, "entry is no object!"); return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Entry 'MapRanges' is no object")); } r = sd_json_dispatch(entry, dispatch_entry_table, SD_JSON_ALLOW_EXTENSIONS, &e); if (r < 0) { log_msg(LOG_ERR, "Failed to parse JSON map_ranges entry: %s", strerror(-r)); return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Failed to parse MapRanges object")); } if (e.upper < 0 || e.upper > UID_MAX || e.lower < 0 || e.lower > UID_MAX || e.count < 1 || e.count > (UID_MAX - e.upper)) { log_msg(LOG_ERR, "Invalid map_ranges upper=%" PRIi64 ", lower=%" PRIi64 ", count=%" PRIi64, e.upper, e.lower, e.count); return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Failed to parse MapRanges object")); } log_msg(LOG_DEBUG, "map_ranges[%i] (%s): upper=%" PRIi64 ", lower=%" PRIi64 ", count=%" PRIi64, i, p.map, e.upper, e.lower, e.count); p.mappings[i] = e; } r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) { log_msg(LOG_ERR, "Failed to get peer UID: %s", strerror(-r)); return r; } r = sd_varlink_get_peer_gid(link, &peer_gid); if (r < 0) { log_msg(LOG_ERR, "Failed to get peer GID: %s", strerror(-r)); return r; } proc_dir_fd = open_pidfd(p.pid); if (proc_dir_fd < 0) return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Cannot open '/proc/'")); /* Get the effective uid and effective gid of the target process */ struct stat st; r = fstat(proc_dir_fd, &st); if (r < 0) { log_msg(LOG_ERR, "Could not stat proc directory: %s", strerror(-r)); return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Cannot access '/proc/'")); } if (st.st_uid != peer_uid || st.st_gid != peer_gid) { log_msg(LOG_ERR, "PID %i is owned by a different user: peer_uid=%u st_uid=%u peer_gid=%u st_gid=%u", p.pid, peer_uid, st.st_uid, peer_gid, st.st_gid); return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.PermissionDenied", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "PID is owned by a different user")); } r = verify_ranges(peer_uid, p.nranges, p.mappings, p.map); if (r < 0) { return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Mapping ranges are not correct")); } if (r > 0) { return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.PermissionDenied", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Mapping ranges are not correct")); } r = write_mapping(proc_dir_fd, p.nranges, p.mappings, p.map); if (r < 0) { return sd_varlink_errorbo(link, "org.openSUSE.newidmapd.PermissionDenied", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "Cannot write to '/proc//'")); } return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true)); } /* Send a messages to systemd daemon, that inicialization of daemon is finished and daemon is ready to accept connections. */ static void announce_ready(void) { int r = sd_notify(0, "READY=1\n" "STATUS=Processing requests..."); if (r < 0) log_msg(LOG_ERR, "sd_notify(READY) failed: %s", strerror(-r)); } static void announce_stopping(void) { int r = sd_notify(0, "STOPPING=1\n" "STATUS=Shutting down..."); if (r < 0) log_msg(LOG_ERR, "sd_notify(STOPPING) failed: %s", strerror(-r)); } static int varlink_event_loop_with_idle(sd_event *e, sd_varlink_server *s) { int r, code; for (;;) { r = sd_event_get_state(e); if (r < 0) return r; if (r == SD_EVENT_FINISHED) break; r = sd_event_run(e, DEFAULT_EXIT_USEC); if (r < 0) return r; if (r == 0 && (sd_varlink_server_current_connections(s) == 0)) sd_event_exit(e, 0); } r = sd_event_get_exit_code(e, &code); if (r < 0) return r; return code; } static int run_varlink(bool socket_activation) { int r; _cleanup_(sd_event_unrefp) sd_event *event = NULL; _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; r = mkdir_p(_VARLINK_NEWIDMAPD_SOCKET_DIR, 0755); if (r < 0) { log_msg(LOG_ERR, "Failed to create directory '"_VARLINK_NEWIDMAPD_SOCKET_DIR"' for Varlink socket: %s", strerror(-r)); return r; } r = sd_event_new(&event); if (r < 0) { log_msg(LOG_ERR, "Failed to create new event: %s", strerror(-r)); return r; } r = sd_varlink_server_new (&varlink_server, SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA|SD_VARLINK_SERVER_INPUT_SENSITIVE); if (r < 0) { log_msg(LOG_ERR, "Failed to allocate varlink server: %s", strerror(-r)); return r; } r = sd_varlink_server_set_description (varlink_server, "newidmapd"); if (r < 0) { log_msg(LOG_ERR, "Failed to set varlink server description: %s", strerror(-r)); return r; } r = sd_varlink_server_set_info(varlink_server, NULL, PACKAGE" (newidmapd)", VERSION, "https://github.com/thkukuk/newidmapd"); if (r < 0) return r; r = sd_varlink_server_add_interface(varlink_server, &vl_interface_org_openSUSE_newidmapd); if (r < 0) { log_msg(LOG_ERR, "Failed to add interface: %s", strerror(-r)); return r; } r = sd_varlink_server_bind_method_many(varlink_server, "org.openSUSE.newidmapd.WriteMappings", vl_method_write_mappings, "org.openSUSE.newidmapd.GetEnvironment", vl_method_get_environment, "org.openSUSE.newidmapd.Ping", vl_method_ping, "org.openSUSE.newidmapd.Quit", vl_method_quit, "org.openSUSE.newidmapd.SetLogLevel", vl_method_set_log_level); if (r < 0) { log_msg(LOG_ERR, "Failed to bind Varlink methods: %s", strerror(-r)); return r; } sd_varlink_server_set_userdata(varlink_server, event); r = sd_varlink_server_attach_event(varlink_server, event, SD_EVENT_PRIORITY_NORMAL); if (r < 0) { log_msg(LOG_ERR, "Failed to attach to event: %s", strerror(-r)); return r; } r = sd_varlink_server_listen_auto(varlink_server); if (r < 0) { log_msg(LOG_ERR, "Failed to listen: %s", strerror(-r)); return r; } if (!socket_activation) { r = sd_varlink_server_listen_address(varlink_server, _VARLINK_NEWIDMAPD_SOCKET, 0666); if (r < 0) { log_msg(LOG_ERR, "Failed to bind to Varlink socket: %s", strerror(-r)); return r; } } announce_ready(); if (socket_activation) r = varlink_event_loop_with_idle(event, varlink_server); else r = sd_event_loop(event); announce_stopping(); return r; } static void print_help(void) { printf("newidmapd - manage passwd and shadow\n"); printf(" -s, --socket Activation through socket\n"); printf(" -d, --debug Debug mode\n"); printf(" -v, --verbose Verbose logging\n"); printf(" -?, --help Give this help list\n"); printf(" --version Print program version\n"); } int main(int argc, char **argv) { bool socket_activation = false; while (1) { int c; int option_index = 0; static struct option long_options[] = { {"socket", no_argument, NULL, 's'}, {"debug", no_argument, NULL, 'd'}, {"verbose", no_argument, NULL, 'v'}, {"version", no_argument, NULL, '\255'}, {"usage", no_argument, NULL, '?'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, '\0'} }; c = getopt_long(argc, argv, "sdvh?", long_options, &option_index); if (c == (-1)) break; switch (c) { case 's': socket_activation = true; break; case 'd': set_max_log_level(LOG_DEBUG); break; case '?': case 'h': print_help(); return 0; case 'v': set_max_log_level(LOG_INFO); break; case '\255': fprintf(stdout, "newidmapd (%s) %s\n", PACKAGE, VERSION); return 0; default: print_help(); return 1; } } argc -= optind; argv += optind; if (argc > 1) { fprintf(stderr, "Try `newidmapd --help' for more information.\n"); return 1; } log_msg(LOG_INFO, "Starting newidmapd (%s) %s...", PACKAGE, VERSION); int r = run_varlink(socket_activation); if (r < 0) { log_msg(LOG_ERR, "ERROR: varlink loop failed: %s", strerror(-r)); return -r; } log_msg(LOG_INFO, "newidmapd stopped."); return 0; } account-utils-1.3.0/src/newxidmap.c000066400000000000000000000147021521474342200172440ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include "basics.h" #include "map_range.h" struct status { bool success; char *error; }; static void status_free (struct status *var) { var->error = mfree(var->error); } static int get_map_ranges(int ranges, char **argv, struct map_range **res) { _cleanup_(map_range_freep) struct map_range *mappings = NULL; char *ep; assert(res); *res = NULL; mappings = calloc(ranges, sizeof(struct map_range)); if (!mappings) { fprintf(stderr, "Out of memory!\n"); return -ENOMEM; } /* Gather up the ranges from the command line */ for (int i = 0; i < ranges; i++) { int j = i*3; errno = 0; mappings[i].upper = strtoul(argv[j], &ep, 10); if (errno == ERANGE || argv[j] == ep || *ep != '\0') { fprintf(stderr, "Cannot parse upper argument ('%s')\n", argv[j]); return EINVAL; } errno = 0; mappings[i].lower = strtoul(argv[j+1], &ep, 10); if (errno == ERANGE || argv[j+1] == ep || *ep != '\0') { fprintf(stderr, "Cannot parse lower argument ('%s')\n", argv[j+1]); return EINVAL; } errno = 0; mappings[i].count = strtoul(argv[j+2], &ep, 10); if (errno == ERANGE || argv[j] == ep || *ep != '\0') { fprintf(stderr, "Cannot parse count argument ('%s')\n", argv[j]); return EINVAL; } } *res = TAKE_PTR(mappings); return 0; } static int connect_to_newidmapd(sd_varlink **ret, const char *socket, char **error) { _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; int r; r = sd_varlink_connect_address(&link, socket); if (r < 0) { if (error) if (asprintf (error, "Failed to connect to %s: %s", socket, strerror(-r)) < 0) { error = NULL; r = -ENOMEM; } return r; } *ret = TAKE_PTR(link); return 0; } static void print_usage(FILE *stream) { fprintf(stream, "Usage: new"XID"map [|fd:] <"XID"> [ <"XID"> ] ... [--help] [--version]\n"); } static void print_help(void) { fprintf(stdout, "new"XID"map - set "XID" mapping of a user namespace\n\n"); print_usage(stdout); fputs(" -h, --help Give this help list\n", stdout); fputs(" -v, --version Print program version\n", stdout); } static void print_error(void) { fprintf (stderr, "Try `new"XID"map --help' for more information.\n"); } int main(int argc, char **argv) { _cleanup_(status_free) struct status p = { .success = false, .error = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "Success", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct status, success), 0 }, { "ErrorMsg", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct status, error), 0 }, {} }; _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; _cleanup_free_ char *error = NULL; sd_json_variant *result = NULL; const char *error_id = NULL; int ranges; _cleanup_(map_range_freep) struct map_range *mappings = NULL; pid_t arg_pid; char *ep; int r; while (1) { int c; int option_index = 0; static struct option long_options[] = { {"version", no_argument, NULL, 'v' }, {"help", no_argument, NULL, 'h' }, {NULL, 0, NULL, '\0'} }; c = getopt_long (argc, argv, "vh", long_options, &option_index); if (c == (-1)) break; switch (c) { case 'h': print_help(); return 0; case 'v': printf("new"XID"map (%s) %s\n", PACKAGE, VERSION); return 0; default: print_error(); return 1; } } argc -= optind; argv += optind; if (argc < 4) { fprintf(stderr, "new"XID"map: Not enough arguments.\n"); print_error(); return 1; } const char *pid_str = argv[0]; if (strlen(pid_str) > 3 && startswith(pid_str, "fd:")) { fprintf(stderr, "'fd:' as argument is currently not supported\n"); return EINVAL; } errno = 0; arg_pid = strtol(pid_str, &ep, 10); if (errno == ERANGE || arg_pid < -1 || pid_str == ep || *ep != '\0') { fprintf(stderr, "Cannot parse PID argument ('%s')\n", pid_str); return EINVAL; } ranges = (argc - 1) / 3; if ((ranges * 3) != (argc -1)) { fprintf(stderr, "Number of arguments is wrong (not a multiple of 3 + 1)!\n"); return EINVAL; } r = get_map_ranges(ranges, argv + 1, &mappings); if (r < 0) return -r; r = connect_to_newidmapd(&link, _VARLINK_NEWIDMAPD_SOCKET, &error); if (r < 0) { if (error) fprintf(stderr, "%s\n", error); else fprintf(stderr, "Cannot connect to newidmapd! (%s)\n", strerror(-r)); return -r; } for (int i = 0; i < ranges; i++) { r = sd_json_variant_append_arraybo(&array, SD_JSON_BUILD_PAIR_UNSIGNED("upper", mappings[i].upper), SD_JSON_BUILD_PAIR_UNSIGNED("lower", mappings[i].lower), SD_JSON_BUILD_PAIR_UNSIGNED("count", mappings[i].count)); if (r < 0) { fprintf(stderr, "Appending array failed: %s\n", strerror(-r)); return -r; } } r = sd_json_buildo(¶ms, SD_JSON_BUILD_PAIR_INTEGER("PID", arg_pid), SD_JSON_BUILD_PAIR_STRING("Map", XID"_map"), SD_JSON_BUILD_PAIR_VARIANT("MapRanges", array)); if (r < 0) { fprintf(stderr, "Failed to build param list: %s\n", strerror(-r)); return -r; } //sd_json_variant_dump(params, SD_JSON_FORMAT_NEWLINE, stdout, NULL); r = sd_varlink_call(link, "org.openSUSE.newidmapd.WriteMappings", params, &result, &error_id); if (r < 0) { fprintf(stderr, "Failed to call WriteMappings method: %s\n", strerror(-r)); return -r; } /* dispatch before checking error_id, we may need the result for the error message */ r = sd_json_dispatch(result, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) { fprintf(stderr, "Failed to parse JSON answer: %s", strerror(-r)); return -r; } if (!isempty(error) || !isempty(error_id)) { fprintf(stderr, "%s\n", p.error?p.error:error_id); return EIO; } return 0; } account-utils-1.3.0/src/pam_debuginfo.c000066400000000000000000000106301521474342200200430ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include "config.h" #include #include #include #include #include #ifdef WITH_SELINUX #include #endif #include "basics.h" #include "no_new_privs.h" static void freeconp(char **p) { #ifdef WITH_SELINUX if (!p || !*p) return; freecon(*p); *p = NULL; #else (void)p; #endif } static const char * selinux_status(pam_handle_t *pamh) { #ifdef WITH_SELINUX if (is_selinux_enabled() > 0) { int r = security_getenforce(); switch (r) { case 1: return ", selinux=enforcing"; break; case 0: return ", selinux=permissive"; break; default: pam_syslog(pamh, LOG_ERR, "selinux error: %s", strerror(errno)); return ", selinux=error"; break; } } else return ", selinux=off"; #else (void)pamh; return ""; #endif } /* XXX add flags */ static void log_info(pam_handle_t *pamh, const char *type, int flags, int loglevel) { _cleanup_(freeconp) char *secon = NULL; const void *service = NULL; const void *user = NULL; const void *ruser = NULL; const void *rhost = NULL; const void *tty = NULL; const char *login_name; #ifdef WITH_SELINUX if (getcon(&secon) < 0) pam_syslog(pamh, LOG_ERR, "getcon() failed: %s", strerror(errno)); #endif pam_get_item(pamh, PAM_SERVICE, &service); pam_get_item(pamh, PAM_USER, &user); pam_get_item(pamh, PAM_RUSER, &ruser); pam_get_item(pamh, PAM_RHOST, &rhost); pam_get_item(pamh, PAM_TTY, &tty); login_name = pam_modutil_getlogin(pamh); /* XXX split flags in single bits with defines */ pam_syslog(pamh, loglevel, "service=%s type=%s flags=%d " "logname=%s uid=%u euid=%u " "tty=%s ruser=%s rhost=%s " "user=%s%s%s%s%s", strna(service), type, flags, strna(login_name), getuid(), geteuid(), strna(tty), strna(ruser), strna(rhost), strna(user), no_new_privs_enabled()?", no_new_privs=1":"", selinux_status(pamh), secon?", context=":"", secon?secon:""); } static int parse_args(pam_handle_t *pamh, int flags _unused_, int argc, const char **argv, int *loglevel) { *loglevel = LOG_DEBUG; /* step through arguments */ for (; argc-- > 0; ++argv) { const char *cp; if ((cp = startswith(*argv, "loglevel=")) != NULL) { if (streq(cp, "debug")) *loglevel = LOG_DEBUG; else if (streq(cp, "info")) *loglevel = LOG_INFO; else if (streq(cp, "notice")) *loglevel = LOG_NOTICE; else if (streq(cp, "warning")) *loglevel = LOG_WARNING; else if (streq(cp, "error")) *loglevel = LOG_ERR; else if (streq(cp, "critical")) *loglevel = LOG_CRIT; else if (streq(cp, "alert")) *loglevel = LOG_ALERT; else if (streq(cp, "emerg")) *loglevel = LOG_EMERG; else pam_syslog(pamh, LOG_ERR, "Unknown loglevel value: %s", cp); } else pam_syslog(pamh, LOG_ERR, "Unknown option: %s", *argv); } return 0; } int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { int loglevel; parse_args(pamh, flags, argc, argv, &loglevel); log_info(pamh, "account", flags, loglevel); return PAM_IGNORE; } int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { int loglevel; parse_args(pamh, flags, argc, argv, &loglevel); log_info(pamh, "auth", flags, loglevel); return PAM_IGNORE; } int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { int loglevel; parse_args(pamh, flags, argc, argv, &loglevel); log_info(pamh, "setcred", flags, loglevel); return PAM_IGNORE; } int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) { int loglevel; parse_args(pamh, flags, argc, argv, &loglevel); log_info(pamh, "password", flags, loglevel); return PAM_IGNORE; } int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { int loglevel; parse_args(pamh, flags, argc, argv, &loglevel); log_info(pamh, "session(open)", flags, loglevel); return PAM_IGNORE; } int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { int loglevel; parse_args(pamh, flags, argc, argv, &loglevel); log_info(pamh, "session(close)", flags, loglevel); return PAM_IGNORE; } account-utils-1.3.0/src/pam_unix_ng-acct.c000066400000000000000000000103701521474342200204610ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include #include #include #include #include "basics.h" #include "pam_unix_ng.h" #include "pwaccess.h" #include "verify.h" static int acct_mgmt(pam_handle_t *pamh, struct config_t *cfg) { const void *void_str; const char *user; _cleanup_free_ char *error = NULL; long daysleft = -1; int r; r = pam_get_item(pamh, PAM_USER, &void_str); if (r != PAM_SUCCESS || isempty(void_str)) { pam_syslog(pamh, LOG_ERR, "Unknown user"); return PAM_USER_UNKNOWN; } user = void_str; r = pwaccess_check_expired(user, &daysleft, NULL /* pwchangeable */, &error); if (r < 0) { if (r == -ENODATA) return PAM_USER_UNKNOWN; if (PWACCESS_IS_NOT_RUNNING(r)) { struct spwd spbuf; struct spwd *sp = NULL; _cleanup_free_ char *buf = NULL; long bufsize = 0; pam_syslog(pamh, LOG_NOTICE, "pwaccess expired failed: %s", error ? error : strerror(-r)); // Try to start service at our own r = start_pwaccessd(pamh, cfg->ctrl); if (r == 0) { error = mfree(error); // reset error string r = pwaccess_check_expired(user, &daysleft, NULL /* pwchangeable */, &error); if (r == -ENODATA) return PAM_USER_UNKNOWN; if (r < 0) pam_syslog(pamh, LOG_ERR, "Second try pwaccess expired failed: %s", error ? error : strerror(-r)); } if (r < 0) { if (!(cfg->ctrl & ARG_QUIET)) pam_syslog(pamh, LOG_NOTICE, "pwaccessd not running, using internal fallback code"); r = alloc_getxxnam_buffer(pamh, &buf, &bufsize); if (r != PAM_SUCCESS) return r; r = getspnam_r(user, &spbuf, buf, bufsize, &sp); if (sp == NULL) { if (r != 0) { pam_syslog(pamh, LOG_WARNING, "getspnam_r(): %s", strerror(r)); pam_error(pamh, "getspnam_r(): %s", strerror(r)); return PAM_SYSTEM_ERR; } else r = PWA_EXPIRED_NO; } else r = expired_check(sp, &daysleft, NULL /* pwchangeable */); } } else { pam_syslog(pamh, LOG_ERR, "pwaccess expired failed: %s", error ? error : strerror(-r)); return PAM_SYSTEM_ERR; } } int retval = PAM_SUCCESS; switch ((pwa_expire_flag_t)r) { case PWA_EXPIRED_NO: break; case PWA_EXPIRED_ACCT: pam_syslog(pamh, LOG_NOTICE, "account %s has expired (account expired)", user); pam_error(pamh, "Your account has expired; please contact your system administrator."); retval = PAM_ACCT_EXPIRED; break; case PWA_EXPIRED_CHANGE_PW: if (daysleft == 0) { pam_syslog(pamh, LOG_NOTICE, "expired password for user %s (admin enforced)", user); pam_error(pamh, "You are required to change your password immediately (administrator enforced)."); } else { pam_syslog(pamh, LOG_NOTICE, "expired password for user %s (password aged)", user); pam_error(pamh, "You are required to change your password immediately (password expired)."); } retval = PAM_NEW_AUTHTOK_REQD; break; case PWA_EXPIRED_PW: pam_syslog(pamh, LOG_NOTICE, "password for user %s is inactive", user); pam_error(pamh, "Your password is inactive; please contact your system administrator."); retval = PAM_AUTHTOK_EXPIRED; break; default: pam_syslog(pamh, LOG_ERR, "Unexpected expire value: %i", r); retval = PAM_SYSTEM_ERR; break; } if (daysleft >= 0) { pam_syslog(pamh, LOG_INFO, "password for user %s will expire in %ld days", user, daysleft); if (!(cfg->ctrl & ARG_QUIET)) pam_info(pamh, "Warning: your password will expire in %ld %s.", daysleft, (daysleft == 1)?"day":"days"); } return retval; } int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct timespec start, stop; struct config_t cfg; int r; r = parse_args(pamh, flags, argc, argv, &cfg, false); if (r < 0) return errno_to_pam(r); if (cfg.ctrl & ARG_DEBUG) { clock_gettime(CLOCK_MONOTONIC, &start); pam_syslog(pamh, LOG_DEBUG, "acct_mgmt called"); } r = acct_mgmt(pamh, &cfg); if (cfg.ctrl & ARG_DEBUG) { clock_gettime(CLOCK_MONOTONIC, &stop); log_runtime_ms(pamh, "acct_mgmt", r, start, stop); } return r; } account-utils-1.3.0/src/pam_unix_ng-auth.c000066400000000000000000000100541521474342200205070ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include #include #include #include #include "basics.h" #include "pam_unix_ng.h" #include "pwaccess.h" #include "verify.h" static bool password_is_blank(const char *user, struct config_t *cfg) { bool nullok = (cfg->ctrl & ARG_NULLOK) && !(cfg->ctrl & ARG_NONULL); bool authenticated = false; int r; /* Never allow empty password if PAM_DISALLOW_NULL_AUTHTOK is set */ if (cfg->ctrl & ARG_NONULL) return false; /* Ask always for a password if empty passwords are forbidden */ if (!nullok) return false; /* if something fails, return false and user has to enter an empty password as worst. */ #if 0 /* XXX this needs a new option like the "nullresetok" from pam_unix.so. */ r = pwaccess_check_expired(user, NULL, NULL, NULL); if (r < 0) return false; else if (r > 0 && r != PWA_EXPIRED_CHANGE_PW) return false; else if (r == PWA_EXPIRED_CHANGE_PW) nullok = true; #endif r = pwaccess_verify_password(user, "", nullok, &authenticated, NULL); if (r != PAM_SUCCESS) return false; if (!authenticated) return false; return true; } static int authenticate(pam_handle_t *pamh, struct config_t *cfg) { bool authenticated = false; _cleanup_free_ char *error = NULL; const char *user = NULL; const char *password = NULL; int r; /* Get login name */ r = pam_get_user(pamh, &user, NULL /* prompt=xxx */); if (r != PAM_SUCCESS) { if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "pam_get_user failed: return %d", r); return (r == PAM_CONV_AGAIN ? PAM_INCOMPLETE:r); } /* can this happen? */ if (isempty(user)) return PAM_USER_UNKNOWN; else if (!valid_name(user)) { pam_syslog(pamh, LOG_ERR, "username contains invalid characters"); return PAM_USER_UNKNOWN; } else if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "username [%s]", user); /* Don't prompt for a password if it is empty */ if (!password_is_blank(user, cfg)) { /* get the users password */ r = pam_get_authtok(pamh, PAM_AUTHTOK, &password, NULL /* prompt */); if (r != PAM_SUCCESS) { if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "pam_get_authtok failed: return %d", r); if (r != PAM_CONV_AGAIN) pam_syslog(pamh, LOG_CRIT, "Could not get password for [%s]", user); return (r == PAM_CONV_AGAIN ? PAM_INCOMPLETE:r); } } if (cfg->fail_delay != 0) { /* convert milliseconds to microseconds */ r = pam_fail_delay(pamh, cfg->fail_delay*1000); if (r != PAM_SUCCESS) { if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "pam_fail_delay failed: return %d", r); pam_syslog(pamh, LOG_CRIT, "Could not set fail delay"); return r; } } r = authenticate_user(pamh, cfg->ctrl, user, strempty(password), &authenticated, &error); if (error && (cfg->ctrl & ARG_DEBUG)) pam_syslog(pamh, LOG_DEBUG, "authenticate_user(%s) failed: %s", user, error); if (r != PAM_SUCCESS) return r; if (authenticated) return PAM_SUCCESS; else log_authentication_failure(pamh, user); return PAM_AUTH_ERR; } int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct timespec start, stop; struct config_t cfg; int r; r = parse_args(pamh, flags, argc, argv, &cfg, false); if (r < 0) return errno_to_pam(r); if (cfg.ctrl & ARG_DEBUG) { clock_gettime(CLOCK_MONOTONIC, &start); pam_syslog(pamh, LOG_DEBUG, "authenticate called"); } r = authenticate(pamh, &cfg); if (cfg.ctrl & ARG_DEBUG) { clock_gettime(CLOCK_MONOTONIC, &stop); log_runtime_ms(pamh, "authenticate", r, start, stop); } return r; } int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct config_t cfg; int r; r = parse_args(pamh, flags, argc, argv, &cfg, false); if (r < 0) return errno_to_pam(r); if (cfg.ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "setcred called"); return PAM_SUCCESS; } account-utils-1.3.0/src/pam_unix_ng-common.c000066400000000000000000000221501521474342200210360ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include #include #include #include "basics.h" #include "pwaccess.h" #include "pam_unix_ng.h" #include "verify.h" #include "no_new_privs.h" #include "get_logindefs.h" int parse_args(pam_handle_t *pamh, int flags, int argc, const char **argv, struct config_t *cfg, bool init_crypt) { const char *cp; /* clear all variables */ memset(cfg, 0, sizeof(struct config_t)); /* defaults */ cfg->fail_delay = 2000; cfg->minlen = 8; /* only read login.defs if we change the password */ if (init_crypt) { _cleanup_free_ char *val; cfg->crypt_count = 0; val = get_logindefs_string("ENCRYPT_METHOD", NULL); if (isempty(val) || streq(val, "YESCRYPT")) cfg->crypt_prefix = "$y$"; else if (streq(val, "GHOST_YESCRYPT")) cfg->crypt_prefix = "$gy$"; else if (streq(val, "SHA512")) { cfg->crypt_count = get_logindefs_num("SHA_CRYPT_MAX_ROUNDS", 5000); cfg->crypt_prefix = "$6$"; } else if (streq(val, "SHA256")) { cfg->crypt_count = get_logindefs_num("SHA_CRYPT_MAX_ROUNDS", 5000); cfg->crypt_prefix = "$5$"; } else if (streq(val, "MD5")) { // cfg->crypt_prefix = "$1$"; pam_info(pamh, "MD5-based algorithms for password encryption are no longer supported!"); pam_syslog(pamh, LOG_NOTICE, "ENCRYPT_METHOD from login.defs has no longer supported MD5 value"); } else if (streq(val, "BLOWFISH") || streq(val, "BCRYPT")) cfg->crypt_prefix = "$2b$"; else { pam_syslog(pamh, LOG_NOTICE, "ENCRYPT_METHOD from login.defs has unknown value: '%s'", val); cfg->crypt_prefix = "$y$"; /* the default if no option is set */ } } /* does the application require quiet? */ if (flags & PAM_SILENT) cfg->ctrl |= ARG_QUIET; if (flags & PAM_DISALLOW_NULL_AUTHTOK) cfg->ctrl |= ARG_NONULL; /* step through arguments */ for (; argc-- > 0; ++argv) { if (streq(*argv, "debug")) cfg->ctrl |= ARG_DEBUG; else if (streq(*argv, "quiet")) cfg->ctrl |= ARG_QUIET; else if (streq(*argv, "nullok")) cfg->ctrl |= ARG_NULLOK; else if ((cp = startswith(*argv, "minlen=")) != NULL) { char *ep; long l; errno = 0; l = strtol(cp, &ep, 10); if (errno == ERANGE || l < 0 || l > INT32_MAX || cp == ep || *ep != '\0') pam_syslog(pamh, LOG_ERR, "Cannot parse 'minlen=%s'", cp); else cfg->minlen = l; } else if ((cp = startswith(*argv, "crypt_prefix=")) != NULL) cfg->crypt_prefix = cp; else if ((cp = startswith(*argv, "crypt_count=")) != NULL) { char *ep; long long ll; errno = 0; ll = strtoll(cp, &ep, 10); if (errno == ERANGE || ll < 0 || ll > UINT32_MAX || cp == ep || *ep != '\0') pam_syslog(pamh, LOG_ERR, "Cannot parse 'crypt_count=%s'", cp); else cfg->crypt_count = ll; } else if ((cp = startswith(*argv, "fail_delay=")) != NULL) { char *ep; long l; errno = 0; l = strtol(cp, &ep, 10); if (errno == ERANGE || l < 0 || l > UINT32_MAX || cp == ep || *ep != '\0') pam_syslog(pamh, LOG_ERR, "Cannot parse 'fail_delay=%s'", cp); else cfg->fail_delay = l; } /* this options are handled by pam_get_authtok() */ else if (!streq(*argv, "try_first_pass") && !streq(*argv, "use_first_pass") && !streq(*argv, "use_authtok") && startswith(*argv, "authtok_type=") == NULL) pam_syslog(pamh, LOG_ERR, "Unknown option: %s", *argv); } if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "Flags set by application:%s%s%s%s%s%s", flags & PAM_SILENT?" PAM_SILENT":"", flags & PAM_DISALLOW_NULL_AUTHTOK?" PAM_DISALLOW_NULL_AUTHTOK":"", flags & PAM_ESTABLISH_CRED?" PAM_ESTABLISH_CRED":"", flags & PAM_DELETE_CRED?" PAM_DELETE_CRED":"", flags & PAM_REINITIALIZE_CRED?" PAM_REINITIALIZE_CRED":"", flags & PAM_REFRESH_CRED?" PAM_REFRESH_CRED":""); return 0; } int alloc_getxxnam_buffer(pam_handle_t *pamh, char **buf, long *size) { long bufsize; bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); if (bufsize == -1) /* Value was indeterminate */ bufsize = 1024; /* sysconf() returns 1024 */ *buf = malloc(bufsize); if (*buf == NULL) { pam_syslog(pamh, LOG_CRIT, "Out of memory!"); return PAM_BUF_ERR; } *size = bufsize; return PAM_SUCCESS; } int authenticate_user(pam_handle_t *pamh, uint32_t ctrl, const char *user, const char *password, bool *ret_authenticated, char **error) { /* NONULL has preference over NULLOK */ bool nullok = (ctrl & ARG_NULLOK) && !(ctrl & ARG_NONULL); int r; r = pwaccess_verify_password(user, password, nullok, ret_authenticated, error); if (r == 0) return PAM_SUCCESS; if (r == -ENODATA) return PAM_USER_UNKNOWN; if (PWACCESS_IS_NOT_RUNNING(r)) { pam_syslog(pamh, LOG_NOTICE, "pwaccess verify failed: %s", *error ? *error : strerror(-r)); // Try to start service at our own r = start_pwaccessd(pamh, ctrl); if (r == 0) { *error = mfree(*error); // reset error string r = pwaccess_verify_password(user, password, nullok, ret_authenticated, error); if (r == 0) return PAM_SUCCESS; } if (r == -ENODATA) return PAM_USER_UNKNOWN; pam_syslog(pamh, LOG_ERR, "Second try pwaccess verify failed: %s", *error ? *error : strerror(-r)); } else pam_syslog(pamh, LOG_ERR, "pwaccess verify failed: %s", *error ? *error : strerror(-r)); // Try internal fallback and read /etc/shadow directly struct passwd pwdbuf; struct passwd *pw = NULL; struct spwd spbuf; struct spwd *sp = NULL; _cleanup_free_ char *buf = NULL; _cleanup_free_ char *hash = NULL; long bufsize; if (!(ctrl & ARG_QUIET)) pam_syslog(pamh, LOG_NOTICE, "pwaccessd not running, using internal fallback code"); r = alloc_getxxnam_buffer(pamh, &buf, &bufsize); if (r != PAM_SUCCESS) return r; r = getpwnam_r(user, &pwdbuf, buf, bufsize, &pw); if (pw == NULL) { if (r == 0) { if (valid_name(user)) pam_error(pamh, "User '%s' not found", user); else pam_error(pamh, "User not found (contains invalid characters)"); return PAM_USER_UNKNOWN; } pam_syslog(pamh, LOG_WARNING, "getpwnam_r(): %s", strerror(r)); pam_error(pamh, "getpwnam_r(): %s", strerror(r)); return PAM_SYSTEM_ERR; } hash = strdup(strempty(pw->pw_passwd)); if (hash == NULL) { pam_syslog(pamh, LOG_CRIT, "Out of memory!"); pam_error(pamh, "Out of memory!"); return PAM_BUF_ERR; } if (is_shadow(pw)) /* Get shadow entry */ { /* reuse buffer, !!! pw is no longer valid !!! */ pw = NULL; r = getspnam_r(user, &spbuf, buf, bufsize, &sp); if (sp == NULL) { if (r != 0) /* r == 0 means there is no shadow entry for this account, so pw->pw_passwd is incorrectly set. Ignore, crypt() will fail. */ { pam_syslog(pamh, LOG_WARNING, "getspnam_r(): %s", strerror(r)); pam_error(pamh, "getspnam_r(): %s", strerror(r)); return PAM_SYSTEM_ERR; } } else { hash = mfree(hash); hash = strdup(strempty(sp->sp_pwdp)); if (hash == NULL) { pam_syslog(pamh, LOG_CRIT, "Out of memory!"); pam_error(pamh, "Out of memory!"); return PAM_BUF_ERR; } } } r = verify_password(hash, password, nullok); if (r == VERIFY_OK) { *ret_authenticated = true; return PAM_SUCCESS; } else if (r != VERIFY_FAILED) { switch(r) { case VERIFY_CRYPT_DISABLED: pam_syslog(pamh, LOG_ERR, "crypt algo of hash is disabled"); pam_error(pamh, "Crypt alogrithm of password hash is disabled"); break; case VERIFY_CRYPT_INVALID: pam_syslog(pamh, LOG_ERR, "crypt algo of hash is not supported"); pam_error(pamh, "Crypt alogrithm of hash is not supported"); break; default: pam_syslog(pamh, LOG_ERR, "Unknown verify_password() error: %i", r); pam_error(pamh, "Unknown verify_password() error: %i", r); break; } return PAM_SYSTEM_ERR; } return PAM_SYSTEM_ERR; } void log_authentication_failure(pam_handle_t *pamh, const char *user) { const void *ruser = NULL; const void *rhost = NULL; const void *tty = NULL; const char *login_name; pam_get_item(pamh, PAM_RUSER, &ruser); pam_get_item(pamh, PAM_RHOST, &rhost); pam_get_item(pamh, PAM_TTY, &tty); login_name = pam_modutil_getlogin(pamh); pam_syslog(pamh, LOG_NOTICE, "authentication failure; " "logname=%s uid=%u euid=%u " "tty=%s ruser=%s rhost=%s " "user=%s%s", strna(login_name), getuid(), geteuid(), strna(tty), strna(ruser), strna(rhost), user, no_new_privs_enabled()?", no_new_privs=1":""); } void log_runtime_ms(pam_handle_t *pamh, const char *type, int retval, struct timespec start, struct timespec stop) { uint64_t delta_ms = timespec_diff_ms(start, stop); pam_syslog(pamh, LOG_DEBUG, "%s finished (%s), executed in %lu milliseconds", type, pam_strerror(pamh, retval), delta_ms); } int errno_to_pam(int e) { if (e < 0) e = -e; switch(e) { case ENOMEM: return PAM_BUF_ERR; default: break; } return PAM_SERVICE_ERR; } account-utils-1.3.0/src/pam_unix_ng-passwd.c000066400000000000000000000311101521474342200210430ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include #include #include #include #include #include "basics.h" #include "pam_unix_ng.h" #include "pwaccess.h" #include "verify.h" #include "files.h" #define MAX_PASSWD_TRIES 3 static inline void secure_freep(char **p) { if (*p) explicit_bzero(*p, strlen(*p)); *p = mfree(*p); } static int get_local_user_record(pam_handle_t *pamh, const char *user, struct passwd **ret_pw, struct spwd **ret_sp) { _cleanup_fclose_ FILE *fp = NULL; struct passwd pw; struct spwd sp; struct passwd *pw_ptr = NULL; struct spwd *sp_ptr = NULL; _cleanup_free_ char *pwbuf; long pwbufsize = 0; _cleanup_free_ char *spbuf; long spbufsize = 0; int r; assert(user); assert(ret_pw); assert(ret_sp); *ret_pw = NULL; *ret_sp = NULL; /* Get passwd entry */ if ((fp = fopen("/etc/passwd", "r")) == NULL) return -errno; r = alloc_getxxnam_buffer(pamh, &pwbuf, &pwbufsize); if (r != PAM_SUCCESS) return r; /* Loop over all passwd entries */ r = 0; while (r == 0) { r = fgetpwent_r(fp, &pw, pwbuf, pwbufsize, &pw_ptr); if (pw_ptr != NULL) { if(streq(pw_ptr->pw_name, user)) break; } } if (r != 0) return -errno; r = fclose(fp); fp = NULL; if (r < 0) return -errno; /* Get shadow entry */ if ((fp = fopen("/etc/shadow", "r")) == NULL) return -errno; r = alloc_getxxnam_buffer(pamh, &spbuf, &spbufsize); if (r != PAM_SUCCESS) return r; /* Loop over all shadow entries */ r = 0; while (r == 0) { r = fgetspent_r(fp, &sp, spbuf, spbufsize, &sp_ptr); if (sp_ptr != NULL) { if (streq(sp_ptr->sp_namp, user)) break; } } if (r != 0) return -errno; r = fclose(fp); fp = NULL; if (r < 0) return -errno; /* ret_pw != NULL -> pw contains valid entry, duplicate that */ if (pw_ptr) { _cleanup_(struct_passwd_freep) struct passwd *tmp = NULL; tmp = calloc(1, sizeof(struct passwd)); if (tmp == NULL) return -ENOMEM; tmp->pw_name = strdup(pw.pw_name); tmp->pw_passwd = strdup(strempty(pw.pw_passwd)); tmp->pw_uid = pw.pw_uid; tmp->pw_gid = pw.pw_gid; tmp->pw_gecos = strdup(strempty(pw.pw_gecos)); tmp->pw_dir = strdup(strempty(pw.pw_dir)); tmp->pw_shell = strdup(strempty(pw.pw_shell)); /* if any of the string pointer is NULL, strdup failed */ if (!tmp->pw_name || !tmp->pw_passwd || !tmp->pw_gecos || !tmp->pw_dir || !tmp->pw_shell) return -ENOMEM; *ret_pw = TAKE_PTR(tmp); } if (sp_ptr) { _cleanup_(struct_shadow_freep) struct spwd *tmp; tmp = calloc(1, sizeof(struct spwd)); if (tmp == NULL) return -ENOMEM; tmp->sp_namp = strdup(sp.sp_namp); tmp->sp_pwdp = strdup(strempty(sp.sp_pwdp)); tmp->sp_lstchg = sp.sp_lstchg; tmp->sp_min = sp.sp_min; tmp->sp_max = sp.sp_max; tmp->sp_warn = sp.sp_warn; tmp->sp_inact = sp.sp_inact; tmp->sp_expire = sp.sp_expire; tmp->sp_flag = sp.sp_flag; if (!tmp->sp_namp || !tmp->sp_pwdp) return -ENOMEM; *ret_sp = TAKE_PTR(tmp); } return 0; } static bool i_am_root_detect(int flags) { bool root = false; /* The test for PAM_CHANGE_EXPIRED_AUTHTOK is here, because login runs as root and we need the old password in this case. */ root = (getuid() == 0 && !(flags & PAM_CHANGE_EXPIRED_AUTHTOK)); return root; } static int unix_chauthtok_prelim_check(pam_handle_t *pamh, int flags, struct config_t *cfg, const char *user, struct passwd *pw, struct spwd *sp, bool i_am_root) { const char *pass_old = NULL; bool authenticated = false; bool pwchangeable = true; _cleanup_free_ char *error = NULL; int r; /* instruct user what is happening */ if (!(cfg->ctrl & ARG_QUIET)) { r = pam_info(pamh, "Changing password for %s.", user); if (r != PAM_SUCCESS) return r; } /* If this is being run by root and we change a local password, we don't need to get the old password. */ if (i_am_root) { if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "process run by root, do nothing"); return PAM_SUCCESS; } /* don't ask for the old password if it is empty */ if (is_blank_password(pw, sp)) { if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "Old password is empty, skip"); return PAM_SUCCESS; } r = expired_check(sp, NULL, &pwchangeable); if (!pwchangeable && !i_am_root) { pam_error(pamh, "You must wait longer to change your password."); return PAM_AUTHTOK_ERR; } if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "expired_check=%i", r); if (r == PWA_EXPIRED_NO && (flags & PAM_CHANGE_EXPIRED_AUTHTOK)) { pam_error(pamh, "Password not expired"); return PAM_AUTHTOK_ERR; } r = pam_get_authtok(pamh, PAM_OLDAUTHTOK, &pass_old, NULL); if (r != PAM_SUCCESS) { pam_syslog(pamh, LOG_NOTICE, "password - old token not obtained"); return r; } r = authenticate_user(pamh, cfg->ctrl, user, pass_old, &authenticated, &error); pass_old = NULL; if (r != PAM_SUCCESS || !authenticated) { if (error) pam_syslog(pamh, LOG_ERR, "authentication error: %s", error); log_authentication_failure(pamh, user); if (r != PAM_SUCCESS) return r; return PAM_AUTH_ERR; } return PAM_SUCCESS; } static int unix_chauthtok_update_authtok(pam_handle_t *pamh, struct config_t *cfg, const char *user, struct passwd *pw, struct spwd *sp, bool i_am_root) { const char *pass_old = NULL; const char *pass_new = NULL; const void *item; int retry = 0; int r; /* Get the old password again. */ r = pam_get_item(pamh, PAM_OLDAUTHTOK, &item); if (r != PAM_SUCCESS) { pam_syslog(pamh, LOG_NOTICE, "User %s not authenticated: %s", user, pam_strerror(pamh, r)); return r; } pass_old = item; r = PAM_AUTHTOK_ERR; while ((r != PAM_SUCCESS) && (retry++ < MAX_PASSWD_TRIES)) { const char *no_new_pass_msg = "No new password has been supplied"; /* use_authtok is to force the use of a previously entered password -- needed for pluggable password strength checking */ r = pam_get_authtok(pamh, PAM_AUTHTOK, &pass_new, NULL); if (r == PAM_TRY_AGAIN) /* New authentication tokens mismatch. */ continue; if (r != PAM_SUCCESS) { if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "%s - %s", no_new_pass_msg, pam_strerror(pamh, r)); pass_old = NULL; return r; } if (isempty(pass_new) || (pass_old && streq(pass_new, pass_old))) { /* remove new password */ pam_set_item(pamh, PAM_AUTHTOK, NULL); pass_new = NULL; if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "%s", no_new_pass_msg); pam_error(pamh, "%s.", no_new_pass_msg); r = PAM_AUTHTOK_ERR; } else if (strlen(strempty(pass_new)) > PAM_MAX_RESP_SIZE) { /* remove new password */ pam_set_item(pamh, PAM_AUTHTOK, NULL); pass_new = NULL; pam_syslog(pamh, LOG_NOTICE, "supplied password to long"); pam_error(pamh, "You must choose a shorter password."); r = PAM_AUTHTOK_ERR; } else if (strlen(strempty(pass_new)) < (size_t)cfg->minlen) { pam_syslog(pamh, LOG_NOTICE, "supplied password for %s too short", user); if (!i_am_root) { /* remove new password */ pam_set_item(pamh, PAM_AUTHTOK, NULL); pass_new = NULL; pam_error(pamh, "You must choose a longer password."); r = PAM_AUTHTOK_ERR; } } } if (r != PAM_SUCCESS) { pam_syslog(pamh, LOG_NOTICE, "new password not acceptable"); pass_new = pass_old = NULL; /* cleanup */ return r; } /* We have an approved password, create new hash and change the database */ if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "Create hash with prefix=%s, count=%lu", cfg->crypt_prefix, cfg->crypt_count); _cleanup_free_ char *error = NULL; _cleanup_(secure_freep) char *new_hash = NULL; r = create_hash(pass_new, cfg->crypt_prefix, cfg->crypt_count, &new_hash, &error); if (r < 0 || new_hash == NULL) { if (r == -ENOMEM) { pam_syslog(pamh, LOG_CRIT, "Out of memory"); return PAM_BUF_ERR; } else { if (error) pam_syslog(pamh, LOG_ERR, "crypt() failure: %s", error); else pam_syslog(pamh, LOG_ERR, "crypt() failure for new password"); } pass_new = pass_old = NULL; /* cleanup */ return PAM_SYSTEM_ERR; } if (sp && (is_shadow(pw) || isempty(pw->pw_passwd))) { /* we use _cleanup_ for this struct */ free(sp->sp_pwdp); sp->sp_pwdp = strdup(new_hash); if (sp->sp_pwdp == NULL) return PAM_BUF_ERR; sp->sp_lstchg = time(NULL) / (60 * 60 * 24); if (sp->sp_lstchg == 0) sp->sp_lstchg = -1; /* Don't request passwort change only because time isn't set yet. */ r = update_shadow(sp, NULL); if (r == 0 && !streq("x", strempty(pw->pw_passwd))) { if (pw->pw_passwd) free(pw->pw_passwd); pw->pw_passwd = strdup("x"); r = update_passwd(pw, NULL); } } else { /* we use _cleanup_ for this struct */ free(pw->pw_passwd); pw->pw_passwd = strdup(new_hash); if (pw->pw_passwd == NULL) return PAM_BUF_ERR; r = update_passwd(pw, NULL); } pass_old = pass_new = NULL; if (r < 0) return PAM_AUTHTOK_ERR; else return PAM_SUCCESS; } static int unix_chauthtok(pam_handle_t *pamh, int flags, struct config_t *cfg) { _cleanup_(struct_passwd_freep) struct passwd *pw = NULL; _cleanup_(struct_shadow_freep) struct spwd *sp = NULL; bool i_am_root = i_am_root_detect(flags); const char *only_expired_authtok = ""; const char *run_as_root = ""; const char *user = NULL; int r; if (i_am_root) run_as_root = ", root"; /* Validate flags */ if (flags & PAM_CHANGE_EXPIRED_AUTHTOK) only_expired_authtok = ", only expired authtok"; if (flags & PAM_PRELIM_CHECK) { if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "chauthtok called (prelim check%s%s)", only_expired_authtok, run_as_root); } else if (flags & PAM_UPDATE_AUTHTOK) { if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "chauthtok called (update authtok%s%s)", only_expired_authtok, run_as_root); } else { pam_syslog(pamh, LOG_ERR, "chauthtok called without flag!"); return PAM_ABORT; } /* We must be root to update passwd and shadow. */ if (geteuid() != 0) { const char *no_root = "Calling process must be root!"; pam_syslog(pamh, LOG_ERR, "%s (euid=%u,uid=%u)", no_root, geteuid(), getuid()); pam_error(pamh, "%s", no_root); return PAM_CRED_INSUFFICIENT; } /* Get login name */ r = pam_get_user(pamh, &user, NULL /* prompt=xxx */); if (r != PAM_SUCCESS) { if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "pam_get_user failed: return %d", r); return (r == PAM_CONV_AGAIN ? PAM_INCOMPLETE:r); } if (isempty(user)) return PAM_USER_UNKNOWN; if (!valid_name(user)) { pam_syslog(pamh, LOG_ERR, "username contains invalid characters"); return PAM_USER_UNKNOWN; } else if (cfg->ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "username [%s]", user); r = get_local_user_record(pamh, user, &pw, &sp); if (r < 0) { if (r == -ENOENT) { pam_syslog(pamh, LOG_ERR, "%s is no local user", user); pam_error(pamh, "You can only change local passwords."); } else { pam_syslog(pamh, LOG_ERR, "getting local user records failed: %s", strerror(-r)); pam_error(pamh, "Error getting user records"); } return PAM_AUTHTOK_RECOVERY_ERR; } if (flags & PAM_PRELIM_CHECK) return unix_chauthtok_prelim_check(pamh, flags, cfg, user, pw, sp, i_am_root); else if (flags & PAM_UPDATE_AUTHTOK) return unix_chauthtok_update_authtok(pamh, cfg, user, pw, sp, i_am_root); else { pam_syslog(pamh, LOG_CRIT, "pam_sm_chauthtok received unknown request (flags=%i)", flags); return PAM_ABORT; } return PAM_SUCCESS; } int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct timespec start, stop; struct config_t cfg; int r; r = parse_args(pamh, flags, argc, argv, &cfg, true); if (r < 0) return errno_to_pam(r); if (cfg.ctrl & ARG_DEBUG) { clock_gettime(CLOCK_MONOTONIC, &start); pam_syslog(pamh, LOG_DEBUG, "chauthtok called"); } r = unix_chauthtok(pamh, flags, &cfg); if (cfg.ctrl & ARG_DEBUG) { clock_gettime(CLOCK_MONOTONIC, &stop); log_runtime_ms(pamh, "chauthtok", r, start, stop); } return r; } account-utils-1.3.0/src/pam_unix_ng-session.c000066400000000000000000000052161521474342200212350ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include #include #include #include #include "basics.h" #include "pam_unix_ng.h" #include "pwaccess.h" #include "verify.h" int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { char lognamebuf[LOGIN_NAME_MAX+1]; const char *logname = lognamebuf; struct passwd pwdbuf; struct passwd *pw = NULL; _cleanup_free_ char *pwbuf = NULL; long pwbufsize; const void *void_str; const char *user; struct config_t cfg; int r; r = parse_args(pamh, flags, argc, argv, &cfg, false); if (r < 0) return errno_to_pam(r); if (cfg.ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "open_session called"); /* don't do anything if we don't log it */ if (cfg.ctrl & ARG_QUIET) return PAM_SUCCESS; r = pam_get_item(pamh, PAM_USER, &void_str); if (r != PAM_SUCCESS || isempty(void_str)) { pam_syslog(pamh, LOG_ERR, "open_session - user is not known?"); return PAM_SESSION_ERR; } user = void_str; /* lognamebuf is bigger than max allowed username length */ if (getlogin_r(lognamebuf, sizeof(lognamebuf)) != 0) logname = strerror(errno); r = alloc_getxxnam_buffer(pamh, &pwbuf, &pwbufsize); if (r != PAM_SUCCESS) return r; r = getpwnam_r(user, &pwdbuf, pwbuf, pwbufsize, &pw); if (pw == NULL) { if (r == 0) { const char *cp; if (!valid_name(user)) cp = ""; else cp = user; pam_syslog(pamh, LOG_INFO, "User '%s' not found", strna(cp)); return PAM_USER_UNKNOWN; } pam_syslog(pamh, LOG_WARNING, "getpwnam_r(): %s", strerror(r)); pam_error(pamh, "getpwnam_r(): %s", strerror(r)); return PAM_SYSTEM_ERR; } pam_syslog(pamh, LOG_INFO, "session opened for user %s(uid=%lu) by %s(uid=%lu)", user, (long unsigned)pw->pw_uid, logname, (long unsigned)getuid()); return PAM_SUCCESS; } int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { const void *void_str; const char *user; struct config_t cfg; int r; r = parse_args(pamh, flags, argc, argv, &cfg, false); if (r < 0) return errno_to_pam(r); if (cfg.ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "close_session called"); /* don't do anything if we don't log it */ if (cfg.ctrl & ARG_QUIET) return PAM_SUCCESS; r = pam_get_item(pamh, PAM_USER, &void_str); if (r != PAM_SUCCESS || isempty(void_str)) { pam_syslog(pamh, LOG_ERR, "close_session - user is not known?"); return PAM_SESSION_ERR; } user = void_str; pam_syslog(pamh, LOG_INFO, "session closed for user %s", user); return PAM_SUCCESS; } account-utils-1.3.0/src/pam_unix_ng-spawn.c000066400000000000000000000074001521474342200206770ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #include "config.h" #include #include #include "basics.h" #include "pam_unix_ng.h" typedef struct { pam_handle_t *pamh; const char *unit; int finished; int success; } job_ctx_t; #define TIMEOUT_USEC 2 * 1000000 // 2 seconds // Callback triggered whenever systemd removes ANY job from its queue. static int on_job_removed_cb(sd_bus_message *m, void *userdata, sd_bus_error *ret_error _unused_) { job_ctx_t *ctx = userdata; uint32_t id; const char *path, *unit, *result; int r; // The JobRemoved signal signature is: uoss (id, object_path, unit_name, result) r = sd_bus_message_read(m, "uoss", &id, &path, &unit, &result); if (r < 0) return 0; // Ignore unparseable signals // Is this the unit we are waiting for? if (streq(unit, ctx->unit)) { ctx->finished = 1; if (streq(result, "done")) ctx->success = 1; else { pam_syslog(ctx->pamh, LOG_ERR, "Job for %s failed with result: %s", unit, result); ctx->success = 0; } } return 0; } int start_pwaccessd(pam_handle_t *pamh, uint32_t ctrl) { job_ctx_t ctx = { .pamh = pamh, .unit = "pwaccessd.socket", .finished = 0, .success = 0 }; sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; int r; if (ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "Trying to start pwaccessd.socket..."); r = sd_bus_default_system(&bus); if (r < 0) { pam_syslog(pamh, LOG_ERR, "Failed to connect to dbus: %s", strerror(-r)); return r; } r = sd_bus_match_signal(bus, &slot, // Slot object for easy cleanup "org.freedesktop.systemd1", // Sender "/org/freedesktop/systemd1", // Object path "org.freedesktop.systemd1.Manager", // Interface "JobRemoved", // Signal name on_job_removed_cb, &ctx); if (r < 0) { pam_syslog(pamh, LOG_ERR, "Failed to subscribe to 'JobRemoved' DBus signal: %s", strerror(-r)); return r; } r = sd_bus_call_method(bus, "org.freedesktop.systemd1", // Destination service "/org/freedesktop/systemd1", // Object path "org.freedesktop.systemd1.Manager", // Interface name "StartUnit", // Method name &error, // Error return object &reply, // Reply message "ss", // Input signature (two strings) "pwaccessd.socket", // Arg 1: Unit name "replace" // Arg 2: Job mode ); if (r < 0) { pam_syslog(pamh, LOG_ERR, "Failed to start pwaccessd.socket: %s", error.message); return r; } else if (ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "Start job queued. Waiting for %s.", ctx.unit); while (!ctx.finished) { // Process any pending bus messages (this will fire our callback if the signal arrived) r = sd_bus_process(bus, NULL); if (r < 0) { pam_syslog(pamh, LOG_ERR, "DBus processing failed: %s", strerror(-r)); return r; } else if (r > 0) // success, check if ctx.finished is 1 continue; r = sd_bus_wait(bus, TIMEOUT_USEC); if (r < 0) { pam_syslog(pamh, LOG_ERR, "DBus wait failed: %s", strerror(-r)); return r; } if (r == 0) { pam_syslog(pamh, LOG_ERR, "DBus timeout, starting pwaccessd failed"); return -ETIME; } } if (!ctx.success) return -ENOENT; if (ctrl & ARG_DEBUG) pam_syslog(pamh, LOG_DEBUG, "pwaccessd successfully started"); return 0; } account-utils-1.3.0/src/pam_unix_ng.h000066400000000000000000000030041521474342200175520ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-2-Clause #pragma once #include #include #include #include #include #define ARG_DEBUG 1 /* send info to syslog(3) */ #define ARG_QUIET 2 /* keep quiet about things */ #define ARG_NULLOK 4 /* allow blank passwords */ #define ARG_NONULL 8 /* don't allow blank passwords */ struct config_t { uint32_t ctrl; uint32_t fail_delay; /* sleep of milliseconds in case of auth failure */ int minlen; /* minimal length of new password */ const char *crypt_prefix; /* see man crypt(5) */ unsigned long crypt_count; /* see man crypt(5) */ }; extern int parse_args(pam_handle_t *pamh, int flags, int argc, const char **argv, struct config_t *cfg, bool init_crypt); extern int alloc_getxxnam_buffer(pam_handle_t *pamh, char **buf, long *size); extern int authenticate_user(pam_handle_t *pamh, uint32_t ctrl, const char *user, const char *password, bool *ret_authenticated, char **error); extern int start_pwaccessd(pam_handle_t *pamh, uint32_t ctrl); extern int errno_to_pam(int e); extern void log_authentication_failure(pam_handle_t *pamh, const char *user); extern void log_runtime_ms(pam_handle_t *pamh, const char *type, int retval, struct timespec start, struct timespec stop); static inline uint64_t timespec_diff_ms(struct timespec start, struct timespec stop) { return ((stop.tv_sec - start.tv_sec) * 1000000000 + (stop.tv_nsec - start.tv_nsec)) / 1000 / 1000; } account-utils-1.3.0/src/pam_unix_ng.map000066400000000000000000000002471521474342200201060ustar00rootroot00000000000000{ global: pam_sm_acct_mgmt; pam_sm_authenticate; pam_sm_chauthtok; pam_sm_close_session; pam_sm_open_session; pam_sm_setcred; local: *; }; account-utils-1.3.0/src/passwd.c000066400000000000000000000426571521474342200165630ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include #include #include "basics.h" #include "pwaccess.h" #include "varlink-client-common.h" #include "verify.h" #include "chauthtok.h" #include "get_logindefs.h" #include "drop_privs.h" #define ARG_DELETE_PASSWORD 1 #define ARG_EXPIRE 2 #define ARG_LOCK_PASSWORD 4 #define ARG_UNLOCK_PASSWORD 8 #define ARG_STATUS_ACCOUNT 16 #define ARG_PASSWORD_STDIN 32 static int oom(void) { fprintf(stderr, "Out of memory!\n"); return ENOMEM; } static void print_usage(FILE *stream) { fprintf(stream, "Usage: passwd [options] [user]\n"); } static void print_help(void) { fprintf(stdout, "passwd - change user password\n\n"); print_usage(stdout); fputs(" -d, --delete Delete password\n", stdout); fputs(" -e, --expire Immediately expire password\n", stdout); fputs(" -h, --help Give this help list\n", stdout); fputs(" -I, --inactive Lock expired account after inactive days\n", stdout); fputs(" -k, --keep-tokens Change only expired passwords\n", stdout); fputs(" -l, --lock Lock password\n", stdout); fputs(" -m, --mindays Minimum # of days before password can be changed\n", stdout); fputs(" -M, --maxdays Maximum # of days before password can be canged\n", stdout); fputs(" -q, --quiet Be silent\n", stdout); fputs(" -s, --stdin Read new password from stdin\n", stdout); fputs(" -S, --status Display account status\n", stdout); fputs(" -u, --unlock Unlock password\n", stdout); fputs(" -v, --version Print program version\n", stdout); fputs(" -w, --warndays # days of warning before password expires\n", stdout); } static void print_error(void) { fprintf(stderr, "Try `passwd --help' for more information.\n"); } #define DAY (24L*3600L) #define SCALE DAY static inline char * date2str(time_t date) { static char buf[12]; struct tm tm; if (date < 0) strlcpy(buf, "never", sizeof(buf)); else if (!gmtime_r(&date, &tm)) strlcpy(buf, "future", sizeof(buf)); else strftime(buf, sizeof(buf), "%Y-%m-%d", &tm); return buf; } static const char * pw_status(const char *pass) { if (startswith(pass, "*") || startswith(pass, "!")) return "L"; if (isempty(pass)) return "NP"; return "P"; } static int print_account_status(const struct passwd *pw, const struct spwd *sp) { if (sp) printf("%s %s %s %ld %ld %ld %ld\n", pw->pw_name, pw_status(sp->sp_pwdp), date2str(sp->sp_lstchg * SCALE), sp->sp_min, sp->sp_max, sp->sp_warn, sp->sp_inact); else if (pw->pw_passwd) printf("%s %s\n", pw->pw_name, pw_status(pw->pw_passwd)); else { fprintf(stderr, "Malformed password data obtained for user '%s'.\n", pw->pw_name); return EINVAL; } return 0; } static int modify_account(struct passwd *pw, struct spwd *sp, int args, const char *inactive, const char *mindays, const char *maxdays, const char *warndays, bool quiet) { _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *passwd = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *shadow = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL; _cleanup_free_ char *error = NULL; _cleanup_(struct_result_free) struct result p = { .success = false, .error = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "Success", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct result, success), 0 }, { "ErrorMsg", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct result, error), 0 }, {} }; int has_change = 0; int r; if (args & ARG_DELETE_PASSWORD) { if (pw->pw_passwd) pw->pw_passwd = mfree(pw->pw_passwd); pw->pw_passwd = strdup(""); if (pw->pw_passwd == NULL) { fprintf(stderr, "Out of memory!\n"); return ENOMEM; } if (sp) { if (sp->sp_pwdp) sp->sp_pwdp = mfree(sp->sp_pwdp); sp->sp_pwdp = strdup(""); if (sp->sp_pwdp == NULL) { fprintf(stderr, "Out of memory!\n"); return ENOMEM; } } has_change = 1; } if ((args & ARG_EXPIRE) && sp) { sp->sp_lstchg = 0; has_change = 1; } if (args & ARG_LOCK_PASSWORD) { char *newpw = NULL; if (is_shadow(pw)) { if (asprintf(&newpw, "!%s", strempty(sp->sp_pwdp)) < 0) return ENOMEM; free(sp->sp_pwdp); sp->sp_pwdp = newpw; } else { if (asprintf(&newpw, "!%s", strempty(pw->pw_passwd)) < 0) return ENOMEM; free(pw->pw_passwd); pw->pw_passwd = newpw; } has_change = 1; } if (args & ARG_UNLOCK_PASSWORD) { char *newpw = NULL; if (is_shadow(pw) && startswith(sp->sp_pwdp, "!")) { newpw=strdup(&(sp->sp_pwdp)[1]); if (!newpw) return ENOMEM; free(sp->sp_pwdp); sp->sp_pwdp = newpw; has_change = 1; } else if (startswith(pw->pw_passwd, "!")) { newpw=strdup(&(pw->pw_passwd)[1]); if (!newpw) return ENOMEM; free(pw->pw_passwd); pw->pw_passwd = newpw; has_change = 1; } } if (inactive || mindays || maxdays || warndays) { char *ep; if (!sp) { sp = calloc(1, sizeof(struct spwd)); if (!sp) return oom(); sp->sp_namp = strdup(pw->pw_name); if (!sp->sp_namp) return oom(); sp->sp_pwdp = pw->pw_passwd; pw->pw_passwd = strdup("x"); if (!pw->pw_passwd) return oom(); sp->sp_lstchg = time(NULL) / DAY; /* disable instead of requesting password change */ if (!sp->sp_lstchg) sp->sp_lstchg = -1; sp->sp_min = get_logindefs_num("PASS_MIN_DAYS", -1); sp->sp_max = get_logindefs_num("PASS_MAX_DAYS", -1); sp->sp_warn = get_logindefs_num("PASS_WARN_AGE", -1); sp->sp_inact = -1; sp->sp_expire = -1; } if (inactive) { long l; errno = 0; l = strtol(inactive, &ep, 10); if (errno == ERANGE || l < -1 || inactive == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'inactive=%s'\n", inactive); return EINVAL; } sp->sp_inact = l; } if (mindays) { long l; errno = 0; l = strtol(mindays, &ep, 10); if (errno == ERANGE || l < -1 || mindays == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'mindays=%s'\n", mindays); return EINVAL; } sp->sp_min = l; } if (maxdays) { long l; errno = 0; l = strtol(maxdays, &ep, 10); if (errno == ERANGE || l < -1 || maxdays == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'maxdays=%s'\n", maxdays); return EINVAL; } sp->sp_max = l; } if (warndays) { long l; errno = 0; l = strtol(warndays, &ep, 10); if (errno == ERANGE || l < -1 || warndays == ep || *ep != '\0') { fprintf(stderr, "Cannot parse 'warndays=%s'\n", warndays); return EINVAL; } sp->sp_warn = l; } has_change = 1; } if (!has_change) { if (!quiet) printf("Nothing to change.\n"); return 0; } r = connect_to_pwupdd(&link, _VARLINK_PWUPD_SOCKET, &error); if (r < 0) { if (error) fprintf(stderr, "%s\n", error); else fprintf(stderr, "Cannot connect to pwupd! (%s)\n", strerror(-r)); return -r; } r = sd_json_variant_merge_objectbo(&passwd, SD_JSON_BUILD_PAIR_STRING("name", pw->pw_name), SD_JSON_BUILD_PAIR_STRING("passwd", pw->pw_passwd), SD_JSON_BUILD_PAIR_INTEGER("UID", pw->pw_uid), SD_JSON_BUILD_PAIR_INTEGER("GID", pw->pw_gid), SD_JSON_BUILD_PAIR_STRING("GECOS", pw->pw_gecos), SD_JSON_BUILD_PAIR_STRING("dir", pw->pw_dir), SD_JSON_BUILD_PAIR_STRING("shell", pw->pw_shell)); if (r < 0) { fprintf(stderr, "Error building passwd data: %s\n", strerror(-r)); return -r; } if (sp) { r = sd_json_variant_merge_objectbo(&shadow, SD_JSON_BUILD_PAIR_STRING("name", sp->sp_namp), SD_JSON_BUILD_PAIR_STRING("passwd", sp->sp_pwdp), SD_JSON_BUILD_PAIR_INTEGER("lstchg", sp->sp_lstchg), SD_JSON_BUILD_PAIR_INTEGER("min", sp->sp_min), SD_JSON_BUILD_PAIR_INTEGER("max", sp->sp_max), SD_JSON_BUILD_PAIR_INTEGER("warn", sp->sp_warn), SD_JSON_BUILD_PAIR_INTEGER("inact", sp->sp_inact), SD_JSON_BUILD_PAIR_INTEGER("expire", sp->sp_expire), SD_JSON_BUILD_PAIR_INTEGER("flag", sp->sp_flag)); if (r < 0) { fprintf(stderr, "Error building shadow data: %s\n", strerror(-r)); return -r; } } r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_VARIANT("passwd", passwd)); if (r >= 0 && shadow) r = sd_json_variant_merge_objectbo(¶ms, SD_JSON_BUILD_PAIR_VARIANT("shadow", shadow)); if (r < 0) { fprintf(stderr, "JSON merge result object failed: %s", strerror(-r)); return -r; } const char *error_id = NULL; r = sd_varlink_call(link, "org.openSUSE.pwupd.UpdatePasswdShadow", params, &result, &error_id); if (r < 0) { fprintf(stderr, "Failed to call UpdatePasswdShadow method: %s\n", strerror(-r)); return -r; } /* dispatch before checking error_id, we may need the result for the error message */ r = sd_json_dispatch(result, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) { fprintf(stderr, "Failed to parse JSON answer: %s\n", strerror(-r)); return -r; } if (error_id && strlen(error_id) > 0) { if (p.error) fprintf(stderr, "Error updating account information:\n%s\n", p.error); else fprintf(stderr, "Error updating account information:\n%s\n", error_id); return EIO; } if (!quiet) printf("Account information updated.\n"); return 0; } /* A conversation function which uses an internally-stored value for * the responses. */ static int stdin_conv (int num_msg, const struct pam_message **msg, struct pam_response **response, void *appdata_ptr) { struct pam_response *reply; const char *stdin_input = appdata_ptr; size_t count; *response = NULL; /* Sanity test. */ if (num_msg <= 0) return PAM_CONV_ERR; /* Allocate memory for the responses. */ reply = calloc (num_msg, sizeof (struct pam_response)); if (reply == NULL) return PAM_CONV_ERR; /* Each prompt elicits the same response. */ for (count = 0; count < (size_t)num_msg; count++) { switch (msg[count]->msg_style) { case PAM_PROMPT_ECHO_ON: /* unsupported, free memory and return error */ fprintf(stderr, "PAM_PROMPT_ECHO_ON unsupported together with --stdin\n"); for (size_t i = 0; i < count; i++) if (reply[i].resp) { explicit_bzero(reply[i].resp, strlen(reply[i].resp)); reply[i].resp = mfree(reply[i].resp); } reply = mfree(reply); return PAM_CONV_ERR; break; case PAM_PROMPT_ECHO_OFF: reply[count].resp_retcode = 0; reply[count].resp = strdup(stdin_input); break; case PAM_ERROR_MSG: fprintf(stderr, "%s\n", msg[count]->msg); break; case PAM_TEXT_INFO: fprintf(stdout, "%s\n", msg[count]->msg); break; default: fprintf(stderr, "Erroneous conversation (%d)\n", msg[count]->msg_style); for (size_t i = 0; i < count; i++) if (reply[i].resp) { explicit_bzero(reply[i].resp, strlen(reply[i].resp)); reply[i].resp = mfree(reply[i].resp); } reply = mfree(reply); return PAM_CONV_ERR; break; } } /* Set the pointers in the response structure and return. */ *response = reply; return PAM_SUCCESS; } int main(int argc, char **argv) { _cleanup_(struct_passwd_freep) struct passwd *pw = NULL; _cleanup_(struct_shadow_freep) struct spwd *sp = NULL; _cleanup_free_ char *error = NULL; bool complete = false; const char *inactive = NULL; const char *mindays = NULL; const char *maxdays = NULL; const char *warndays = NULL; const char *user = NULL; int args = 0; int pam_flags = 0; bool quiet = false; int r; setlocale(LC_ALL, ""); while (1) { int c; int option_index = 0; static struct option long_options[] = { {"delete", no_argument, NULL, 'd' }, {"expire", no_argument, NULL, 'e' }, {"help", no_argument, NULL, 'h' }, {"inactive", required_argument, NULL, 'I' }, {"keep-tokens", no_argument, NULL, 'k' }, {"lock", no_argument, NULL, 'l' }, {"mindays", required_argument, NULL, 'm' }, {"maxdays", required_argument, NULL, 'M' }, {"quiet", no_argument, NULL, 'q' }, {"stdin", no_argument, NULL, 's' }, {"status", no_argument, NULL, 'S' }, {"unlock", no_argument, NULL, 'u' }, {"version", no_argument, NULL, 'v' }, {"warndays", required_argument, NULL, 'w' }, {NULL, 0, NULL, '\0'} }; c = getopt_long(argc, argv, "dehI:klm:M:qsSuvw:", long_options, &option_index); if (c == (-1)) break; switch (c) { case 'd': args |= ARG_DELETE_PASSWORD; break; case 'e': args |= ARG_EXPIRE; break; case 'h': print_help(); return 0; case 'I': inactive = optarg; break; case 'k': pam_flags |= PAM_CHANGE_EXPIRED_AUTHTOK; break; case 'l': args |= ARG_LOCK_PASSWORD; break; case 'm': mindays = optarg; break; case 'M': maxdays = optarg; break; case 'q': quiet = true; pam_flags |= PAM_SILENT; break; case 's': args |= ARG_PASSWORD_STDIN; break; case 'S': args |= ARG_STATUS_ACCOUNT; break; case 'u': args |= ARG_UNLOCK_PASSWORD; break; case 'v': printf("passwd (%s) %s\n", PACKAGE, VERSION); return 0; case 'w': warndays = optarg; break; default: print_error(); return 1; } } argc -= optind; argv += optind; if (argc == 1) user = argv[0]; if (argc > 1) { fprintf(stderr, "passwd: Too many arguments.\n"); print_error(); return EINVAL; } r = check_and_drop_privs(); if (r < 0) return -r; /* get user account data */ r = pwaccess_get_user_record(user?-1:(int64_t)getuid(), user?user:NULL, &pw, &sp, &complete, &error); if (r < 0) { if (error && streq(error, "org.openSUSE.pwaccess.NoEntryFound")) { if (user) fprintf(stderr, "The user '%s' does not exist.\n", user); else fprintf(stderr, "No user for UID '%u' found.\n", getuid()); } else fprintf(stderr, "Cannot get user account data: %s\n", error?error:strerror(-r)); return -r; } if (pw == NULL) { fprintf(stderr, "ERROR: Unknown user '%s'.\n", user); return ENODATA; } if (!complete) { fprintf(stderr, "Permission denied.\n"); return EPERM; } /* if no user provided on commandline */ if (!user) user = pw->pw_name; if (args & ARG_STATUS_ACCOUNT) return print_account_status(pw, sp); else if (args & (ARG_DELETE_PASSWORD | ARG_EXPIRE | ARG_LOCK_PASSWORD | ARG_UNLOCK_PASSWORD | ARG_STATUS_ACCOUNT)) return modify_account(pw, sp, args, inactive, mindays, maxdays, warndays, quiet); else { pam_handle_t *pamh = NULL; if (args & ARG_PASSWORD_STDIN) { char *ptr; char password[PAM_MAX_RESP_SIZE]; /* independent of crypt type, PAM will not accept anything longer */ r = read(STDIN_FILENO, password, sizeof(password) - 1); if (r < 0) { r = errno; fprintf(stderr, "Error reading from stdin: %s\n", strerror(r)); return r; } password[r] = '\0'; /* Remove trailing \n. */ ptr = strchr(password, '\n'); if (ptr) *ptr = 0; conv.conv = stdin_conv; conv.appdata_ptr = strdup(password); explicit_bzero(password, sizeof(password)); if (conv.appdata_ptr == NULL) return oom(); } r = chauthtok(user, pam_flags); if (!PWACCESS_IS_NOT_RUNNING(-r)) return r; /* Fallback, run PAM stack ourself if we are root. That's needed to allow root to fix his password if system got booted with e.g. init=/bin/bash */ if (geteuid() != 0) return r; /* return error accessing pwupdd */ r = pam_start("passwd", user, &conv, &pamh); if (r != PAM_SUCCESS) { fprintf(stderr, "pam_start(\"passwd\", %s) failed: %s", user, pam_strerror(NULL, r)); return r; } r = pam_chauthtok(pamh, pam_flags); if (r != PAM_SUCCESS) { pam_end(pamh, r); fprintf(stderr, "pam_chauthtok() failed: %s", pam_strerror(NULL, r)); return r; } r = pam_end(pamh, 0); if (r != PAM_SUCCESS) { fprintf(stderr, "pam_end() failed: %s", pam_strerror(NULL, r)); return r; } } return 0; } account-utils-1.3.0/src/pwaccessd.c000066400000000000000000000571751521474342200172370ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "pwaccess.h" #include "basics.h" #include "mkdir_p.h" #include "verify.h" #include "check_caller_perms.h" #include "varlink-service-common.h" #include "read_config.h" #include "context.h" #include "varlink-org.openSUSE.pwaccess.h" #define USEC_PER_SEC ((uint64_t) 1000000ULL) #define DEFAULT_EXIT_USEC (30*USEC_PER_SEC) static int error_user_not_found(sd_varlink *link, int64_t uid, const char *name, int errcode) { if (errcode == 0) { if (uid >= 0) log_msg(LOG_INFO, "User (%" PRId64 ") not found", uid); else { const char *cp; if (!valid_name(name)) cp = ""; else cp = name; log_msg(LOG_INFO, "User '%s' not found", strna(cp)); } return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.NoEntryFound", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false)); } else { _cleanup_free_ char *error = NULL; if (asprintf(&error, "user not found: %s", strerror(errcode)) < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } } struct parameters { int64_t uid; char *name; char *password; bool nullok; }; static void parameters_free(struct parameters *var) { var->name = mfree(var->name); if (var->password) { explicit_bzero(var->password, strlen(var->password)); var->password = mfree(var->password); } } static int vl_method_get_account_name(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL; _cleanup_(parameters_free) struct parameters p = { .uid = -1, .name = NULL, .password = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "uid", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct parameters, uid), SD_JSON_MANDATORY}, {} }; struct passwd *pw = NULL; int r; log_msg(LOG_INFO, "Varlink method \"GetAccountName\" called..."); r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); if (r < 0) { log_msg(LOG_ERR, "GetAccountName request: varlink dispatch failed: %s", strerror(-r)); return r; } log_msg(LOG_DEBUG, "GetAccountName(%" PRId64 ")", p.uid); if (p.uid < 0 || p.uid > UINT32_MAX) { log_msg(LOG_ERR, "GetAccountName request: UID is out of range"); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "UID is out of range")); } errno = 0; /* to find out if getpwuid succeed and there is no entry if there was an error */ pw = getpwuid(p.uid); if (pw == NULL) return error_user_not_found(link, p.uid, NULL, errno); return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true), SD_JSON_BUILD_PAIR_STRING("userName", pw->pw_name)); } static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void *userdata) { struct context_t *ctx = userdata; _cleanup_(sd_json_variant_unrefp) sd_json_variant *passwd = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *shadow = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL; _cleanup_(parameters_free) struct parameters p = { .uid = -1, .name = NULL, .password = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "uid", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct parameters, uid), 0}, { "userName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, name), 0}, {} }; struct passwd *pw = NULL; struct spwd *sp = NULL; bool complete = true; uid_t peer_uid; int r; log_msg(LOG_INFO, "Varlink method \"GetUserRecord\" called..."); r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) { log_msg(LOG_ERR, "Failed to get peer UID: %s", strerror(-r)); return r; } r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); if (r < 0) { log_msg(LOG_ERR, "GetUserRecord request: varlink dispatch failed: %s", strerror(-r)); return r; } log_msg(LOG_DEBUG, "GetUserRecord(%" PRId64 ",%s)", p.uid, strna(p.name)); if (p.uid == -1 && isempty(p.name)) { log_msg(LOG_ERR, "GetUserRecord request: no UID nor user name specified"); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "No UID nor user name specified")); } if (p.uid != -1) { if (!isempty(p.name)) { log_msg(LOG_ERR, "GetUserRecord request: UID and user name specified"); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "UID and user name specified")); } if (p.uid < 0 || p.uid > UINT32_MAX) { log_msg(LOG_ERR, "GetUserRecord request: UID is out of range"); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "UID is out of range")); } } errno = 0; /* to find out if getpwuid/getpwnam succeed and there is no entry if there was an error */ if (p.uid != -1) pw = getpwuid(p.uid); else pw = getpwnam(p.name); if (pw == NULL) return error_user_not_found(link, p.uid, p.name, errno); /* Don't return password if query does not come from root and result is not the one of the calling user */ if (!check_caller_perms(peer_uid, pw->pw_uid, ctx->cfg.allow_get_user_record)) { log_msg(LOG_DEBUG, "Peer UID %u is not allowed to access data of '%s'", peer_uid, pw->pw_name); pw->pw_passwd = NULL; complete = false; /* no shadow entries for others */ sp = NULL; } else { /* Get shadow entry */ errno = 0; sp = getspnam(pw->pw_name); if (sp == NULL && errno != 0) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "getspnam() failed: %m") < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } } r = sd_json_variant_merge_objectbo(&passwd, SD_JSON_BUILD_PAIR_STRING("name", pw->pw_name), SD_JSON_BUILD_PAIR_STRING("passwd", pw->pw_passwd), SD_JSON_BUILD_PAIR_INTEGER("UID", pw->pw_uid), SD_JSON_BUILD_PAIR_INTEGER("GID", pw->pw_gid), SD_JSON_BUILD_PAIR_STRING("GECOS", pw->pw_gecos), SD_JSON_BUILD_PAIR_STRING("dir", pw->pw_dir), SD_JSON_BUILD_PAIR_STRING("shell", pw->pw_shell)); if (r < 0) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "JSON merge object passwd failed: %s", strerror(-r)) < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } if (sp) { r = sd_json_variant_merge_objectbo(&shadow, SD_JSON_BUILD_PAIR_STRING("name", sp->sp_namp), SD_JSON_BUILD_PAIR_STRING("passwd", sp->sp_pwdp), SD_JSON_BUILD_PAIR_INTEGER("lstchg", sp->sp_lstchg), SD_JSON_BUILD_PAIR_INTEGER("min", sp->sp_min), SD_JSON_BUILD_PAIR_INTEGER("max", sp->sp_max), SD_JSON_BUILD_PAIR_INTEGER("warn", sp->sp_warn), SD_JSON_BUILD_PAIR_INTEGER("inact", sp->sp_inact), SD_JSON_BUILD_PAIR_INTEGER("expire", sp->sp_expire), SD_JSON_BUILD_PAIR_INTEGER("flag", sp->sp_flag)); if (r < 0) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "JSON merge object shadow failed: %s", strerror(-r)) < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } } r = sd_json_variant_merge_objectbo(&result, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true)); if (r >= 0 && (passwd || shadow)) r = sd_json_variant_merge_objectbo(&result, SD_JSON_BUILD_PAIR_BOOLEAN("Complete", complete)); if (r >= 0 && passwd) r = sd_json_variant_merge_objectbo(&result, SD_JSON_BUILD_PAIR_VARIANT("passwd", passwd)); if (r >= 0 && shadow) r = sd_json_variant_merge_objectbo(&result, SD_JSON_BUILD_PAIR_VARIANT("shadow", shadow)); if (r < 0) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "JSON merge result object failed: %s", strerror(-r)) < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } return sd_varlink_reply(link, result); } static int vl_method_verify_password(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void *userdata) { struct context_t *ctx = userdata; _cleanup_(parameters_free) struct parameters p = { .name = NULL, .password = NULL, .nullok = false }; static const sd_json_dispatch_field dispatch_table[] = { { "userName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, name), SD_JSON_MANDATORY}, { "password", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, password), SD_JSON_MANDATORY}, { "nullOK", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct parameters, nullok), 0}, {} }; uid_t peer_uid; int r; log_msg(LOG_INFO, "Varlink method \"VerifyPassword\" called..."); r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) { log_msg(LOG_ERR, "Failed to get peer UID: %s", strerror(-r)); return r; } r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); if (r < 0) { log_msg(LOG_ERR, "VerifyPassword request: varlink dispatch failed: %s", strerror(-r)); return r; } if (isempty(p.name)) { log_msg(LOG_ERR, "VerifyPassword request: no user name specified"); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "No user name specified")); } struct passwd *pw = NULL; errno = 0; /* to find out if getpwnam succeed and there is no entry or if there was an error */ pw = getpwnam(p.name); if (pw == NULL) return error_user_not_found(link, -1, p.name, errno); if (!check_caller_perms(peer_uid, pw->pw_uid, ctx->cfg.allow_verify_password)) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "Peer UID %u is not allowed to verify password of '%s'", peer_uid, p.name) < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } const char *hash = pw->pw_passwd; if (is_shadow(pw)) { /* Get shadow entry */ errno = 0; struct spwd *sp = getspnam(pw->pw_name); if (sp == NULL) { /* errno == 0 => no shadow entry exists, do nothing */ if (errno != 0) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "getspnam() failed: %m") < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } } else hash = sp->sp_pwdp; } r = verify_password(hash, p.password, p.nullok); if (r < 0) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "verify_password() failed: %s", strerror(-r)) < 0) error = NULL; log_msg(LOG_ERR, "verify_password: %s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } else if (r > 0) { if (r == VERIFY_FAILED) /* password does not match */ { log_msg(LOG_DEBUG, "verify_password (%s): password does not match", strna(pw->pw_name)); return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", false)); } else /* libcrypt/internal error */ { const char *error = NULL; if (r == VERIFY_CRYPT_DISABLED) error = "The used salt is disabled in libcrypt"; else if (r == VERIFY_CRYPT_INVALID) error = "The used salt is not supported by libcrypt"; log_msg(LOG_ERR, "verify_password failed: %s", strna(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", strna(error))); } } log_msg(LOG_DEBUG, "verify_password (%s): password matches", strna(pw->pw_name)); return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true)); } static int vl_method_expired_check(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void *userdata) { struct context_t *ctx = userdata; _cleanup_(parameters_free) struct parameters p = { .name = NULL, .password = NULL, }; static const sd_json_dispatch_field dispatch_table[] = { { "userName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, name), SD_JSON_MANDATORY}, {} }; uid_t peer_uid; long daysleft; bool pwchangeable; int r; log_msg(LOG_INFO, "Varlink method \"ExpiredCheck\" called..."); r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) { log_msg(LOG_ERR, "Failed to get peer UID: %s", strerror(-r)); return r; } r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); if (r < 0) { log_msg(LOG_ERR, "ExpiredCheck request: varlink dispatch failed: %s", strerror(-r)); return r; } if (isempty(p.name)) { log_msg(LOG_ERR, "ExpiredCheck request: no user name specified"); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "No user name specified")); } struct passwd *pw; errno = 0; /* to find out if getpwnam succeed and there is no entry or if there was an error */ pw = getpwnam(p.name); if (pw == NULL) return error_user_not_found(link, -1, p.name, errno); /* Don't verify password if query does not come from root and result is not the one of the calling user */ if (!check_caller_perms(peer_uid, pw->pw_uid, ctx->cfg.allow_expired_check)) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "Peer UID %u is not allowed to access data of '%s'", peer_uid, p.name) < 0) error = NULL; log_msg(LOG_ERR, "ExpiredCheck: %s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } /* Get shadow entry */ errno = 0; struct spwd *sp = getspnam(pw->pw_name); if (sp == NULL) { if (errno != 0) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "getspnam() failed: %m") < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } log_msg(LOG_DEBUG, "ExpiredCheck: no shadow entry for %s", pw->pw_name); return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true), SD_JSON_BUILD_PAIR_INTEGER("Expired", PWA_EXPIRED_NO)); } r = expired_check(sp, &daysleft, &pwchangeable); if (r < 0) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "expired_check() failed: %s", strerror(-r)) < 0) error = NULL; log_msg(LOG_ERR, "expired_check: %s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } log_msg(LOG_DEBUG, "expired_check(%s): expired: %d, daysleft: %ld", strna(p.name), r, daysleft); return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true), SD_JSON_BUILD_PAIR_INTEGER("DaysLeft", daysleft), SD_JSON_BUILD_PAIR_INTEGER("Expired", r), SD_JSON_BUILD_PAIR_BOOLEAN("PWChangeAble", pwchangeable)); } /* Send a messages to systemd daemon, that inicialization of daemon is finished and daemon is ready to accept connections. */ static void announce_ready (void) { int r = sd_notify (0, "READY=1\n" "STATUS=Processing requests..."); if (r < 0) log_msg (LOG_ERR, "sd_notify(READY) failed: %s", strerror(-r)); } static void announce_stopping (void) { int r = sd_notify (0, "STOPPING=1\n" "STATUS=Shutting down..."); if (r < 0) log_msg (LOG_ERR, "sd_notify(STOPPING) failed: %s", strerror(-r)); } /* event loop which quits after 30 seconds idle time */ #define DEFAULT_EXIT_USEC (30*USEC_PER_SEC) static int varlink_event_loop_with_idle(sd_event *e, sd_varlink_server *s) { int r, code; for (;;) { r = sd_event_get_state(e); if (r < 0) return r; if (r == SD_EVENT_FINISHED) break; r = sd_event_run(e, DEFAULT_EXIT_USEC); if (r < 0) return r; if (r == 0 && (sd_varlink_server_current_connections(s) == 0)) sd_event_exit(e, 0); } r = sd_event_get_exit_code(e, &code); if (r < 0) return r; return code; } static int run_varlink(bool socket_activation, struct context_t *ctx) { int r; _cleanup_(sd_event_unrefp) sd_event *event = NULL; _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; r = mkdir_p(_VARLINK_PWACCESS_SOCKET_DIR, 0755); if (r < 0) { log_msg(LOG_ERR, "Failed to create directory '"_VARLINK_PWACCESS_SOCKET_DIR"' for Varlink socket: %s", strerror(-r)); return r; } r = sd_event_new(&event); if (r < 0) { log_msg(LOG_ERR, "Failed to create new event: %s", strerror(-r)); return r; } ctx->loop = TAKE_PTR(event); r = sd_varlink_server_new(&varlink_server, SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA|SD_VARLINK_SERVER_INPUT_SENSITIVE); if (r < 0) { log_msg(LOG_ERR, "Failed to allocate varlink server: %s", strerror (-r)); return r; } r = sd_varlink_server_set_description(varlink_server, "pwaccessd"); if (r < 0) { log_msg(LOG_ERR, "Failed to set varlink server description: %s", strerror(-r)); return r; } r = sd_varlink_server_set_info (varlink_server, NULL, PACKAGE" (pwaccessd)", VERSION, "https://github.com/thkukuk/pwaccess"); if (r < 0) return r; r = sd_varlink_server_add_interface (varlink_server, &vl_interface_org_openSUSE_pwaccess); if (r < 0) { log_msg(LOG_ERR, "Failed to add interface: %s", strerror(-r)); return r; } sd_varlink_server_set_userdata(varlink_server, ctx); r = sd_varlink_server_bind_method_many(varlink_server, "org.openSUSE.pwaccess.GetAccountName", vl_method_get_account_name, "org.openSUSE.pwaccess.GetUserRecord", vl_method_get_user_record, "org.openSUSE.pwaccess.VerifyPassword", vl_method_verify_password, "org.openSUSE.pwaccess.ExpiredCheck", vl_method_expired_check, "org.openSUSE.pwaccess.GetEnvironment", vl_method_get_environment, "org.openSUSE.pwaccess.Ping", vl_method_ping, "org.openSUSE.pwaccess.Quit", vl_method_quit, "org.openSUSE.pwaccess.SetLogLevel", vl_method_set_log_level); if (r < 0) { log_msg(LOG_ERR, "Failed to bind Varlink methods: %s", strerror(-r)); return r; } r = sd_varlink_server_attach_event(varlink_server, ctx->loop, SD_EVENT_PRIORITY_NORMAL); if (r < 0) { log_msg(LOG_ERR, "Failed to attach to event: %s", strerror(-r)); return r; } r = sd_varlink_server_listen_auto(varlink_server); if (r < 0) { log_msg (LOG_ERR, "Failed to listen: %s", strerror(-r)); return r; } if (!socket_activation) { r = sd_varlink_server_listen_address(varlink_server, _VARLINK_PWACCESS_SOCKET, 0666); if (r < 0) { log_msg(LOG_ERR, "Failed to bind to Varlink socket: %s", strerror(-r)); return r; } } announce_ready(); if (socket_activation) r = varlink_event_loop_with_idle(ctx->loop, varlink_server); else r = sd_event_loop(ctx->loop); announce_stopping(); return r; } static void print_help(void) { printf("pwaccessd - manage passwd and shadow\n"); printf(" -s, --socket Activation through socket\n"); printf(" -d, --debug Debug mode\n"); printf(" -v, --verbose Verbose logging\n"); printf(" -?, --help Give this help list\n"); printf(" --version Print program version\n"); } static void struct_context_free(struct context_t *var) { struct_config_free(&(var->cfg)); sd_event_unrefp(&(var->loop)); } int main(int argc, char **argv) { int socket_activation = false; _cleanup_(struct_context_free) struct context_t ctx = { {NULL, NULL, NULL}, NULL }; econf_err error = read_config(&ctx.cfg); if (error != ECONF_SUCCESS) log_msg(LOG_NOTICE, "Error reading config file: %s", econf_errString(error)); while (1) { int c; int option_index = 0; static struct option long_options[] = { {"socket", no_argument, NULL, 's'}, {"debug", no_argument, NULL, 'd'}, {"verbose", no_argument, NULL, 'v'}, {"version", no_argument, NULL, '\255'}, {"usage", no_argument, NULL, '?'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, '\0'} }; c = getopt_long (argc, argv, "sdvh?", long_options, &option_index); if (c == (-1)) break; switch (c) { case 's': socket_activation = true; break; case 'd': set_max_log_level(LOG_DEBUG); break; case '?': case 'h': print_help (); return 0; case 'v': set_max_log_level(LOG_INFO); break; case '\255': fprintf (stdout, "pwaccessd (%s) %s\n", PACKAGE, VERSION); return 0; default: print_help (); return 1; } } argc -= optind; argv += optind; if (argc > 1) { fprintf(stderr, "Try `pwaccessd --help' for more information.\n"); return 1; } log_msg(LOG_INFO, "Starting pwaccessd (%s) %s...", PACKAGE, VERSION); int r = run_varlink(socket_activation, &ctx); if (r < 0) { log_msg(LOG_ERR, "ERROR: varlink loop failed: %s", strerror (-r)); return -r; } log_msg(LOG_INFO, "pwaccessd stopped."); return 0; } account-utils-1.3.0/src/pwupdd.c000066400000000000000000001222231521474342200165510ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pwaccess.h" #include "basics.h" #include "mkdir_p.h" #include "varlink-service-common.h" #include "files.h" #include "verify.h" #include "chfn_checks.h" #include "check_caller_perms.h" #include "varlink-org.openSUSE.pwupd.h" static int error_user_not_found(sd_varlink *link, int64_t uid, const char *name) { if (errno == 0) { const char *cp; if (!valid_name(name)) cp = ""; else cp = name; if (uid >= 0) log_msg(LOG_INFO, "User (%" PRId64 "|%s) not found", uid, strna(cp)); else log_msg(LOG_INFO, "User '%s' not found", strna(cp)); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.NoEntryFound", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false)); } else { _cleanup_free_ char *error = NULL; if (asprintf(&error, "user not found: %m") < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } } static int return_errno_error(sd_varlink *link, const char *function, int r) { _cleanup_free_ char *error = NULL; const char *varlink_error = "org.openSUSE.pwupd.InternalError"; if (r < 0) r = -r; if (r == EPERM) varlink_error = "org.openSUSE.pwupd.PermissionDenied"; if (asprintf(&error, "%s failed: %s", function, strerror(r)) < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, varlink_error, SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } struct parameters { const char *pam_service; char *name; char *shell; char *full_name; char *home_phone; char *other; char *room; char *work_phone; char *old_gecos; char *response; int flags; sd_json_variant *content_passwd; sd_json_variant *content_shadow; sd_varlink *link; }; static void parameters_free(struct parameters *var) { var->name = mfree(var->name); var->shell = mfree(var->shell); var->full_name = mfree(var->full_name); var->home_phone = mfree(var->home_phone); var->other = mfree(var->other); var->room = mfree(var->room); var->work_phone = mfree(var->work_phone); var->old_gecos = mfree(var->old_gecos); var->response = mfree(var->response); var->content_passwd = sd_json_variant_unref(var->content_passwd); var->content_shadow = sd_json_variant_unref(var->content_shadow); } static sd_json_variant *send_v = NULL; static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; static char *answer = NULL; static bool got_answer = false; static bool broadcast_called = false; static int varlink_conv(int num_msg, const struct pam_message **msgm, struct pam_response **response, void *appdata_ptr) { struct parameters *p = appdata_ptr; int r; log_msg(LOG_DEBUG, "varlink_conv with %i messages called", num_msg); assert(p); if (num_msg <= 0) return PAM_CONV_ERR; log_msg(LOG_DEBUG, "style=%i, msg=%s", msgm[0]->msg_style, msgm[0]->msg); for (int count = 0; count < num_msg; ++count) { switch (msgm[count]->msg_style) { case PAM_PROMPT_ECHO_ON: case PAM_PROMPT_ECHO_OFF: pthread_mutex_lock(&mut); send_v = sd_json_variant_unref(send_v); r = sd_json_variant_merge_objectbo(&send_v, SD_JSON_BUILD_PAIR_INTEGER("msg_style", msgm[count]->msg_style), SD_JSON_BUILD_PAIR("message", SD_JSON_BUILD_STRING(msgm[count]->msg))); if (r < 0) log_msg(LOG_ERR, "Failed to build send_v list: %s\n", strerror(-r)); broadcast_called = true; pthread_cond_broadcast(&cond); pthread_mutex_unlock(&mut); *response = calloc(num_msg, sizeof(struct pam_response)); if (*response == NULL) { log_msg(LOG_ERR, "Out of memory!"); return PAM_BUF_ERR; } /* waiting for answer */ pthread_mutex_lock(&mut); log_msg(LOG_DEBUG, "varlink_conv: wait for answer from client"); while (!got_answer) pthread_cond_wait(&cond, &mut); response[0]->resp_retcode = 0; response[0]->resp = TAKE_PTR(answer); got_answer = false; pthread_mutex_unlock(&mut); log_msg(LOG_DEBUG, "varlink_conv: after mutex"); break; case PAM_ERROR_MSG: case PAM_TEXT_INFO: r = sd_varlink_notifybo(p->link, SD_JSON_BUILD_PAIR_INTEGER("msg_style", msgm[count]->msg_style), SD_JSON_BUILD_PAIR("message", SD_JSON_BUILD_STRING(msgm[count]->msg))); sd_varlink_flush(p->link); if (r < 0) { log_msg(LOG_ERR, "Failed to send notify: %s\n", strerror(-r)); return PAM_SYSTEM_ERR; } break; default: log_msg(LOG_ERR, "Unknown msg style: %i\n", msgm[count]->msg_style); return PAM_SYSTEM_ERR; } } return PAM_SUCCESS; } static void * broadcast_and_return(intptr_t r) { pthread_mutex_lock(&mut); broadcast_called = true; pthread_cond_broadcast(&cond); pthread_mutex_unlock(&mut); return (void *)r; } static void * run_pam_auth(void *arg) { struct parameters *param = arg; _cleanup_(parameters_free) struct parameters p = { .pam_service = param->pam_service, .name = param->name, .shell = param->shell, .full_name = param->full_name, .home_phone = param->home_phone, .other = param->other, .room = param->room, .work_phone = param->work_phone, .old_gecos = param->old_gecos, .response = NULL, .flags = param->flags, .content_passwd = NULL, .content_shadow = NULL, .link = param->link, }; const struct pam_conv conv = { varlink_conv, &p, }; pam_handle_t *pamh = NULL; intptr_t r; r = pam_start(p.pam_service, p.name, &conv, &pamh); if (r != PAM_SUCCESS) { log_msg(LOG_ERR, "pam_start(\"%s\", %s) failed: %s", p.pam_service, p.name, pam_strerror(NULL, r)); return broadcast_and_return(r); } r = pam_authenticate(pamh, 0); if (r != PAM_SUCCESS) { pam_end (pamh, r); log_msg(LOG_ERR, "pam_authenticate() failed: %s", pam_strerror(NULL, r)); return broadcast_and_return(r); } r = pam_acct_mgmt(pamh, 0); if (r == PAM_NEW_AUTHTOK_REQD) { r = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK); if (r != PAM_SUCCESS) { pam_end (pamh, r); log_msg(LOG_ERR, "pam_chauthtok() failed: %s", pam_strerror(NULL, r)); return broadcast_and_return(r); } } else if (r != PAM_SUCCESS) { pam_end (pamh, r); log_msg(LOG_ERR, "pam_acct_mgmt() failed: %s", pam_strerror(NULL, r)); return broadcast_and_return(r); } r = pam_end(pamh, 0); if (r != PAM_SUCCESS) { log_msg(LOG_ERR, "pam_end() failed: %s", pam_strerror(NULL, r)); return broadcast_and_return(r); } if (!isempty(p.shell)) { struct passwd pw; memset(&pw, 0, sizeof(pw)); pw.pw_name = p.name; pw.pw_shell = p.shell; r = update_passwd(&pw, NULL); if (r < 0) { log_msg(LOG_ERR, "update_passwd() failed: %s", strerror(-r)); return broadcast_and_return(PAM_SYSTEM_ERR); } log_msg(LOG_INFO, "chsh: changed shell for '%s' to '%s'", p.name, p.shell); } else if (p.full_name || p.home_phone || p.other || p.room || p.work_phone) { const char *full_name = NULL; const char *home_phone = NULL; const char *other = NULL; const char *room = NULL; const char *work_phone = NULL; struct passwd pw; char *cp; const char *f; size_t s; _cleanup_free_ char *new_gecos = NULL; /* Split old GECOS field and overwrite single parts */ cp = p.old_gecos; f = strsep(&cp, ","); full_name = f; f = strsep(&cp, ","); room = f; f = strsep(&cp, ","); work_phone = f; f = strsep(&cp, ","); home_phone = f; /* Anything left over is "other". */ other = cp; if (p.full_name != NULL) full_name = p.full_name; if (p.room != NULL) room = p.room; if (p.work_phone != NULL) work_phone = p.work_phone; if (p.home_phone != NULL) home_phone = p.home_phone; if (p.other) other = p.other; if (asprintf(&new_gecos, "%s,%s,%s,%s,%s", strempty(full_name), strempty(room), strempty(work_phone), strempty(home_phone), strempty(other)) < 0) return broadcast_and_return(PAM_BUF_ERR); /* remove trailing ',' */ s = strlen(new_gecos); while (s > 0 && new_gecos[s-1] == ',') { new_gecos[s-1] = '\0'; s--; } memset(&pw, 0, sizeof(pw)); pw.pw_name = p.name; pw.pw_gecos = new_gecos; r = update_passwd(&pw, NULL); if (r < 0) { log_msg(LOG_ERR, "update_passwd() failed: %s", strerror(-r)); return broadcast_and_return(PAM_SYSTEM_ERR); } log_msg(LOG_INFO, "chfn: changed GECOS for '%s' to '%s'", p.name, strempty(new_gecos)); } else log_msg(LOG_INFO, "chfn/chsh: nothing to update"); return broadcast_and_return(PAM_SUCCESS); } static pthread_t pam_thread; static bool pam_thread_is_valid = false; static int thread_is_running(void) { if (pam_thread_is_valid) { int r = pthread_kill(pam_thread, 0); return -r; } return -ENOENT; } static int vl_method_chfn(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { /* don't free via _cleanup_, can be still in use by the pam thread */ struct parameters p = { .pam_service = "pwupd-chfn", .name = NULL, .shell = NULL, .full_name = NULL, .home_phone = NULL, .other = NULL, .room = NULL, .work_phone = NULL, .old_gecos = NULL, .response = NULL, .flags = 0, .content_passwd = NULL, .content_shadow = NULL, .link = link, }; static const sd_json_dispatch_field dispatch_table[] = { { "userName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, name), SD_JSON_MANDATORY}, { "fullName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, full_name), SD_JSON_NULLABLE}, { "homePhone", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, home_phone), SD_JSON_NULLABLE}, { "other", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, other), SD_JSON_NULLABLE}, { "room", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, room), SD_JSON_NULLABLE}, { "workPhone", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, work_phone), SD_JSON_NULLABLE}, {} }; _cleanup_free_ char *error = NULL; struct passwd *pw = NULL; uid_t peer_uid; int r; log_msg(LOG_INFO, "Varlink method \"chfn\" called..."); /* If there is already a thread running, quit with error. The conv method needs to be called to continue. */ r = thread_is_running(); if (r == 0) { log_msg(LOG_ERR, "chfn method called while already running!"); return -EPERM; } r = sd_varlink_dispatch(p.link, parameters, dispatch_table, &p); if (r < 0) { log_msg(LOG_ERR, "chfn request: varlink dispatch failed: %s", strerror(-r)); return r; } if (isempty(p.name)) { parameters_free(&p); log_msg(LOG_ERR, "chfn request: no user name specified"); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "No user name specified")); } errno = 0; /* to find out if getpwnam succeed and there is no entry or if there was an error */ pw = getpwnam(p.name); if (pw == NULL) { r = error_user_not_found(link, -1, p.name); parameters_free(&p); return r; } r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) return return_errno_error(link, "Get peer UID", r); /* Don't change GECOS if query does not come from root and result is not the one of the calling user */ if (!check_caller_perms(peer_uid, pw->pw_uid, NULL)) { if (asprintf(&error, "Peer UID %u is not allowed to access data of '%s'", peer_uid, p.name) < 0) error = NULL; log_msg(LOG_ERR, "chfn: %s", stroom(error)); parameters_free(&p); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } /* Now that we know the caller is allowed to make changes, check if the caller is allowed to change the single fields and the new values are valid */ if (p.full_name) { if (!may_change_field(peer_uid, 'f', &error)) { if (error) log_msg(LOG_ERR, "chfn (full name): %s", error); parameters_free(&p); return return_errno_error(link, "permission check (full name)", -EPERM); } if (!chfn_check_string(p.full_name, ":,=", &error)) { if (error) log_msg(LOG_ERR, "chfn (full name): %s", error); parameters_free(&p); return return_errno_error(link, "character check (full name)", -EINVAL); } } if (p.home_phone) { if (!may_change_field(peer_uid, 'h', &error)) { if (error) log_msg(LOG_ERR, "chfn (home phone): %s", error); parameters_free(&p); return return_errno_error(link, "permission check (home phone)", -EPERM); } if (!chfn_check_string(p.home_phone, ":,=", &error)) { if (error) log_msg(LOG_ERR, "chfn (home phone): %s", error); parameters_free(&p); return return_errno_error(link, "character check (home phone)", -EINVAL); } } if (p.other) { if (!may_change_field(peer_uid, 'o', &error)) { if (error) log_msg(LOG_ERR, "chfn (other): %s", error); parameters_free(&p); return return_errno_error(link, "permission check (other)", -EPERM); } if (!chfn_check_string(p.other, ":", &error)) { if (error) log_msg(LOG_ERR, "chfn (other): %s", error); parameters_free(&p); return return_errno_error(link, "character check (other)", -EINVAL); } } if (p.room) { if (!may_change_field(peer_uid, 'r', &error)) { if (error) log_msg(LOG_ERR, "chfn (room): %s", error); parameters_free(&p); return return_errno_error(link, "permission check (room)", -EPERM); } if (!chfn_check_string(p.room, ":,=", &error)) { if (error) log_msg(LOG_ERR, "chfn (room): %s", error); parameters_free(&p); return return_errno_error(link, "character check (room)", -EINVAL); } } if (p.work_phone) { if (!may_change_field(peer_uid, 'w', &error)) { if (error) log_msg(LOG_ERR, "chfn (work phone): %s", error); parameters_free(&p); return return_errno_error(link, "permission check (work phone)", -EPERM); } if (!chfn_check_string(p.work_phone, ":,=", &error)) { if (error) log_msg(LOG_ERR, "chfn (work phone): %s", error); parameters_free(&p); return return_errno_error(link, "character check (work phone)", -EINVAL); } } p.old_gecos = strdup(strempty(pw->pw_gecos)); if (p.old_gecos == NULL) { parameters_free(&p); return return_errno_error(link, "strdup", -ENOMEM); } /* Run under the UID of the caller, else pam_unix will not ask for old password and pam_rootok will wrongly match. */ if (peer_uid != 0) { log_msg(LOG_DEBUG, "Calling setresuid(%u,0,0)", peer_uid); if (setresuid(peer_uid, 0, 0) != 0) { parameters_free(&p); return return_errno_error(link, "setresuid", errno); } } r = pthread_create(&pam_thread, NULL, &run_pam_auth, &p); if (r != 0) return return_errno_error(link, "pthread_create", r); pam_thread_is_valid = true; pthread_mutex_lock(&mut); log_msg(LOG_DEBUG, "chfn: waiting for PAM thread"); while(!broadcast_called) pthread_cond_wait(&cond, &mut); broadcast_called = false; /* we need input from the user, quit method and send prompt back */ if (send_v != NULL) { r = sd_varlink_reply(link, send_v); pthread_mutex_unlock(&mut); return r; } pthread_mutex_unlock(&mut); intptr_t *thread_res = NULL; r = pthread_join(pam_thread, (void **)&thread_res); if (r != 0) return return_errno_error(link, "pthread_join", r); if (thread_res != PAM_SUCCESS) { int64_t t = (int64_t)thread_res; if (t > 0) { if (asprintf(&error, "PAM authentication failed: %s", pam_strerror(NULL, t)) < 0) error = NULL; } else { if (asprintf(&error, "Updating passwd/shadow failed: %s", strerror(-t)) < 0) error = NULL; } return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true)); } /* XXX move all code access /etc/shells into one file */ static bool is_known_shell(const char *shell) { _cleanup_(econf_freeFilep) econf_file *key_file = NULL; _cleanup_(econf_freeArrayp) char **keys = NULL; size_t size = 0; econf_err error; error = econf_readConfig(&key_file, NULL /* project */, _PATH_VENDORDIR /* usr_conf_dir */, "shells" /* config_name */, NULL /* config_suffix */, "" /* delim, key only */, "#" /* comment */); if (error != ECONF_SUCCESS) { log_msg(LOG_ERR, "Cannot parse shell files: %s", econf_errString(error)); return false; } error = econf_getKeys(key_file, NULL, &size, &keys); if (error) { log_msg(LOG_ERR, "Cannot evaluate entries in shell files: %s", econf_errString(error)); return false; } for (size_t i = 0; i < size; i++) if (streq(keys[i], shell)) return true; return false; } /* If the shell is completely invalid, print an error and return false. If root changes the shell, print only a warning. Only exception: Invalid characters are always not allowed. */ static bool valid_shell(const char *shell, uid_t uid, char **msg) { assert(msg); /* Keep /etc/passwd clean. This is always required, even for root. */ for (size_t i = 0; i < strlen(shell); i++) { char c = shell[i]; if (iscntrl (c)) { *msg = strdup("Error: control characters are not allowed."); return false; } if (c == ',' || c == ':' || c == '=' || c == '"') { if (asprintf(msg, "Error: '%c' is not allowed.", c) < 0) *msg = NULL; return false; } } /* Check if the shell exists and is known. Error for normal users, warning only for root */ if (*shell != '/') { if (asprintf(msg, "%s: shell must be a full path name.", uid?"Error":"Warning") < 0) *msg = NULL; if (uid) return false; } else if (access(shell, F_OK) < 0) { if (asprintf(msg, "%s: '%s' does not exist.", uid?"Error":"Warning", shell) < 0) *msg = NULL; if (uid) return false; } else if (access(shell, X_OK) < 0) { if (asprintf(msg, "%s: '%s' is not executable.", uid?"Error":"Warning", shell) < 0) *msg = NULL; if (uid) return false; } else if (!is_known_shell(shell)) { if (asprintf(msg, "%s: '%s' is not listed as valid login shell.", uid?"Error":"Warning", shell) < 0) *msg = NULL; if (uid) return false; } return true; } static int vl_method_chsh(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { /* don't free, can be still in use by the pam thread */ struct parameters p = { .pam_service = "pwupd-chsh", .name = NULL, .shell = NULL, .full_name = NULL, .home_phone = NULL, .other = NULL, .room = NULL, .work_phone = NULL, .old_gecos = NULL, .response = NULL, .flags = 0, .content_passwd = NULL, .content_shadow = NULL, .link = link, }; static const sd_json_dispatch_field dispatch_table[] = { { "userName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, name), SD_JSON_MANDATORY}, { "shell", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, shell), SD_JSON_MANDATORY}, {} }; uid_t peer_uid; int r; log_msg(LOG_INFO, "Varlink method \"chsh\" called..."); /* If there is already a thread running, quit with error. The conv method needs to be called to continue. */ r = thread_is_running(); if (r == 0) { log_msg(LOG_ERR, "chsh method called while already running!"); return -EPERM; } r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) return return_errno_error(link, "Get peer UID", r); r = sd_varlink_dispatch(p.link, parameters, dispatch_table, &p); if (r < 0) return return_errno_error(link, "chsh request: varlink dispatch", r); if (isempty(p.name)) { log_msg(LOG_ERR, "chsh request: no user name specified"); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "No user name specified")); } struct passwd *pw = NULL; errno = 0; /* to find out if getpwnam succeed and there is no entry or if there was an error */ pw = getpwnam(p.name); if (pw == NULL) { r = error_user_not_found(link, -1, p.name); parameters_free(&p); return r; } /* Don't change shell if query does not come from root and result is not the one of the calling user */ if (!check_caller_perms(peer_uid, pw->pw_uid, NULL)) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "Peer UID %u is not allowed to access data of '%s'", peer_uid, p.name) < 0) error = NULL; log_msg(LOG_ERR, "chsh: %s", stroom(error)); parameters_free(&p); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } _cleanup_free_ char *msg = NULL; if (!valid_shell(p.shell, peer_uid, &msg)) { log_msg(LOG_ERR, "%s", stroom(msg)); parameters_free(&p); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InvalidShell", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(msg))); } if (msg) /* ignore if it fails or not */ sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_INTEGER("msg_style", PAM_TEXT_INFO), SD_JSON_BUILD_PAIR_STRING("message", msg)); /* Run under the UID of the caller, else pam_unix will not ask for old password and pam_rootok will wrongly match. */ if (peer_uid != 0) { log_msg(LOG_DEBUG, "Calling setresuid(%u,0,0)", peer_uid); if (setresuid(peer_uid, 0, 0) != 0) { parameters_free(&p); return return_errno_error(link, "setresuid", errno); } } r = pthread_create(&pam_thread, NULL, &run_pam_auth, &p); if (r != 0) return return_errno_error(link, "pthread_create", r); pam_thread_is_valid = true; pthread_mutex_lock(&mut); log_msg(LOG_DEBUG, "chsh: waiting for PAM thread"); while(!broadcast_called) pthread_cond_wait(&cond, &mut); broadcast_called = false; /* we need input from the user, quit method and send prompt back */ if (send_v != NULL) { r = sd_varlink_reply(link, send_v); pthread_mutex_unlock(&mut); return r; } pthread_mutex_unlock(&mut); intptr_t *thread_res = NULL; r = pthread_join(pam_thread, (void **)&thread_res); if (r != 0) return return_errno_error(link, "pthread_join", r); if (thread_res != PAM_SUCCESS) { _cleanup_free_ char *error = NULL; int64_t t = (int64_t)thread_res; if (t > 0) { if (asprintf(&error, "PAM authentication failed: %s", pam_strerror(NULL, t)) < 0) error = NULL; } else { if (asprintf(&error, "Updating passwd/shadow failed: %s", strerror(-t)) < 0) error = NULL; } return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true)); } static void * run_pam_chauthtok(void *arg) { struct parameters *param = arg; _cleanup_(parameters_free) struct parameters p = { .pam_service = param->pam_service, .name = param->name, .shell = param->shell, .full_name = param->full_name, .home_phone = param->home_phone, .other = param->other, .room = param->room, .work_phone = param->work_phone, .old_gecos = param->old_gecos, .response = NULL, .flags = param->flags, .content_passwd = NULL, .content_shadow = NULL, .link = param->link, }; const struct pam_conv conv = { varlink_conv, &p, }; pam_handle_t *pamh = NULL; intptr_t r; r = pam_start(p.pam_service, p.name, &conv, &pamh); if (r != PAM_SUCCESS) { log_msg(LOG_ERR, "pam_start(\"%s\", %s) failed: %s", p.pam_service, p.name, pam_strerror(NULL, r)); return broadcast_and_return(r); } log_msg(LOG_DEBUG, "pam_chauthtok(pamh, %i)", p.flags); r = pam_chauthtok(pamh, p.flags); if (r != PAM_SUCCESS) { pam_end(pamh, r); log_msg(LOG_ERR, "pam_chauthtok() failed: %s", pam_strerror(NULL, r)); return broadcast_and_return(r); } r = pam_end(pamh, 0); if (r != PAM_SUCCESS) { log_msg(LOG_ERR, "pam_end() failed: %s", pam_strerror(NULL, r)); return broadcast_and_return(r); } return broadcast_and_return(r); } static int vl_method_chauthtok(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { /* don't free, can be still in use by the pam thread */ struct parameters p = { .pam_service = "pwupd-passwd", .name = NULL, .shell = NULL, .full_name = NULL, .home_phone = NULL, .other = NULL, .room = NULL, .work_phone = NULL, .old_gecos = NULL, .response = NULL, .flags = 0, .content_passwd = NULL, .content_shadow = NULL, .link = link, }; static const sd_json_dispatch_field dispatch_table[] = { { "userName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, name), SD_JSON_MANDATORY}, { "flags", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, offsetof(struct parameters, flags), 0}, {} }; uid_t peer_uid; int r; log_msg(LOG_INFO, "Varlink method \"chauthtok\" called..."); /* If there is already a thread running, quit with error. The conv method needs to be called to continue. */ r = thread_is_running(); if (r == 0) { log_msg(LOG_ERR, "chauthtok method called while already running!"); return -EPERM; } r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) return return_errno_error(link, "Get peer UID", r); r = sd_varlink_dispatch(p.link, parameters, dispatch_table, &p); if (r < 0) return return_errno_error(link, "chauthtok: varlink dispatch", r); if (isempty(p.name)) { log_msg(LOG_ERR, "chauthtok request: no user name specified"); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InvalidParameter", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", "No user name specified")); } struct passwd *pw = NULL; errno = 0; /* to find out if getpwnam succeed and there is no entry or if there was an error */ pw = getpwnam(p.name); if (pw == NULL) { r = error_user_not_found(link, -1, p.name); parameters_free(&p); return r; } /* Don't change password if query does not come from root and result is not the one of the calling user */ if (!check_caller_perms(peer_uid, pw->pw_uid, NULL)) { _cleanup_free_ char *error = NULL; if (asprintf(&error, "Peer UID %u is not allowed to access data of '%s'", peer_uid, p.name) < 0) error = NULL; log_msg(LOG_ERR, "chauthtok: %s", stroom(error)); parameters_free(&p); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.PermissionDenied", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } /* Run under the UID of the caller, else pam_unix will not ask for old password and pam_rootok will wrongly match. */ if (peer_uid != 0) { log_msg(LOG_DEBUG, "Calling setresuid(%u,0,0)", peer_uid); if (setresuid(peer_uid, 0, 0) != 0) { parameters_free(&p); return return_errno_error(link, "setresuid", errno); } } r = pthread_create(&pam_thread, NULL, &run_pam_chauthtok, &p); if (r != 0) return return_errno_error(link, "pthread_create", errno); pam_thread_is_valid = true; pthread_mutex_lock(&mut); log_msg(LOG_DEBUG, "chauthtok: waiting for PAM thread"); while(!broadcast_called) pthread_cond_wait(&cond, &mut); broadcast_called = false; /* we need input from the user, quit method and send prompt back */ if (send_v != NULL) { r = sd_varlink_reply(link, send_v); pthread_mutex_unlock(&mut); return r; } pthread_mutex_unlock(&mut); intptr_t *thread_res = NULL; r = pthread_join(pam_thread, (void **)&thread_res); if (r != 0) return return_errno_error(link, "pthread_join", errno); if (thread_res != PAM_SUCCESS) { _cleanup_free_ char *error = NULL; int64_t t = (int64_t)thread_res; if (asprintf(&error, "PAM authentication failed: %s", pam_strerror(NULL, t)) < 0) error = NULL; return sd_varlink_errorbo(link, "org.openSUSE.pwupd.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true)); } static int vl_method_conv(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { _cleanup_(parameters_free) struct parameters p = { .name = NULL, .shell = NULL, .full_name = NULL, .home_phone = NULL, .other = NULL, .room = NULL, .work_phone = NULL, .old_gecos = NULL, .response = NULL, .flags = 0, .content_passwd = NULL, .content_shadow = NULL, .link = link, }; static const sd_json_dispatch_field dispatch_table[] = { { "response", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct parameters, response), SD_JSON_NULLABLE}, {} }; int r; log_msg(LOG_INFO, "Varlink method \"conv\" called..."); /* make sure there is a pam_start() thread running! */ r = thread_is_running(); if (r != 0) return return_errno_error(link, "Finding PAM thread", r); r = sd_varlink_dispatch(p.link, parameters, dispatch_table, &p); if (r < 0) return return_errno_error(link, "Conv request: varlink dispatch", r); /* set pam_response */ pthread_mutex_lock(&mut); log_msg(LOG_DEBUG, "method_conv: set response and send cond_broadcast"); send_v = sd_json_variant_unref(send_v); if (p.response) answer = strdup(p.response); else answer = NULL; got_answer = true; /* inform PAM thread about answer */ pthread_cond_broadcast(&cond); pthread_mutex_unlock(&mut); /* wait for next PAM_PROMPT_ECHO_* message or exit */ pthread_mutex_lock(&mut); log_msg(LOG_DEBUG, "method_conv: waiting for PAM thread"); while(!broadcast_called) pthread_cond_wait(&cond, &mut); broadcast_called = false; /* we need input from the user, quit method and send prompt back */ if (send_v != NULL) { r = sd_varlink_reply(link, send_v); pthread_mutex_unlock(&mut); return r; } pthread_mutex_unlock(&mut); intptr_t *thread_res = NULL; r = pthread_join(pam_thread, (void **)&thread_res); if (r != 0) return return_errno_error(link, "pthread_join", r); if (thread_res != PAM_SUCCESS) { _cleanup_free_ char *error = NULL; int64_t t = (int64_t)thread_res; if (asprintf(&error, "Password change aborted: %s", pam_strerror(NULL, t)) < 0) error = NULL; log_msg(LOG_ERR, "%s", stroom(error)); return sd_varlink_errorbo(link, "org.openSUSE.pwupd.PasswordChangeAborted", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false), SD_JSON_BUILD_PAIR_STRING("ErrorMsg", stroom(error))); } return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true)); } static int vl_method_UpdatePasswdShadow(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { _cleanup_(parameters_free) struct parameters p = { .name = NULL, .shell = NULL, .full_name = NULL, .home_phone = NULL, .other = NULL, .room = NULL, .work_phone = NULL, .old_gecos = NULL, .response = NULL, .flags = 0, .content_passwd = NULL, .content_shadow = NULL, .link = link, }; static const sd_json_dispatch_field dispatch_table[] = { { "passwd", SD_JSON_VARIANT_OBJECT, sd_json_dispatch_variant, offsetof(struct parameters, content_passwd), SD_JSON_NULLABLE }, { "shadow", SD_JSON_VARIANT_OBJECT, sd_json_dispatch_variant, offsetof(struct parameters, content_shadow), SD_JSON_NULLABLE }, {} }; static const sd_json_dispatch_field dispatch_passwd_table[] = { { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_name), SD_JSON_MANDATORY }, { "passwd", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_passwd), SD_JSON_NULLABLE }, { "UID", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, offsetof(struct passwd, pw_uid), SD_JSON_MANDATORY }, { "GID", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, offsetof(struct passwd, pw_gid), SD_JSON_MANDATORY }, { "GECOS", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_gecos), SD_JSON_NULLABLE }, { "dir", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_dir), SD_JSON_NULLABLE }, { "shell", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct passwd, pw_shell), SD_JSON_NULLABLE }, {} }; static const sd_json_dispatch_field dispatch_shadow_table[] = { { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct spwd, sp_namp), SD_JSON_MANDATORY }, { "passwd", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(struct spwd, sp_pwdp), SD_JSON_NULLABLE }, { "lstchg", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd, sp_lstchg), 0 }, { "min", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd, sp_min), 0 }, { "max", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd, sp_max), 0 }, { "warn", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd, sp_warn), 0 }, { "inact", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd, sp_inact), 0 }, { "expire", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd, sp_expire), 0 }, { "flag", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int64, offsetof(struct spwd, sp_flag), 0 }, {} }; _cleanup_(struct_passwd_freep) struct passwd *pw = NULL; _cleanup_(struct_shadow_freep) struct spwd *sp = NULL; uid_t peer_uid; int r; log_msg(LOG_INFO, "Varlink method \"UpdatePasswdShadow\" called..."); r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) return return_errno_error(link, "Get peer UID", r); if (peer_uid != 0) return return_errno_error(link, "UpdatePasswdShadow", -EPERM); r = sd_varlink_dispatch(p.link, parameters, dispatch_table, &p); if (r < 0) return return_errno_error(link, "UpdatePasswdShadow - varlink dispatch", r); if (sd_json_variant_is_null(p.content_passwd)) { log_msg(LOG_ERR, "UpdatePasswdShadow request: no entry found\n"); return 0; } if (!sd_json_variant_is_null(p.content_passwd) && sd_json_variant_elements(p.content_passwd) > 0) { pw = calloc(1, sizeof(struct passwd)); if (pw == NULL) return -ENOMEM; r = sd_json_dispatch(p.content_passwd, dispatch_passwd_table, SD_JSON_ALLOW_EXTENSIONS, pw); if (r < 0) return return_errno_error(link, "Parsing JSON passwd entry", r); } if (!sd_json_variant_is_null(p.content_shadow) && sd_json_variant_elements(p.content_shadow) > 0) { sp = calloc(1, sizeof(struct spwd)); if (sp == NULL) return -ENOMEM; r = sd_json_dispatch(p.content_shadow, dispatch_shadow_table, SD_JSON_ALLOW_EXTENSIONS, sp); if (r < 0) return return_errno_error(link, "Parsing JSON shadow entry", r); } /* Check that pw->pw_name and sp->sp_namp are identical if both are provided */ if (pw && sp && !streq(pw->pw_name, sp->sp_namp)) return return_errno_error(link, "Check for identical account names", -EINVAL); if (pw) { r = update_passwd(pw, NULL); if (r < 0) return return_errno_error(link, "Update of passwd", r); } if (sp) { r = update_shadow(sp, NULL); if (r < 0) return return_errno_error(link, "Update of shadow", r); } return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true)); } static int run_varlink (void) { int r; _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; r = mkdir_p(_VARLINK_PWUPD_SOCKET_DIR, 0755); if (r < 0) { log_msg(LOG_ERR, "Failed to create directory '"_VARLINK_PWUPD_SOCKET_DIR"' for Varlink socket: %s", strerror(-r)); return r; } r = sd_varlink_server_new (&varlink_server, SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA|SD_VARLINK_SERVER_INPUT_SENSITIVE); if (r < 0) { log_msg (LOG_ERR, "Failed to allocate varlink server: %s", strerror (-r)); return r; } r = sd_varlink_server_set_description (varlink_server, "pwupdd"); if (r < 0) { log_msg (LOG_ERR, "Failed to set varlink server description: %s", strerror (-r)); return r; } r = sd_varlink_server_set_info (varlink_server, NULL, PACKAGE" (pwupdd)", VERSION, "https://github.com/thkukuk/pwaccess"); if (r < 0) return r; r = sd_varlink_server_add_interface (varlink_server, &vl_interface_org_openSUSE_pwupd); if (r < 0) { log_msg(LOG_ERR, "Failed to add interface: %s", strerror(-r)); return r; } r = sd_varlink_server_bind_method_many (varlink_server, "org.openSUSE.pwupd.Chauthtok", vl_method_chauthtok, "org.openSUSE.pwupd.Chfn", vl_method_chfn, "org.openSUSE.pwupd.Chsh", vl_method_chsh, "org.openSUSE.pwupd.Conv", vl_method_conv, "org.openSUSE.pwupd.UpdatePasswdShadow", vl_method_UpdatePasswdShadow, "org.openSUSE.pwupd.GetEnvironment", vl_method_get_environment, "org.openSUSE.pwupd.Ping", vl_method_ping, "org.openSUSE.pwupd.Quit", vl_method_quit, "org.openSUSE.pwupd.SetLogLevel", vl_method_set_log_level); if (r < 0) { log_msg(LOG_ERR, "Failed to bind Varlink methods: %s", strerror(-r)); return r; } r = sd_varlink_server_loop_auto(varlink_server); if (r == -EPERM) { log_msg(LOG_ERR, "Invoked by unprivileged Varlink peer, refusing."); return r; } if (r < 0) { log_msg(LOG_ERR, "Failed to run Varlink event loop: %s", strerror(-r)); return r; } return 0; } static void print_help (void) { printf("pwupd - manage updating passwd and shadow entries\n"); printf(" -d, --debug Debug mode\n"); printf(" -v, --verbose Verbose logging\n"); printf(" -?, --help Give this help list\n"); printf(" --version Print program version\n"); } int main (int argc, char **argv) { while (1) { int c; int option_index = 0; static struct option long_options[] = { {"debug", no_argument, NULL, 'd'}, {"verbose", no_argument, NULL, 'v'}, {"version", no_argument, NULL, '\255'}, {"usage", no_argument, NULL, '?'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, '\0'} }; c = getopt_long (argc, argv, "dvh?", long_options, &option_index); if (c == (-1)) break; switch (c) { case 'd': set_max_log_level(LOG_DEBUG); break; case '?': case 'h': print_help (); return 0; case 'v': set_max_log_level(LOG_INFO); break; case '\255': fprintf (stdout, "pwupdd (%s) %s\n", PACKAGE, VERSION); return 0; default: print_help (); return 1; } } argc -= optind; argv += optind; if (argc > 1) { fprintf (stderr, "Try `pwupdd --help' for more information.\n"); return 1; } log_msg (LOG_INFO, "Starting pwupdd (%s) %s...", PACKAGE, VERSION); int r = run_varlink (); if (r < 0) return -r; log_msg (LOG_INFO, "pwupdd stopped."); return 0; } account-utils-1.3.0/src/varlink-org.openSUSE.newidmapd.c000066400000000000000000000062261521474342200231140ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "varlink-org.openSUSE.pwaccess.h" static SD_VARLINK_DEFINE_STRUCT_TYPE(MapRange, SD_VARLINK_FIELD_COMMENT("struct map_range"), SD_VARLINK_DEFINE_FIELD(upper, SD_VARLINK_INT, 0), SD_VARLINK_DEFINE_FIELD(lower, SD_VARLINK_INT, 0), SD_VARLINK_DEFINE_FIELD(count, SD_VARLINK_INT, 0)); static SD_VARLINK_DEFINE_METHOD( WriteMappings, SD_VARLINK_FIELD_COMMENT("PID for which to set the map range"), SD_VARLINK_DEFINE_INPUT(PID, SD_VARLINK_INT, 0), SD_VARLINK_FIELD_COMMENT("Which map to use: 'uid_map' or 'gid_map'"), SD_VARLINK_DEFINE_INPUT(Map, SD_VARLINK_STRING, 0), SD_VARLINK_FIELD_COMMENT("The map ranges"), SD_VARLINK_DEFINE_INPUT_BY_TYPE(MapRanges, MapRange, SD_VARLINK_ARRAY), SD_VARLINK_FIELD_COMMENT("If call succeeded"), SD_VARLINK_DEFINE_OUTPUT(Success, SD_VARLINK_BOOL, 0), SD_VARLINK_FIELD_COMMENT("Error Message"), SD_VARLINK_DEFINE_OUTPUT(ErrorMsg, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( Quit, SD_VARLINK_FIELD_COMMENT("Stop the daemon"), SD_VARLINK_DEFINE_INPUT(ExitCode, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(Success, SD_VARLINK_BOOL, 0)); static SD_VARLINK_DEFINE_METHOD( Ping, SD_VARLINK_FIELD_COMMENT("Check if service is alive"), SD_VARLINK_DEFINE_OUTPUT(Alive, SD_VARLINK_BOOL, 0)); static SD_VARLINK_DEFINE_METHOD( SetLogLevel, SD_VARLINK_FIELD_COMMENT("The maximum log level, using BSD syslog log level integers."), SD_VARLINK_DEFINE_INPUT(Level, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( GetEnvironment, SD_VARLINK_FIELD_COMMENT("Returns the current environment block, i.e. the contents of environ[]."), SD_VARLINK_DEFINE_OUTPUT(Environment, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY)); static SD_VARLINK_DEFINE_ERROR(PermissionDenied); static SD_VARLINK_DEFINE_ERROR(InvalidParameter); static SD_VARLINK_DEFINE_ERROR(InternalError); SD_VARLINK_DEFINE_INTERFACE( org_openSUSE_newidmapd, "org.openSUSE.newidmapd", SD_VARLINK_INTERFACE_COMMENT("newidmapd control APIs"), SD_VARLINK_SYMBOL_COMMENT("Describe map_range entry"), &vl_type_MapRange, SD_VARLINK_SYMBOL_COMMENT("Set map ranges"), &vl_method_WriteMappings, SD_VARLINK_SYMBOL_COMMENT("Stop the daemon"), &vl_method_Quit, SD_VARLINK_SYMBOL_COMMENT("Check if the service is running."), &vl_method_Ping, SD_VARLINK_SYMBOL_COMMENT("Set the maximum log level."), &vl_method_SetLogLevel, SD_VARLINK_SYMBOL_COMMENT("Get current environment block."), &vl_method_GetEnvironment, SD_VARLINK_SYMBOL_COMMENT("Permission Denied"), &vl_error_PermissionDenied, SD_VARLINK_SYMBOL_COMMENT("Invalid parameter for varlink function call"), &vl_error_InvalidParameter, SD_VARLINK_SYMBOL_COMMENT("Internal Error"), &vl_error_InternalError); account-utils-1.3.0/src/varlink-org.openSUSE.newidmapd.h000066400000000000000000000002471521474342200231160ustar00rootroot00000000000000//SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include extern const sd_varlink_interface vl_interface_org_openSUSE_newidmapd; account-utils-1.3.0/src/varlink-org.openSUSE.pwaccess.c000066400000000000000000000157501521474342200227560ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "varlink-org.openSUSE.pwaccess.h" static SD_VARLINK_DEFINE_STRUCT_TYPE(PasswdEntry, SD_VARLINK_FIELD_COMMENT("User's login name"), SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, 0), SD_VARLINK_DEFINE_FIELD(passwd, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(UID, SD_VARLINK_INT, 0), SD_VARLINK_DEFINE_FIELD(GID, SD_VARLINK_INT, 0), SD_VARLINK_DEFINE_FIELD(GECOS, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(dir, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(shell, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_STRUCT_TYPE(ShadowEntry, SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, 0), SD_VARLINK_DEFINE_FIELD(passwd, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(lstchg, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(min, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(max, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(warn, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(inact, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(expire, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(flag, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( GetAccountName, SD_VARLINK_FIELD_COMMENT("The numeric 32bit UNIX UID of the account"), SD_VARLINK_DEFINE_INPUT(uid, SD_VARLINK_INT, 0), SD_VARLINK_FIELD_COMMENT("The account name of the UID."), SD_VARLINK_DEFINE_OUTPUT(userName, SD_VARLINK_STRING, 0), SD_VARLINK_FIELD_COMMENT("If call succeeded"), SD_VARLINK_DEFINE_OUTPUT(Success, SD_VARLINK_BOOL, 0), SD_VARLINK_FIELD_COMMENT("Error Message"), SD_VARLINK_DEFINE_OUTPUT(ErrorMsg, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( GetUserRecord, SD_VARLINK_FIELD_COMMENT("The numeric 32bit UNIX UID of the record, if look-up by UID is desired."), SD_VARLINK_DEFINE_INPUT(uid, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The UNIX user name of the record, if look-up by name is desired."), SD_VARLINK_DEFINE_INPUT(userName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("passwd entry"), SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(passwd, PasswdEntry, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("shadow entry"), SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(shadow, ShadowEntry, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("If all data got replied (depends on UID)"), SD_VARLINK_DEFINE_OUTPUT(Complete, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("If call succeeded"), SD_VARLINK_DEFINE_OUTPUT(Success, SD_VARLINK_BOOL, 0), SD_VARLINK_FIELD_COMMENT("Error Message"), SD_VARLINK_DEFINE_OUTPUT(ErrorMsg, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( GetGroupRecord, SD_VARLINK_FIELD_COMMENT("The numeric 32bit UNIX GID of the record, if look-up by GID is desired."), SD_VARLINK_DEFINE_INPUT(gid, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The UNIX group name of the record, if look-up by name is desired."), SD_VARLINK_DEFINE_INPUT(groupName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( VerifyPassword, SD_VARLINK_FIELD_COMMENT("The account of the user to verify the password."), SD_VARLINK_DEFINE_INPUT(userName, SD_VARLINK_STRING, 0), SD_VARLINK_FIELD_COMMENT("The password of the user to verify."), SD_VARLINK_DEFINE_INPUT(password, SD_VARLINK_STRING, 0), SD_VARLINK_FIELD_COMMENT("If empty password is ok, default false"), SD_VARLINK_DEFINE_INPUT(nullOK, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( ExpiredCheck, SD_VARLINK_FIELD_COMMENT("The account to check if expired."), SD_VARLINK_DEFINE_INPUT(userName, SD_VARLINK_STRING, 0)); static SD_VARLINK_DEFINE_METHOD( Quit, SD_VARLINK_FIELD_COMMENT("Stop the daemon"), SD_VARLINK_DEFINE_INPUT(ExitCode, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(Success, SD_VARLINK_BOOL, 0)); static SD_VARLINK_DEFINE_METHOD( Ping, SD_VARLINK_FIELD_COMMENT("Check if service is alive"), SD_VARLINK_DEFINE_OUTPUT(Alive, SD_VARLINK_BOOL, 0)); static SD_VARLINK_DEFINE_METHOD( SetLogLevel, SD_VARLINK_FIELD_COMMENT("The maximum log level, using BSD syslog log level integers."), SD_VARLINK_DEFINE_INPUT(Level, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( GetEnvironment, SD_VARLINK_FIELD_COMMENT("Returns the current environment block, i.e. the contents of environ[]."), SD_VARLINK_DEFINE_OUTPUT(Environment, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY)); static SD_VARLINK_DEFINE_ERROR(NoEntryFound); static SD_VARLINK_DEFINE_ERROR(InvalidParameter); static SD_VARLINK_DEFINE_ERROR(InternalError); SD_VARLINK_DEFINE_INTERFACE( org_openSUSE_pwaccess, "org.openSUSE.pwaccess", SD_VARLINK_INTERFACE_COMMENT("PWAccessD control APIs"), SD_VARLINK_SYMBOL_COMMENT("Describe passwd entry"), &vl_type_PasswdEntry, SD_VARLINK_SYMBOL_COMMENT("Describe shadow entry"), &vl_type_ShadowEntry, SD_VARLINK_SYMBOL_COMMENT("Get account name for UID"), &vl_method_GetAccountName, SD_VARLINK_SYMBOL_COMMENT("Get user entries from passwd and shadow"), &vl_method_GetUserRecord, SD_VARLINK_SYMBOL_COMMENT("Get group entries from group and gshadow"), &vl_method_GetGroupRecord, SD_VARLINK_SYMBOL_COMMENT("Verify password of account"), &vl_method_VerifyPassword, SD_VARLINK_SYMBOL_COMMENT("Check if account is expired"), &vl_method_ExpiredCheck, SD_VARLINK_SYMBOL_COMMENT("Stop the daemon"), &vl_method_Quit, SD_VARLINK_SYMBOL_COMMENT("Check if the service is running."), &vl_method_Ping, SD_VARLINK_SYMBOL_COMMENT("Set the maximum log level."), &vl_method_SetLogLevel, SD_VARLINK_SYMBOL_COMMENT("Get current environment block."), &vl_method_GetEnvironment, SD_VARLINK_SYMBOL_COMMENT("No entry found"), &vl_error_NoEntryFound, SD_VARLINK_SYMBOL_COMMENT("Invalid parameter for varlink function call"), &vl_error_InvalidParameter, SD_VARLINK_SYMBOL_COMMENT("Internal Error"), &vl_error_InternalError); account-utils-1.3.0/src/varlink-org.openSUSE.pwaccess.h000066400000000000000000000002461521474342200227550ustar00rootroot00000000000000//SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include extern const sd_varlink_interface vl_interface_org_openSUSE_pwaccess; account-utils-1.3.0/src/varlink-org.openSUSE.pwupd.c000066400000000000000000000154601521474342200223030ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "varlink-org.openSUSE.pwupd.h" static SD_VARLINK_DEFINE_STRUCT_TYPE(PasswdEntry, SD_VARLINK_FIELD_COMMENT("User's login name"), SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, 0), SD_VARLINK_DEFINE_FIELD(passwd, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(UID, SD_VARLINK_INT, 0), SD_VARLINK_DEFINE_FIELD(GID, SD_VARLINK_INT, 0), SD_VARLINK_DEFINE_FIELD(GECOS, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(dir, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(shell, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_STRUCT_TYPE(ShadowEntry, SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, 0), SD_VARLINK_DEFINE_FIELD(passwd, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(lstchg, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(min, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(max, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(warn, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(inact, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(expire, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_FIELD(flag, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( Chauthtok, SD_VARLINK_FIELD_COMMENT("The account of the user to change the password."), SD_VARLINK_DEFINE_INPUT(userName, SD_VARLINK_STRING, 0), SD_VARLINK_DEFINE_INPUT(flags, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( Chfn, SD_VARLINK_FIELD_COMMENT("The account of the user to change the GECOS information."), SD_VARLINK_DEFINE_INPUT(userName, SD_VARLINK_STRING, 0), SD_VARLINK_FIELD_COMMENT("The new full name."), SD_VARLINK_DEFINE_INPUT(fullName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The new room number."), SD_VARLINK_DEFINE_INPUT(room, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The new work phone number."), SD_VARLINK_DEFINE_INPUT(workPhone, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The new private phone number."), SD_VARLINK_DEFINE_INPUT(homePhone, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The new other field."), SD_VARLINK_DEFINE_INPUT(other, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( Chsh, SD_VARLINK_FIELD_COMMENT("The account of the user to change the shell."), SD_VARLINK_DEFINE_INPUT(userName, SD_VARLINK_STRING, 0), SD_VARLINK_FIELD_COMMENT("The new shell of the user."), SD_VARLINK_DEFINE_INPUT(shell, SD_VARLINK_STRING, 0)); static SD_VARLINK_DEFINE_METHOD( Conv, SD_VARLINK_FIELD_COMMENT("Response for PAM_PROMPT_ECHO_*."), SD_VARLINK_DEFINE_INPUT(response, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( UpdatePasswdShadow, SD_VARLINK_FIELD_COMMENT("Update passwd and shadow entries."), SD_VARLINK_FIELD_COMMENT("passwd entry"), SD_VARLINK_DEFINE_INPUT_BY_TYPE(passwd, PasswdEntry, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("shadow entry"), SD_VARLINK_DEFINE_INPUT_BY_TYPE(shadow, ShadowEntry, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( Quit, SD_VARLINK_FIELD_COMMENT("Stop the daemon"), SD_VARLINK_DEFINE_INPUT(ExitCode, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(Success, SD_VARLINK_BOOL, 0)); static SD_VARLINK_DEFINE_METHOD( Ping, SD_VARLINK_FIELD_COMMENT("Check if service is alive"), SD_VARLINK_DEFINE_OUTPUT(Alive, SD_VARLINK_BOOL, 0)); static SD_VARLINK_DEFINE_METHOD( SetLogLevel, SD_VARLINK_FIELD_COMMENT("The maximum log level, using BSD syslog log level integers."), SD_VARLINK_DEFINE_INPUT(Level, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( GetEnvironment, SD_VARLINK_FIELD_COMMENT("Returns the current environment block, i.e. the contents of environ[]."), SD_VARLINK_DEFINE_OUTPUT(Environment, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY)); static SD_VARLINK_DEFINE_ERROR(NoEntryFound); static SD_VARLINK_DEFINE_ERROR(InvalidParameter); static SD_VARLINK_DEFINE_ERROR(InternalError); static SD_VARLINK_DEFINE_ERROR(AuthenticationFailed); static SD_VARLINK_DEFINE_ERROR(InvalidShell); static SD_VARLINK_DEFINE_ERROR(PasswordChangeAborted); static SD_VARLINK_DEFINE_ERROR(PermissionDenied); SD_VARLINK_DEFINE_INTERFACE( org_openSUSE_pwupd, "org.openSUSE.pwupd", SD_VARLINK_INTERFACE_COMMENT("PWUpdD control APIs"), SD_VARLINK_SYMBOL_COMMENT("Describe passwd entry"), &vl_type_PasswdEntry, SD_VARLINK_SYMBOL_COMMENT("Describe shadow entry"), &vl_type_ShadowEntry, SD_VARLINK_SYMBOL_COMMENT("Change password via PAM module"), &vl_method_Chauthtok, SD_VARLINK_SYMBOL_COMMENT("Change GECOS information of account"), &vl_method_Chfn, SD_VARLINK_SYMBOL_COMMENT("Change shell of account"), &vl_method_Chsh, SD_VARLINK_SYMBOL_COMMENT("Provide response for PAM_PROMPT_ECHO_*"), &vl_method_Conv, SD_VARLINK_SYMBOL_COMMENT("Update passwd and/or shadow file"), &vl_method_UpdatePasswdShadow, SD_VARLINK_SYMBOL_COMMENT("Stop the daemon"), &vl_method_Quit, SD_VARLINK_SYMBOL_COMMENT("Check if the service is running"), &vl_method_Ping, SD_VARLINK_SYMBOL_COMMENT("Set the maximum log level"), &vl_method_SetLogLevel, SD_VARLINK_SYMBOL_COMMENT("Get current environment block"), &vl_method_GetEnvironment, SD_VARLINK_SYMBOL_COMMENT("Authentication failure"), &vl_error_AuthenticationFailed, SD_VARLINK_SYMBOL_COMMENT("No entry found"), &vl_error_NoEntryFound, SD_VARLINK_SYMBOL_COMMENT("Invalid parameter for varlink function call"), &vl_error_InvalidParameter, SD_VARLINK_SYMBOL_COMMENT("Invalid shell"), &vl_error_InvalidShell, SD_VARLINK_SYMBOL_COMMENT("Password change aborted"), &vl_error_PasswordChangeAborted, SD_VARLINK_SYMBOL_COMMENT("Permission denied"), &vl_error_PermissionDenied, SD_VARLINK_SYMBOL_COMMENT("Internal Error"), &vl_error_InternalError); account-utils-1.3.0/src/varlink-org.openSUSE.pwupd.h000066400000000000000000000002431521474342200223010ustar00rootroot00000000000000//SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include extern const sd_varlink_interface vl_interface_org_openSUSE_pwupd; account-utils-1.3.0/src/varlink-service-common.c000066400000000000000000000113641521474342200216430ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include "basics.h" #include "varlink-service-common.h" #include "context.h" static int log_level = LOG_WARNING; void set_max_log_level(int level) { log_level = level; } void log_msg(int priority, const char *fmt, ...) { static int is_tty = -1; if (priority > log_level) return; if (is_tty == -1) is_tty = isatty(STDOUT_FILENO); va_list ap; va_start(ap, fmt); if (is_tty) { if (priority <= LOG_ERR) { vfprintf(stderr, fmt, ap); fputc('\n', stderr); } else { vprintf(fmt, ap); putchar('\n'); } } else sd_journal_printv(priority, fmt, ap); va_end(ap); } int vl_method_ping(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { int r; log_msg(LOG_INFO, "Varlink method \"Ping\" called..."); r = sd_varlink_dispatch(link, parameters, NULL, NULL); if (r != 0) return r; return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Alive", true)); } int vl_method_set_log_level(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { static const sd_json_dispatch_field dispatch_table[] = { { "Level", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, 0, SD_JSON_MANDATORY }, {} }; int r, level; log_msg(LOG_INFO, "Varlink method \"SetLogLevel\" called..."); r = sd_varlink_dispatch(link, parameters, dispatch_table, &level); if (r != 0) return r; log_msg(LOG_DEBUG, "Log level %i requested", level); uid_t peer_uid; r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) { log_msg(LOG_ERR, "Failed to get peer UID: %s", strerror(-r)); return r; } if (peer_uid != 0) { log_msg(LOG_WARNING, "SetLogLevel: peer UID %i denied", peer_uid); return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, parameters); } set_max_log_level(level); log_msg(LOG_INFO, "New log setting: level=%i", level); return sd_varlink_reply(link, NULL); } int vl_method_get_environment(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void _unused_(*userdata)) { int r; log_msg(LOG_INFO, "Varlink method \"GetEnvironment\" called..."); uid_t peer_uid; r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) { log_msg(LOG_ERR, "Failed to get peer UID: %s", strerror(-r)); return r; } if (peer_uid != 0) { log_msg(LOG_WARNING, "GetEnvironment: peer UID %i denied", peer_uid); return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, parameters); } r = sd_varlink_dispatch(link, parameters, NULL, NULL); if (r != 0) return r; #if 0 /* XXX */ for (char **e = environ; *e != 0; e++) { if (!env_assignment_is_valid(*e)) goto invalid; if (!utf8_is_valid(*e)) goto invalid; } #endif return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("Environment", environ)); #if 0 invalid: return sd_varlink_error(link, "io.systemd.service.InconsistentEnvironment", parameters); #endif } int vl_method_quit(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t _unused_(flags), void *userdata) { static const sd_json_dispatch_field dispatch_table[] = { { "ExitCode", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int, 0, 0 }, {} }; uid_t peer_uid; struct context_t *ctx = userdata; int exit_code = 0; int r; log_msg(LOG_INFO, "Varlink method \"Quit\" called..."); r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) { log_msg(LOG_ERR, "Failed to get peer UID: %s", strerror(-r)); return r; } if (peer_uid != 0) { log_msg(LOG_WARNING, "Quit: peer UID %i denied", peer_uid); return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, parameters); } r = sd_varlink_dispatch(link, parameters, dispatch_table, &exit_code); if (r != 0) { log_msg (LOG_ERR, "Quit request: varlink dispatch failed: %s", strerror(-r)); return r; } /* exit code must be negative, systemd will convert that to a positive value */ if (exit_code > 0) exit_code = -exit_code; r = sd_event_exit(ctx->loop, exit_code); if (r != 0) { log_msg(LOG_ERR, "Quit request: disabling event loop failed: %s", strerror(-r)); return sd_varlink_errorbo(link, "org.openSUSE.pwaccess.InternalError", SD_JSON_BUILD_PAIR_BOOLEAN("Success", false)); } return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_BOOLEAN("Success", true)); } account-utils-1.3.0/src/varlink-service-common.h000066400000000000000000000013771521474342200216530ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include extern void log_msg (int priority, const char *fmt, ...) __attribute__ ((__format__ (__printf__, 2, 3))); extern void set_max_log_level (int level); extern int vl_method_ping(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); extern int vl_method_set_log_level(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); extern int vl_method_get_environment(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); extern int vl_method_quit(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); account-utils-1.3.0/tests/000077500000000000000000000000001521474342200154535ustar00rootroot00000000000000account-utils-1.3.0/tests/ci/000077500000000000000000000000001521474342200160465ustar00rootroot00000000000000account-utils-1.3.0/tests/ci/IMPLEMENTATION.md000066400000000000000000000173201521474342200205200ustar00rootroot00000000000000# Integration Test Framework Implementation Summary ## Overview A comprehensive integration test framework has been implemented for the account-utils project, enabling safe testing of systemd socket-activated services (`pwaccessd` and `pwupdd`) that modify system files (`/etc/passwd` and `/etc/shadow`). ## Solution Architecture ### Technology Choice: systemd-nspawn - **Full systemd support**: Native socket activation exactly as in production - **Lightweight**: 2-5 second startup vs 30+ seconds for VMs - **Complete isolation**: Separate /etc, /run, /var directories - **Easy debugging**: Can enter and inspect running containers - **No special setup**: Part of standard systemd installation - **Same kernel**: No emulation overhead ## Components Delivered ### 1. Core Infrastructure (3 files) **setup-container.sh** - Creates minimal systemd-nspawn container - Copies systemd, essential binaries, and dependencies - Installs built services and libraries - Configures systemd units and PAM - Sets up test user database **run-tests.sh** - Main test orchestrator - Handles container lifecycle - Executes test suites - Supports selective test execution - Cleanup and error handling **test-utils.sh** - Logging utilities (info, warn, error, test) - Container execution helpers - Service/socket wait functions - User management functions - Assertion framework (equals, contains, success, failure) - Test summary reporting ### 2. Test Suites **test-pwaccessd.sh** - Socket activation mechanism - Socket permissions and security - User lookup functionality (root and regular users) - Multiple concurrent user lookups - Passwd/shadow file integrity - Service logging verification - Socket restart resilience **test-pwupdd.sh** - Socket activation mechanism - Accept=yes socket configuration (inetd-style) - Socket permissions and security - Socket directory structure - MaxConnectionsPerSource limits - Concurrent connection handling - Password change infrastructure setup - Shell change infrastructure setup - GECOS modification infrastructure setup - Service binary verification - Varlink protocol readiness - Service restart resilience - Service logging verification **test-pam.sh** - PAM module installation verification - PAM module loadability (dlopen test) - PAM module dependencies (library linking) **test-chfn.sh** - Binary existence and help/version options - Test user creation and pwupdd socket readiness - All GECOS field options (-f, -r, -w, -h, -o) - Multiple field changes in single command - Clearing GECOS fields - Special characters in fields - Spaces in field values - Empty initial GECOS handling - Long field value handling - Various phone number formats - Different user modifications - GECOS structure integrity (5-field format) - Non-existent user error handling - pwaccess socket verification - Sequential field changes - User cleanup **test-chage.sh** - Binary existence and help/version options - Test user creation and pwupdd socket readiness - All command-line options (-m, -M, -W, -I, -E, -d, -l) - Minimum password age setting - Maximum password age setting - Password warning period - Password inactivity period - Account expiration date - Last password change date - Multiple option combinations - Disabled values (-1 handling) - Expiration date "never" value - List mode (-l) for displaying settings - Zero values handling - Different user modifications - Shadow structure integrity (9-field format) - Non-existent user error handling - pwaccess socket verification - Sequential shadow field changes - Shadow file security (permissions) - User cleanup **test-chsh.sh** - Binary existence and help/version options - Test user creation and pwupdd socket readiness - Shell change operations (-s) - Preservation of other passwd fields - Absolute path handling - Different user modifications - List shells option (-l) - Valid shell verification - Passwd structure integrity (7-field format) - Sequential shell changes - Shell absolute path requirements - Independent shell changes per user - Common shells existence (/bin/bash, /bin/sh, etc.) - Non-existent user error handling - pwaccess socket verification - Root user shell changes - User cleanup **test-expiry.sh** - Binary existence and help/version options - Test user creation and pwaccess socket readiness - Check mode for non-expired accounts - Check mode for specific users - Check mode for expired passwords - Account expiration detection - Force password change option (-f) - Option conflict detection - Argument validation - Too many arguments error handling - Non-existent user error handling - Multiple user expiration scenarios - Shadow field dependencies (lstchg, max, expire, inact) - Recent password scenarios - Maximum password age scenarios - Warning period scenarios - Root user expiration checks - User cleanup **test-passwd.sh** - Binary existence and help/version options - Test user creation and pwupdd socket readiness - Password reading from stdin - Lock password operation (-l) - Unlock password operation (-u) - Delete password operation (-d) - Expire password operation (-e) - Status display operation (-S) - Lock/unlock cycle testing - Different user password operations - Sequential password operations - Expire then change scenarios - Delete then lock scenarios - Independent password changes per user - Non-existent user error handling - pwaccess socket verification - User cleanup **test-varlink-protocol.sh** - Varlink socket availability verification - Socket security and permissions - Socket file descriptor names - Protocol test infrastructure readiness - Varlink GetInfo method support - Concurrent connection infrastructure - GetUserEntry method infrastructure - VerifyPassword method infrastructure - ChangePassword method infrastructure - ChangeShell method infrastructure - ChangeGECOS method infrastructure - Error handling infrastructure - Concurrent connection testing infrastructure ### 3. Documentation (4 files) **README.md** - Architecture overview and component description **TESTING.md** - Comprehensive testing guide with debugging tips **QUICKSTART.md** - Quick start guide for immediate use **IMPLEMENTATION.md** - This file, implementation summary ## Usage Examples ### Basic Usage ```bash cd tests/ci sudo ./run-tests.sh ``` ### Selective Testing ```bash sudo ./run-tests.sh test-pwaccessd # Only pwaccessd tests ``` ### Debugging ```bash sudo ./run-tests.sh --keep-container sudo systemd-nspawn -D /tmp/account-utils-test-container ``` ### CI/CD Integration ```yaml # GitHub Actions - name: Integration tests run: | cd tests/ci sudo ./run-tests.sh ``` ## How It Works ### Workflow 1. **Build Phase**: Project built with meson/ninja 2. **Setup Phase**: - Create container directory structure - Copy systemd and essential binaries - Install service binaries and libraries - Configure systemd units - Set up PAM configuration 3. **Boot Phase**: - Start container with systemd - Wait for systemd initialization - Verify socket activation 4. **Test Phase**: - Execute test suites - Each test uses assertion framework - Results collected and reported 5. **Cleanup Phase**: - Terminate container - Remove container directory (optional) ### Container Structure ``` /tmp/account-utils-test-container/ ├── etc/ │ ├── passwd, shadow, group # Test user database │ ├── pam.d/ # PAM configurations │ └── account-utils/ # Service configs ├── usr/ │ ├── bin/ # Service binaries │ ├── lib/ # Libraries │ │ └── security/ # PAM modules │ └── lib/systemd/system/ # Unit files ├── run/account/ # Socket directory └── var/log/ # Service logs ``` account-utils-1.3.0/tests/ci/QUICKSTART.md000066400000000000000000000176321521474342200200730ustar00rootroot00000000000000# Integration Testing Quick Start ## TL;DR ```bash # Build the project meson setup build meson compile -C build # Run all integration tests (requires root) cd tests/ci sudo ./run-tests.sh ``` ## What This Does 1. Creates an isolated systemd-nspawn container in a secure temporary directory 2. Installs your built services (`pwaccessd`, `pwupdd`) into the container 3. Starts the container with systemd 4. Activates the socket-activated services 5. Runs comprehensive tests on: - Socket activation - User management - Password/shadow file modifications - PAM integration 6. Reports pass/fail for each test 7. Cleans up the container ## Expected Output ``` [INFO] Setting up test container at: /tmp/account-utils-test.AbC123XyZ [INFO] Creating container directory structure [INFO] Installing account-utils binaries and libraries [INFO] Container setup complete [INFO] Starting container: account-utils-test [INFO] Waiting for container to boot [INFO] Services are ready [TEST] Running: test_socket_activation [INFO] ✓ pwaccessd socket exists ... ========================================= Test Summary ========================================= Total tests run: 45 Tests passed: 45 Tests failed: 0 ========================================= All tests passed! ``` ## Run Specific Tests ```bash # Test only pwaccessd sudo ./run-tests.sh test-pwaccessd # Test only pwupdd sudo ./run-tests.sh test-pwupdd # Test only PAM integration sudo ./run-tests.sh test-pam # Test only chfn utility sudo ./run-tests.sh test-chfn # Test only chage utility sudo ./run-tests.sh test-chage # Test only chsh utility sudo ./run-tests.sh test-chsh # Test only expiry utility sudo ./run-tests.sh test-expiry # Test only passwd utility sudo ./run-tests.sh test-passwd # Test varlink protocol sudo ./run-tests.sh test-varlink-protocol ``` ## Debugging Failed Tests ### Keep the container after test failure ```bash sudo ./run-tests.sh --keep-container ``` ### Enter the container The container path will be shown in the output. Use that path: ```bash # From the test output, find the container path, then: sudo systemd-nspawn -D /tmp/account-utils-test.XXXXXXXXXX # Or use the container name: sudo machinectl shell account-utils-test-PID-TIMESTAMP ``` ### Check service status ```bash # Start container sudo systemd-nspawn -D /tmp/account-utils-test-container -b # In another terminal sudo machinectl shell account-utils-test # Inside container: systemctl status pwaccessd.socket systemctl status pwupdd.socket journalctl -u pwaccessd.socket ls -la /run/account/ cat /etc/passwd ``` ### View service logs ```bash sudo machinectl shell account-utils-test /bin/bash journalctl -u pwaccessd.socket --no-pager journalctl -u pwupdd.socket --no-pager ``` ## Common Issues ### "Container failed to start" **Cause**: Missing systemd or library dependencies **Fix**: Check the setup-container.sh script copied all required libraries ```bash sudo systemd-nspawn -D /tmp/account-utils-test-container # Try to start systemd manually to see errors /usr/lib/systemd/systemd ``` ### "Socket not ready" **Cause**: Service binary missing dependencies or unit files not installed **Fix**: Check service binaries and unit files ```bash sudo systemd-nspawn -D /tmp/account-utils-test-container ls -la /usr/bin/pwaccessd /usr/bin/pwupdd ls -la /usr/lib/systemd/system/*.socket ldd /usr/bin/pwaccessd ``` ### "Permission denied" **Cause**: Not running as root **Fix**: All integration tests must run as root (for systemd-nspawn and passwd/shadow access) ```bash sudo ./run-tests.sh # Not just ./run-tests.sh ``` ### Build not found **Cause**: Project not built yet **Fix**: Build the project first ```bash cd /path/to/account-utils meson setup build ninja -C build ``` ## Requirements - Linux with systemd - systemd-nspawn (part of systemd package) - Root access - Built project binaries - ~50MB disk space for container ## Test Coverage ### pwaccessd Tests (10 tests) - Socket activation mechanism - Socket permissions (0666) - User lookup functionality - Passwd file integrity - Shadow file security (600, root-owned) - Service logging - Socket restart resilience ### pwupdd Tests (13 tests) - Accept=yes socket configuration - Socket permissions - MaxConnectionsPerSource limits - Password change infrastructure - Shell modification setup - GECOS field access - Concurrent connection handling - Service binary availability ### PAM Tests (11 tests) - PAM module installation - Module loadability - Configuration correctness - Service dependencies - User authentication setup - Multiple user handling - Configuration file verification ### chfn Tests (25 tests) - Binary availability and options - All command-line options (-f, -r, -w, -h, -o) - GECOS field structure and parsing - Varlink integration with pwupdd - PAM authentication requirements - Special characters handling - Field length limits - Multi-user scenarios ### chage Tests (33 tests) - Binary availability and options - All command-line options (-d, -E, -I, -i, -l, -m, -M, -W) - Shadow field structure and parsing (9 fields) - Date format validation (YYYY-MM-DD) - Numeric value parsing - Special value -1 (never/disabled) - Varlink integration with pwupdd - Root privilege requirements - Interactive mode - Multi-user scenarios ### chsh Tests (33 tests) - Binary availability and options - All command-line options (-s, -l, -h, -v) - Passwd shell field (field 7) - Shell list configuration (vendordir/shells) - Shell validation and allowed lists - Common shell paths - Varlink integration with pwupdd - PAM authentication requirements - Interactive mode - Permission model - Multi-user scenarios ### expiry Tests (33 tests) - Binary availability and options - All command-line options (-c, -f, -h, -v) - Password expiration checking - Account vs password expiration - Check mode (-c) - days until expiration - Force mode (-f) - force password change - pwaccess integration (check_expired method) - PAM integration (chauthtok) - Expiration status types and messages - Shadow field dependency - Permission model - Multi-user scenarios ### passwd Tests (40 tests) - Binary availability and options - All command-line options (-d, -e, -I, -k, -l, -m, -M, -q, -s, -S, -u, -w) - Password change operations - Delete, expire, lock, unlock passwords - Status display (P/NP/L codes) - Stdin password reading - Shadow field modifications - pwupdd and pwaccess integration - PAM integration and fallback - Quiet and keep-tokens modes - Permission model - Multi-user scenarios ### Varlink Protocol Tests (7 tests) - Socket accessibility - Security attributes - FileDescriptor naming - Protocol test infrastructure - Concurrent connection readiness - Service instance lifecycle **Total: 205 tests** across 9 test suites ## Next Steps 1. Run the tests: `sudo ./run-tests.sh` 2. Check the output for any failures 3. Read TESTING.md for detailed documentation 4. Add custom tests by creating test-*.sh files 5. Integrate into your CI/CD pipeline ## CI/CD Integration ### GitHub Actions ```yaml - name: Run integration tests run: | cd tests/ci sudo ./run-tests.sh ``` ### GitLab CI ```yaml integration-tests: script: - cd tests/ci - ./run-tests.sh # Run as root in CI ``` ## Files Overview ``` tests/ci/ ├── README.md # Architecture and detailed docs ├── TESTING.md # Comprehensive testing guide ├── QUICKSTART.md # This file ├── run-tests.sh # Main test runner ├── setup-container.sh # Container setup script ├── test-utils.sh # Common test utilities ├── test-pwaccessd.sh # pwaccessd test suite ├── test-pwupdd.sh # pwupdd test suite ├── test-pam.sh # PAM integration tests └── test-varlink-protocol.sh # Varlink protocol tests ``` ## Getting Help - Read the full documentation: `TESTING.md` - Check test utilities: `cat test-utils.sh` - View test examples: `cat test-pwaccessd.sh` account-utils-1.3.0/tests/ci/README.md000066400000000000000000000073771521474342200173430ustar00rootroot00000000000000# Integration Test Framework This directory contains an integration test framework for testing `pwaccessd` and `pwupdd` services in isolated systemd-nspawn containers. ## Architecture The test framework uses **systemd-nspawn** containers to provide: - Full systemd support for socket activation - Isolated /etc/passwd and /etc/shadow files - Minimal overhead compared to VMs - Clean environment for each test run - Secure temporary directories (unique per run) - Support for parallel test execution ## Components ### 1. Container Setup (`setup-container.sh`) Creates a minimal systemd-nspawn container in a secure temporary directory with: - Base system files (passwd, shadow, group, etc.) - Systemd units for socket activation - Test binaries and libraries - Restricted permissions (mode 700) - Unique path per test run (supports parallel execution) ### 2. Test Runner (`run-tests.sh`) Orchestrates test execution: - Builds the project - Sets up the container environment - Executes individual test suites - Collects and reports results - Cleans up containers ### 3. Test Suites Individual test scripts for each service and utility: - `test-pwaccessd.sh` - Tests for pwaccessd service - `test-pwupdd.sh` - Tests for pwupdd service - `test-pam.sh` - Tests for PAM module integration - `test-chfn.sh` - Tests for chfn utility (change finger information) - `test-chage.sh` - Tests for chage utility (change password aging) - `test-chsh.sh` - Tests for chsh utility (change login shell) - `test-expiry.sh` - Tests for expiry utility (check password expiration) - `test-passwd.sh` - Tests for passwd utility (change user password) - `test-varlink-protocol.sh` - Tests for varlink protocol infrastructure ### 4. Test Utilities (`test-utils.sh`) Common functions for: - Container management - Service control - Result verification - Logging ## Requirements - systemd-nspawn (part of systemd) - Root privileges (for container creation and passwd/shadow modification) - meson/ninja for building the project ## Usage ### Run all tests: ```bash sudo ./run-tests.sh ``` ### Run specific test suite: ```bash sudo ./run-tests.sh test-pwaccessd ``` ### Keep container for debugging: ```bash sudo ./run-tests.sh --keep-container ``` ### Enter test container manually: ```bash # Use the container path shown in test output sudo systemd-nspawn -D /tmp/account-utils-test.XXXXXXXXXX --boot ``` ## Test Workflow 1. Build project in host system 2. Create fresh container with minimal base system 3. Install built binaries and libraries into container 4. Start container with systemd 5. Execute tests that interact with services via sockets 6. Verify passwd/shadow modifications 7. Collect results and logs 8. Clean up container (unless --keep-container specified) ## Writing Tests Test scripts should: 1. Source `test-utils.sh` for common functions 2. Set up test users if needed 3. Start required services 4. Execute test operations via varlink sockets 5. Verify expected outcomes 6. Clean up test data 7. Report PASS/FAIL results Example: ```bash #!/bin/bash source "$(dirname "$0")/test-utils.sh" test_password_verification() { # Create test user create_test_user "testuser" "testpass" # Verify password via pwaccessd result=$(verify_password "testuser" "testpass") assert_equals "$result" "success" } run_test test_password_verification ``` ## Container Structure ``` container-root/ ├── etc/ │ ├── passwd │ ├── shadow │ ├── group │ ├── pam.d/ │ └── account-utils/ ├── usr/ │ ├── bin/ # Test utilities │ ├── lib/ # Libraries │ └── lib/systemd/system/ # Unit files ├── run/ │ └── account/ # Socket directory └── var/ └── log/ # Service logs ``` account-utils-1.3.0/tests/ci/TESTING.md000066400000000000000000000205161521474342200175110ustar00rootroot00000000000000# Integration Testing Guide ## Overview The integration test framework tests `pwaccessd` and `pwupdd` services in isolated systemd-nspawn containers. Each test run creates a unique, secure temporary directory to ensure parallel test execution is safe and to prevent security issues from predictable paths. ## Why systemd-nspawn? - **Full systemd support**: Services can be socket-activated exactly as in production - **Lightweight**: Much faster than full VMs - **Isolation**: Complete filesystem isolation, safe for passwd/shadow modifications - **Native systemd**: Same activation mechanism as production - **Easy debugging**: Can enter containers and inspect state ## Quick Start ### Prerequisites 1. Root access (required for systemd-nspawn and passwd/shadow modifications) 2. systemd with nspawn support 3. Built project (`meson setup build && ninja -C build`) ### Run All Tests ```bash cd tests/ci sudo ./run-tests.sh ``` ### Run Specific Test Suite ```bash sudo ./run-tests.sh test-pwaccessd sudo ./run-tests.sh test-pwupdd sudo ./run-tests.sh test-pam ``` ### Keep Container for Debugging ```bash sudo ./run-tests.sh --keep-container ``` ## Test Suites ### 1. pwaccessd Tests (`test-pwaccessd.sh`) Tests the socket-activated read-only service: - Socket activation and permissions - User lookup functionality - Passwd/shadow file access - Service logging - Socket restart resilience - File integrity checks ### 2. pwupdd Tests (`test-pwupdd.sh`) Tests the inetd-style write service: - Accept=yes socket configuration - Concurrent connection handling - Password change capabilities - Shell modification setup - GECOS field access - MaxConnectionsPerSource limits ### 3. PAM Module Tests (`test-pam.sh`) Tests the PAM integration: - Module installation and loadability - PAM configuration correctness - Service dependencies - User authentication setup - Multiple user handling ## Architecture Details ### Container Structure Each test run creates a unique, secure temporary directory: ``` /tmp/account-utils-test.XXXXXXXXXX/ # Unique per test run ├── etc/ │ ├── passwd # Test passwd file │ ├── shadow # Test shadow file (mode 600) │ ├── group # Test group file │ ├── pam.d/ # PAM configurations │ │ ├── system-auth │ │ └── passwd │ └── account-utils/ # Service configurations ├── usr/ │ ├── bin/ # Service binaries (pwaccessd, pwupdd) │ ├── lib/ # Shared libraries │ │ └── security/ # PAM modules │ └── lib/systemd/system/ # Unit files ├── run/ │ └── account/ # Socket directory │ ├── pwaccess-socket │ └── pwupd-socket └── var/log/ # Service logs ``` ### Test Workflow 1. **Build Phase**: Compile project with meson/ninja 2. **Setup Phase**: - Create minimal container filesystem - Copy systemd and essential binaries - Install built services and libraries - Configure systemd units - Set up PAM configurations 3. **Boot Phase**: - Start container with systemd - Wait for systemd initialization - Verify socket activation 4. **Test Phase**: - Execute test suites - Each test suite runs independently - Tests interact with services via sockets 5. **Cleanup Phase**: - Terminate container - Remove container directory (unless --keep-container) ## Writing Custom Tests ### Test Script Template ```bash #!/bin/bash SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" log_info "=====================================" log_info "My Custom Test Suite" log_info "=====================================" test_something() { log_test "Testing something specific" # Setup create_test_user "testuser" "testpass" # Execute local result=$(container_exec some_command) # Assert assert_equals "$result" "expected" "Description" # Cleanup delete_test_user "testuser" } # Run tests run_test test_something # Print summary print_summary ``` ### Available Utilities (test-utils.sh) #### Logging - `log_info "message"` - Info message (green) - `log_warn "message"` - Warning message (yellow) - `log_error "message"` - Error message (red) - `log_test "message"` - Test message (yellow) #### Container Operations - `container_exec command [args...]` - Execute command as root in container - `container_exec_user username command [args...]` - Execute as specific user - `wait_for_service servicename [timeout]` - Wait for systemd service - `wait_for_socket socketpath [timeout]` - Wait for Unix socket #### User Management - `create_test_user username password [uid]` - Create test user in container - `delete_test_user username` - Remove test user from container #### Assertions - `assert_equals actual expected [message]` - Assert equality - `assert_not_equals actual expected [message]` - Assert inequality - `assert_success exit_code [message]` - Assert command succeeded - `assert_failure exit_code [message]` - Assert command failed - `assert_contains haystack needle [message]` - Assert substring present #### Test Management - `run_test test_function_name` - Execute a test function - `print_summary` - Print test results summary - `cleanup_test_users` - Remove all standard test users ## Debugging Failed Tests ### View Container Logs ```bash # Keep container after test sudo ./run-tests.sh --keep-container test-pwaccessd # Enter container sudo systemd-nspawn -D /tmp/account-utils-test-container # Inside container: journalctl -u pwaccessd.socket journalctl -u pwupdd.socket systemctl status pwaccessd.socket systemctl status pwupdd.socket ``` ### Manual Container Testing ```bash # Setup and start container sudo ./setup-container.sh sudo systemd-nspawn -D /tmp/account-utils-test-container -b # In another terminal, execute commands: sudo machinectl shell account-utils-test /bin/bash # Inside container: systemctl status ls -la /run/account/ cat /etc/passwd ``` ### Common Issues #### Container Fails to Start - **Symptom**: Container boot fails or hangs - **Solution**: Check systemd and library dependencies - **Debug**: `sudo systemd-nspawn -D /tmp/account-utils-test-container` (without -b) #### Sockets Not Created - **Symptom**: `/run/account/*-socket` files missing - **Solution**: Check unit files are installed, verify socket activation - **Debug**: `container_exec systemctl list-sockets` #### Services Not Starting - **Symptom**: Socket exists but service doesn't activate - **Solution**: Check binary dependencies with `ldd` - **Debug**: `container_exec journalctl -xe` #### Permission Denied - **Symptom**: Tests can't modify passwd/shadow - **Solution**: Ensure running as root, check file permissions - **Debug**: Check shadow file is mode 600, owned by root ## CI/CD Integration ### GitHub Actions Example ```yaml name: Integration Tests on: [push, pull_request] jobs: integration: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y meson ninja-build libpam0g-dev libvarlink-dev - name: Build run: | meson setup build ninja -C build - name: Run integration tests run: | cd tests/ci sudo ./run-tests.sh ``` ### Local Development Add to your `.git/hooks/pre-push`: ```bash #!/bin/bash echo "Running integration tests..." cd tests/ci sudo ./run-tests.sh exit $? ``` ## Performance Considerations - Container creation: ~2-5 seconds - Container boot: ~3-5 seconds - Each test suite: ~10-30 seconds - Total runtime: ~1-2 minutes for all tests ## Security Notes 1. Tests must run as root (systemd-nspawn requirement) 2. Container is fully isolated from host 3. No host passwd/shadow files are ever modified 4. Container uses separate /etc and /run directories 5. Test cleanup removes all container data ## Future Enhancements - [ ] Add varlink client tests for direct protocol testing - [ ] Add performance/load tests for concurrent connections - [ ] Add security tests (privilege escalation, etc.) - [ ] Add failure injection tests - [ ] Add tests for configuration file parsing - [ ] Integration with CI/CD pipelines - [ ] Container image caching for faster runs - [ ] Parallel test execution account-utils-1.3.0/tests/ci/TROUBLESHOOTING.md000066400000000000000000000101331521474342200206550ustar00rootroot00000000000000## Advanced Debugging ### Enable Verbose Output ```bash # In run-tests.sh, add set -x sudo bash -x ./run-tests.sh # Or modify test script set -x # Add to top of test script ``` ### Inspect Container State ```bash # Keep container running sudo ./run-tests.sh --keep-container # List all files in container (use actual container path from test output) sudo find /tmp/account-utils-test-XXXXXX -ls # Check all services sudo systemd-nspawn -D /tmp/account-utils-test-XXXXXX # Inside container: systemctl list-units --all systemctl list-sockets --all ``` ### Debug Service Activation ```bash # Get shell in container (use actual container path from test output) sudo systemd-nspawn -D /tmp/account-utils-test-XXXXXX # Inside container, enable debug logging for systemd systemctl log-level debug # Restart socket systemctl restart pwaccessd.socket # Watch logs in real-time journalctl -u pwaccessd.socket -f ``` ### Test Service Manually ```bash # Start container (use actual container path from test output) sudo systemd-nspawn -D /tmp/account-utils-test-XXXXXX -b # In another terminal, get shell in container sudo systemd-nspawn -D /tmp/account-utils-test-XXXXXX # Inside container, stop automatic socket systemctl stop pwaccessd.socket # Run service manually /usr/bin/pwaccessd --help /usr/bin/pwaccessd --debug # If debug flag exists ``` ### Check Library Dependencies ```bash # For each service binary sudo systemd-nspawn -D /tmp/account-utils-test-container ldd /usr/bin/pwaccessd | grep "not found" ldd /usr/bin/pwupdd | grep "not found" ldd /usr/lib/security/pam_unix_ng.so | grep "not found" # If libraries missing, add to setup-container.sh ``` ## Getting More Help ### Collect Debug Information ```bash #!/bin/bash # debug-info.sh - Collect debugging information echo "=== System Info ===" uname -a systemd-nspawn --version echo "=== Container Files ===" ls -laR /tmp/account-utils-test-container/usr/bin/ ls -laR /tmp/account-utils-test-container/usr/lib/systemd/system/ echo "=== Service Dependencies ===" ldd /tmp/account-utils-test-container/usr/bin/pwaccessd ldd /tmp/account-utils-test-container/usr/bin/pwupdd echo "=== Container Status ===" machinectl status account-utils-test || echo "Container not running" echo "=== Service Logs ===" # Note: Replace XXXXXX with actual container path sudo systemd-nspawn -D /tmp/account-utils-test-XXXXXX journalctl -u pwaccessd.socket sudo systemd-nspawn -D /tmp/account-utils-test-XXXXXX journalctl -u pwupdd.socket ``` ### Report an Issue Include: 1. Output from debug-info.sh above 2. Full test output with `--verbose` 3. OS/distribution (e.g., Ubuntu 22.04, RHEL 9) 4. systemd version: `systemd-nspawn --version` 5. Build output: `ninja -C build 2>&1` 6. Test command used 7. Expected vs actual behavior ## Prevention ### Before Running Tests ```bash # Checklist [ ] Project is built: ls build/src/pwaccessd [ ] Running as root: id -u returns 0 [ ] Have systemd-nspawn: which systemd-nspawn [ ] Have disk space: df -h /tmp shows >100MB [ ] No stale containers: sudo machinectl list ``` ### Clean State ```bash # Start fresh sudo machinectl terminate account-utils-test 2>/dev/null || true sudo rm -rf /tmp/account-utils-test-container cd ../.. && meson compile -C build cd tests/ci && sudo ./run-tests.sh ``` ## Performance Issues ### Tests are Slow **Normal timing:** - Container setup: 2-5 seconds - Container boot: 3-5 seconds - Test execution: 10-30 seconds per suite - Total: 1-2 minutes **If slower:** 1. **Disk I/O**: Use faster disk or tmpfs ```bash # Use memory-backed filesystem sudo mount -t tmpfs -o size=100M tmpfs /tmp/account-utils-test-container ``` 2. **CPU**: Reduce parallelism ```bash # Run tests sequentially for test in test-*.sh; do sudo ./"$test" done ``` 3. **Network**: Disable if not needed ```bash # Add to nspawn command in run-tests.sh --network-veth=no ``` ## Still Stuck? 1. Read TESTING.md for detailed documentation 2. Check example tests: test-pwaccessd.sh 3. Review container setup: setup-container.sh 4. Enable debug output: `set -x` in scripts 5. Ask for help with debug information collected above account-utils-1.3.0/tests/ci/run-tests.sh000077500000000000000000000261341521474342200203570ustar00rootroot00000000000000#!/bin/bash # Main test runner for account-utils integration tests set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" BUILD_DIR="${BUILD_DIR:-$PROJECT_ROOT/build}" LOG_FILE="${LOG_FILE:-$SCRIPT_DIR/test-run-$(date +%Y%m%d-%H%M%S).log}" # Setup logging to both stdout/stderr and log file exec > >(tee -a "$LOG_FILE") exec 2> >(tee -a "$LOG_FILE" >&2) # Create secure temporary directory if CONTAINER_ROOT not explicitly set if [ -z "$CONTAINER_ROOT" ]; then CONTAINER_ROOT=$(mktemp -d -t account-utils-test.XXXXXXXXXX) TMPDIR_CREATED=1 else TMPDIR_CREATED=0 fi # Generate unique container name to allow parallel runs CONTAINER_NAME="account-utils-test-$$-$(date +%s)" # Export for test-utils.sh export CONTAINER_ROOT export CONTAINER_NAME source "$SCRIPT_DIR/test-utils.sh" KEEP_CONTAINER=0 SPECIFIC_TEST="" NSPAWN_PID="" # Log start of test run log_info "Test run started at $(date)" log_info "Log file: $LOG_FILE" # Function to cleanup (defined early so trap works for early failures) cleanup() { local exit_code=$? log_info "Cleaning up" # Export journal logs before stopping container if [ -n "$CONTAINER_NAME" ] && machinectl status "$CONTAINER_NAME" >/dev/null 2>&1; then log_info "Exporting journal logs from container" # Export to main log file with both full journal and service-specific logs echo "" >> "$LOG_FILE" echo "=========================================" >> "$LOG_FILE" echo "Container Journal Logs (Full)" >> "$LOG_FILE" echo "=========================================" >> "$LOG_FILE" container_exec journalctl --no-pager --all >> "$LOG_FILE" 2>&1 || log_warn "Failed to export full journal" echo "" >> "$LOG_FILE" echo "=========================================" >> "$LOG_FILE" echo "pwaccessd Service Logs" >> "$LOG_FILE" echo "=========================================" >> "$LOG_FILE" container_exec journalctl -u pwaccessd.service --no-pager >> "$LOG_FILE" 2>&1 || true echo "" >> "$LOG_FILE" echo "=========================================" >> "$LOG_FILE" echo "pwupdd Service Logs" >> "$LOG_FILE" echo "=========================================" >> "$LOG_FILE" container_exec journalctl -u 'pwupdd@*' --no-pager >> "$LOG_FILE" 2>&1 || true echo "" >> "$LOG_FILE" echo "=========================================" >> "$LOG_FILE" echo "Socket Activation Logs" >> "$LOG_FILE" echo "=========================================" >> "$LOG_FILE" container_exec journalctl -u pwaccessd.socket -u pwupdd.socket --no-pager >> "$LOG_FILE" 2>&1 || true echo "=========================================" >> "$LOG_FILE" fi # Terminate container if [ -n "$CONTAINER_NAME" ] && machinectl status "$CONTAINER_NAME" >/dev/null 2>&1; then log_info "Stopping container: $CONTAINER_NAME" # Try graceful poweroff first machinectl poweroff "$CONTAINER_NAME" 2>/dev/null || true # Wait up to 5 seconds for graceful shutdown local wait_count=0 while [ $wait_count -lt 5 ] && machinectl status "$CONTAINER_NAME" >/dev/null 2>&1; do sleep 1 wait_count=$((wait_count + 1)) done # If still running, force terminate if machinectl status "$CONTAINER_NAME" >/dev/null 2>&1; then log_warn "Container did not shut down gracefully, forcing termination" machinectl terminate "$CONTAINER_NAME" 2>/dev/null || true sleep 1 fi fi # Force kill nspawn process if still running if [ -n "$NSPAWN_PID" ] && kill -0 "$NSPAWN_PID" 2>/dev/null; then log_info "Waiting for nspawn process to exit" # Give it 2 more seconds local wait_count=0 while [ $wait_count -lt 2 ] && kill -0 "$NSPAWN_PID" 2>/dev/null; do sleep 1 wait_count=$((wait_count + 1)) done # Force kill if still alive if kill -0 "$NSPAWN_PID" 2>/dev/null; then log_warn "Force killing nspawn process $NSPAWN_PID" kill -9 "$NSPAWN_PID" 2>/dev/null || true fi # Final wait to reap the process wait $NSPAWN_PID 2>/dev/null || true fi # Remove container directory with safety checks if [ "$KEEP_CONTAINER" -eq 0 ]; then # Verify CONTAINER_ROOT is set and not empty if [ -z "$CONTAINER_ROOT" ]; then log_error "CONTAINER_ROOT is empty, refusing to delete" exit $exit_code fi # Verify path matches expected pattern (defense in depth) # Pattern: /tmp/account-utils-test.XXXXXXXXXX (10 random alphanumeric chars) if ! [[ "$CONTAINER_ROOT" =~ ^/tmp/account-utils-test\.[A-Za-z0-9]{10}$ ]]; then log_error "CONTAINER_ROOT path doesn't match expected pattern: $CONTAINER_ROOT" log_error "Expected: /tmp/account-utils-test.XXXXXXXXXX" log_error "Refusing to delete for safety" exit $exit_code fi # Verify we created this directory (if tracking variable exists) if [ -n "$TMPDIR_CREATED" ] && [ "$TMPDIR_CREATED" -ne 1 ]; then log_warn "TMPDIR_CREATED flag not set, directory may not have been created by this script" log_warn "Skipping deletion: $CONTAINER_ROOT" exit $exit_code fi # Verify directory exists and is a directory if [ ! -d "$CONTAINER_ROOT" ]; then log_warn "Container directory doesn't exist or is not a directory: $CONTAINER_ROOT" elif [ -L "$CONTAINER_ROOT" ]; then log_error "Container path is a symlink, refusing to delete: $CONTAINER_ROOT" else # Final safety check: verify ownership local owner owner=$(stat -c '%U' "$CONTAINER_ROOT" 2>/dev/null) if [ "$owner" != "root" ]; then log_error "Container directory not owned by root (owner: $owner), refusing to delete: $CONTAINER_ROOT" else log_info "Removing container directory: $CONTAINER_ROOT" rm -rf "${CONTAINER_ROOT:?}" || log_error "Failed to remove container directory" fi fi else log_info "Keeping container at: $CONTAINER_ROOT" log_info "Container name: $CONTAINER_NAME" log_info "To inspect: systemd-nspawn -D $CONTAINER_ROOT" log_info "Or: machinectl shell $CONTAINER_NAME" log_info "To cleanup later: sudo rm -rf $CONTAINER_ROOT" fi log_info "Test run ended at $(date)" # Flush output buffers before printing final log location sync log_info "Full log saved to: $LOG_FILE" exit $exit_code } # Register cleanup trap early to handle failures during setup trap cleanup EXIT INT TERM # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in --keep-container) KEEP_CONTAINER=1 shift ;; --help|-h) cat << EOF Usage: $0 [OPTIONS] [TEST_SUITE] Run integration tests for account-utils in systemd-nspawn container. OPTIONS: --keep-container Don't remove container after tests --verbose, -v Verbose output --help, -h Show this help message ENVIRONMENT: LOG_FILE Path to log file (default: test-run-YYYYMMDD-HHMMSS.log) TEST_SUITE: test-pwaccessd Run only pwaccessd tests test-pwupdd Run only pwupdd tests test-pam Run only PAM tests (default: run all tests) EXAMPLES: sudo ./run-tests.sh sudo ./run-tests.sh --keep-container test-pwaccessd sudo ./run-tests.sh -v test-pwupdd EOF exit 0 ;; test-*) SPECIFIC_TEST="$1" shift ;; *) log_error "Unknown option: $1" exit 1 ;; esac done # Check if running as root if [ "$(id -u)" -ne 0 ]; then log_error "This script must be run as root (for systemd-nspawn)" exit 1 fi # Build the project if needed log_info "Checking build" if [ ! -d "$BUILD_DIR" ]; then log_info "Build directory not found, building project" cd "$PROJECT_ROOT" meson setup "$BUILD_DIR" ninja -C "$BUILD_DIR" else log_info "Rebuilding project" ninja -C "$BUILD_DIR" fi if [ ! -e /usr/bin/machinectl ]; then log_error "machinectl not found!" exit 1; fi if [ ! -e /usr/bin/systemd-nspawn ]; then log_error "systemd-nspawn not found!" exit 1; fi # Setup container log_info "Setting up test container" "$SCRIPT_DIR/setup-container.sh" # Start container log_info "Starting container: $CONTAINER_NAME" # Stop any existing container with the same name machinectl terminate "$CONTAINER_NAME" 2>/dev/null || true sleep 1 # Start the container in the background systemd-nspawn -D "$CONTAINER_ROOT" \ --machine="$CONTAINER_NAME" \ --boot \ --notify-ready=yes \ --suppress-sync=yes \ --register=yes \ --keep-unit \ --quiet \ & NSPAWN_PID=$! # Wait for container to be ready log_info "Waiting for container to boot" sleep 3 # Check if container is running if ! machinectl status "$CONTAINER_NAME" >/dev/null 2>&1; then log_error "Container failed to start" wait $NSPAWN_PID || true exit 1 fi log_info "Container is running" # Wait for services to be ready sleep 2 # Check if sockets are available log_info "Checking if services are ready" if ! wait_for_socket "/run/account/pwaccess-socket" 15; then log_error "pwaccessd socket not ready" container_exec journalctl -u pwaccessd.socket --no-pager || true exit 1 fi if ! wait_for_socket "/run/account/pwupd-socket" 15; then log_error "pwupdd socket not ready" container_exec journalctl -u pwupdd.socket --no-pager || true exit 1 fi log_info "Services are ready" # Run tests ALL_TESTS_PASSED=0 if [ -z "$SPECIFIC_TEST" ]; then # Run all tests for test_script in "$SCRIPT_DIR"/test-*.sh; do if [ -f "$test_script" ] && [ "$test_script" != "$SCRIPT_DIR/test-utils.sh" ]; then log_info "Running test suite: $(basename "$test_script")" if bash "$test_script"; then log_info "✓ Test suite passed: $(basename "$test_script")" else log_error "✗ Test suite failed: $(basename "$test_script")" ALL_TESTS_PASSED=1 fi echo "" fi done else # Run specific test test_script="$SCRIPT_DIR/${SPECIFIC_TEST}.sh" if [ ! -f "$test_script" ]; then log_error "Test script not found: $test_script" exit 1 fi log_info "Running test suite: $SPECIFIC_TEST" if bash "$test_script"; then log_info "✓ Test suite passed" else log_error "✗ Test suite failed" ALL_TESTS_PASSED=1 fi fi # Print final summary echo "" echo "=========================================" echo "Integration Test Results" echo "=========================================" if [ $ALL_TESTS_PASSED -eq 0 ]; then echo -e "${GREEN}All test suites passed!${NC}" else echo -e "${RED}Some test suites failed!${NC}" fi echo "=========================================" exit $ALL_TESTS_PASSED account-utils-1.3.0/tests/ci/setup-container.sh000077500000000000000000000447221521474342200215360ustar00rootroot00000000000000#!/bin/bash # Setup systemd-nspawn container for integration testing set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" BUILD_DIR="${BUILD_DIR:-$PROJECT_ROOT/build}" # CONTAINER_ROOT should be set by caller (run-tests.sh) # If not set, create a temporary directory (but this is not recommended) if [ -z "$CONTAINER_ROOT" ]; then log_error "CONTAINER_ROOT not set. This script should be called by run-tests.sh" exit 1 fi source "$SCRIPT_DIR/test-utils.sh" # Detect system library directory (lib or lib64) case "$(uname -m)" in x86_64|aarch64|ppc64|ppc64le|s390x|riscv64|mips64|mips64el) LIBDIR="lib64" ;; *) LIBDIR="lib" ;; esac log_info "Setting up test container at: $CONTAINER_ROOT" log_info "Detected architecture: $(uname -m), using /usr/$LIBDIR" # Check if we're running as root if [ "$(id -u)" -ne 0 ]; then log_error "This script must be run as root" exit 1 fi # Container directory should not exist yet (created by mktemp) # But ensure we have proper permissions if [ ! -d "$CONTAINER_ROOT" ]; then log_error "Container directory does not exist: $CONTAINER_ROOT" exit 1 fi # Verify the directory is empty or owned by root if [ "$(ls -A "$CONTAINER_ROOT" 2>/dev/null)" ]; then log_warn "Container directory not empty, cleaning: $CONTAINER_ROOT" rm -rf "${CONTAINER_ROOT:?}"/* fi # Create container directory structure with secure permissions log_info "Creating container directory structure" mkdir -p "$CONTAINER_ROOT"/{etc,usr/{bin,sbin,lib,lib64,libexec,share},var/{log,lib},run,tmp,root,home} mkdir -p "$CONTAINER_ROOT/usr/lib/systemd/system" mkdir -p "$CONTAINER_ROOT/usr/$LIBDIR/security" mkdir -p "$CONTAINER_ROOT/etc/"{pam.d,account-utils/pwaccessd.conf.d,account-utils/pwupdd.conf.d} mkdir -p "$CONTAINER_ROOT/run/account" mkdir -p "$CONTAINER_ROOT/usr/share/file" mkdir -p "$CONTAINER_ROOT/usr/share/misc" # Create critical systemd directories mkdir -p "$CONTAINER_ROOT/etc/systemd/system" mkdir -p "$CONTAINER_ROOT/run/systemd" mkdir -p "$CONTAINER_ROOT/sys" mkdir -p "$CONTAINER_ROOT/proc" mkdir -p "$CONTAINER_ROOT/var/lib/systemd" # Create device nodes mkdir -p "$CONTAINER_ROOT/dev" mknod -m 666 "$CONTAINER_ROOT/dev/null" c 1 3 || true mknod -m 666 "$CONTAINER_ROOT/dev/zero" c 1 5 || true mknod -m 666 "$CONTAINER_ROOT/dev/random" c 1 8 || true mknod -m 666 "$CONTAINER_ROOT/dev/urandom" c 1 9 || true # Create base system files log_info "Creating base system files" cat > "$CONTAINER_ROOT/etc/passwd" << 'EOF' root:x:0:0:root:/root:/bin/bash nobody:x:65534:65534:Nobody:/:/usr/sbin/nologin EOF cat > "$CONTAINER_ROOT/etc/group" << 'EOF' root:x:0: users:x:100: nobody:x:65534: EOF # Create shadow file with root password (password: root) cat > "$CONTAINER_ROOT/etc/shadow" << 'EOF' root:$6$rounds=5000$test$YvWvE4K1G7kqkJ8FNF8V5kJ8Z7vK1G7kqkJ8FNF8V5kJ8Z7vK1G7kqkJ8FNF8V5kJ8Z7vK1G7kqkJ8FNF8V.:19000:0:99999:7::: nobody:*:19000:0:99999:7::: EOF chmod 600 "$CONTAINER_ROOT/etc/shadow" # Create shells file (list of valid login shells) cat > "$CONTAINER_ROOT/etc/shells" << 'EOF' /bin/sh /bin/bash /usr/bin/sh /usr/bin/bash EOF # Create nsswitch.conf cat > "$CONTAINER_ROOT/etc/nsswitch.conf" << 'EOF' passwd: files group: files shadow: files hosts: files dns networks: files protocols: files services: files ethers: files rpc: files EOF # Create os-release file (required by systemd-nspawn) cat > "$CONTAINER_ROOT/etc/os-release" << 'EOF' NAME="account-utils Test Container" ID=account-utils-test PRETTY_NAME="account-utils Integration Test Container" VERSION_ID=1.0 EOF # Create login.defs (required by useradd, usermod, etc.) cat > "$CONTAINER_ROOT/etc/login.defs" << 'EOF' # Basic login.defs configuration for testing # Password aging controls PASS_MAX_DAYS 99999 PASS_MIN_DAYS 0 PASS_MIN_LEN 5 PASS_WARN_AGE 7 # Min/max values for automatic uid selection in useradd UID_MIN 1000 UID_MAX 60000 # System accounts SYS_UID_MIN 100 SYS_UID_MAX 999 # Min/max values for automatic gid selection in groupadd GID_MIN 1000 GID_MAX 60000 # System accounts SYS_GID_MIN 100 SYS_GID_MAX 999 # Create home directories by default CREATE_HOME yes # Use SHA512 to encrypt password ENCRYPT_METHOD SHA512 EOF # Create machine-id (required by systemd) # Use a deterministic machine-id for testing echo "00000000000000000000000000000001" > "$CONTAINER_ROOT/etc/machine-id" chmod 444 "$CONTAINER_ROOT/etc/machine-id" # Find and copy systemd and essential libraries log_info "Copying systemd and essential libraries" # Define systemd binary path SYSTEMD_BIN="/usr/lib/systemd/systemd" # Copy systemd binary (critical - container cannot boot without this) if [ ! -f "$SYSTEMD_BIN" ]; then log_error "systemd binary not found at: $SYSTEMD_BIN" exit 1 fi cp -a "$SYSTEMD_BIN" "$CONTAINER_ROOT/usr/lib/systemd/systemd" # Copy systemd components for comp in systemctl systemd-run journalctl; do if ! which "$comp" >/dev/null 2>&1; then log_error "Systemd component not found: $comp" exit 1 fi cp -a "$(which $comp)" "$CONTAINER_ROOT/usr/bin/" done # Copy systemd-shutdown from its actual location (critical for shutdown) if [ ! -f "/usr/lib/systemd/systemd-shutdown" ]; then log_warn "systemd-shutdown not found at /usr/lib/systemd/systemd-shutdown" else cp -a "/usr/lib/systemd/systemd-shutdown" "$CONTAINER_ROOT/usr/lib/systemd/" fi # Create shutdown command symlinks (these are typically symlinks to systemctl) # Create them in both /usr/bin and /usr/sbin for compatibility for cmd in shutdown poweroff halt reboot; do ln -sf ../bin/systemctl "$CONTAINER_ROOT/usr/sbin/$cmd" ln -sf systemctl "$CONTAINER_ROOT/usr/bin/$cmd" done # Copy required systemd libraries found_systemd_lib=0 for lib in /usr/lib*/systemd/libsystemd*.so* /lib*/libsystemd*.so*; do if [ -e "$lib" ]; then cp -a -P "$lib" "$CONTAINER_ROOT/usr/$LIBDIR/" found_systemd_lib=1 fi done if [ $found_systemd_lib -eq 0 ]; then log_error "No systemd libraries found in /usr/lib*/systemd/ or /lib*/" exit 1 fi # Function to copy library dependencies copy_deps() { local binary="$1" local destdir="$CONTAINER_ROOT/usr/$LIBDIR" ldd "$binary" 2>/dev/null | grep -oP '=> \K[^ ]+' | while read -r lib; do if [ ! -e "$lib" ]; then continue fi local srcdir=$(dirname "$lib") local libname=$(basename "$lib") # Copy the actual file (following all symlinks to get the real file) local realfile=$(readlink -f "$lib") local realname=$(basename "$realfile") if [ -f "$realfile" ] && [ ! -e "$destdir/$realname" ]; then cp -a "$realfile" "$destdir/" fi # Extract SONAME from the library and create symlink if needed if [ -f "$realfile" ]; then local soname=$(objdump -p "$realfile" 2>/dev/null | grep SONAME | awk '{print $2}') if [ -n "$soname" ] && [ "$soname" != "$realname" ] && [ ! -e "$destdir/$soname" ]; then ln -sf "$realname" "$destdir/$soname" fi fi # Also copy the symlink from ldd output if it's different from the real file if [ "$libname" != "$realname" ]; then if [ -L "$lib" ]; then # Get the immediate target of the symlink (not fully resolved) local linktarget=$(readlink "$lib") # Create the symlink in the destination if [ ! -e "$destdir/$libname" ]; then ln -sf "$linktarget" "$destdir/$libname" fi fi fi done } # Copy dependencies for systemd copy_deps "$SYSTEMD_BIN" # Copy systemd-executor (required by systemd 260+, optional for older versions) if [ -f "/usr/lib/systemd/systemd-executor" ]; then cp -a "/usr/lib/systemd/systemd-executor" "$CONTAINER_ROOT/usr/lib/systemd/" copy_deps "/usr/lib/systemd/systemd-executor" fi # Copy libmount which is required by systemd but not always caught by ldd found_libmount=0 for mount_lib in /usr/$LIBDIR/libmount.so*; do if [ -e "$mount_lib" ]; then cp -a -P "$mount_lib" "$CONTAINER_ROOT/usr/$LIBDIR/" found_libmount=1 fi done if [ $found_libmount -eq 0 ]; then log_error "libmount library not found in /usr/$LIBDIR/ or /lib*/" exit 1 fi # Copy system binaries log_info "Copying system binaries" for cmd in bash sh ls cat echo mkdir rm chmod chown useradd userdel usermod chpasswd getent id stat grep cut head tail file varlinkctl; do if ! which "$cmd" >/dev/null 2>&1; then log_error "Essential system binary not found: $cmd" exit 1 fi cmdpath=$(which "$cmd") cp -a "$cmdpath" "$CONTAINER_ROOT/usr/bin/" copy_deps "$cmdpath" done # Create dynamic linker symlinks # XXX make portable for other architectures mkdir -p "$CONTAINER_ROOT/lib64" if [ -f /lib64/ld-linux-x86-64.so.2 ]; then cp -a -P /lib64/ld-linux-x86-64.so.2 "$CONTAINER_ROOT/lib64/" elif [ "$(uname -m)" = "x86_64" ]; then log_error "Dynamic linker /lib64/ld-linux-x86-64.so.2 not found on x86_64 system" exit 1 fi # Copy NSS libraries for user/group lookups (critical for getent, id, etc.) found_nss_files=0 for nss_lib in /usr/lib*/libnss_{files,dns}*.so*; do cp -a -P "$nss_lib" "$CONTAINER_ROOT/usr/$LIBDIR/" case "$nss_lib" in *libnss_files*) found_nss_files=1 ;; esac done if [ $found_nss_files -eq 0 ]; then log_error "libnss_files library not found - required for user/group lookups" exit 1 fi # Copy magic files for file command (at least one is required) found_magic=0 if [ -f /usr/share/file/magic.mgc ]; then cp -a /usr/share/file/magic.mgc "$CONTAINER_ROOT/usr/share/file/" ln -sf ../file/magic.mgc "$CONTAINER_ROOT/usr/share/misc/magic.mgc" found_magic=1 fi if [ -f /usr/share/file/magic ]; then cp -a /usr/share/file/magic "$CONTAINER_ROOT/usr/share/file/" ln -sf ../file/magic "$CONTAINER_ROOT/usr/share/misc/magic" found_magic=1 fi if [ $found_magic -eq 0 ]; then log_error "Magic file database not found in /usr/share/file/ - required for 'file' command" exit 1 fi # Install built binaries and libraries using meson install log_info "Installing account-utils binaries and libraries" if [ ! -d "$BUILD_DIR" ]; then log_error "Build directory not found: $BUILD_DIR" log_error "Please build the project first: meson setup build && ninja -C build" exit 1 fi # Use meson install with DESTDIR to install everything into the container log_info "Running meson install with DESTDIR=$CONTAINER_ROOT" DESTDIR="$CONTAINER_ROOT" meson install -C "$BUILD_DIR" --no-rebuild # --quiet # Copy dependencies for all installed binaries log_info "Copying library dependencies for installed binaries" # Copy deps for service binaries for service in pwaccessd pwupdd newidmapd; do if [ -f "$CONTAINER_ROOT/usr/libexec/$service" ]; then copy_deps "$CONTAINER_ROOT/usr/libexec/$service" fi done # Copy deps for client utilities for util in passwd chsh chfn chage expiry newuidmap newgidmap; do if [ -f "$CONTAINER_ROOT/usr/bin/$util" ]; then copy_deps "$CONTAINER_ROOT/usr/bin/$util" fi done # Copy deps for PAM modules for pam_mod in pam_unix_ng.so pam_debuginfo.so; do if [ -f "$CONTAINER_ROOT/usr/$LIBDIR/security/$pam_mod" ]; then copy_deps "$CONTAINER_ROOT/usr/$LIBDIR/security/$pam_mod" fi done # Copy system PAM modules needed for administrative tools for pam_mod in pam_deny.so pam_permit.so pam_rootok.so pam_warn.so; do if [ ! -f "/usr/$LIBDIR/security/$pam_mod" ]; then log_error "Required PAM module not found: /usr/$LIBDIR/security/$pam_mod" exit 1 fi cp -a "/usr/$LIBDIR/security/$pam_mod" "$CONTAINER_ROOT/usr/$LIBDIR/security/" done # Create systemd drop-in override for pwupdd@ to disable namespace restrictions in test container # These restrictions don't work inside systemd-nspawn which already provides namespace isolation mkdir -p "$CONTAINER_ROOT/etc/systemd/system/pwupdd@.service.d" cat > "$CONTAINER_ROOT/etc/systemd/system/pwupdd@.service.d/test-override.conf" << 'EOF' # Override for integration tests running in systemd-nspawn container # Disable all namespace and sandboxing restrictions that conflict with container isolation [Service] # Configure logging to work inside container StandardOutput=inherit StandardError=inherit # Disable filesystem namespace restrictions PrivateDevices=no PrivateTmp=no ProtectHome=no ProtectSystem=no ProtectProc=default ProcSubset=all ReadWritePaths= # Disable other namespace restrictions RestrictNamespaces=no ProtectKernelTunables=no ProtectKernelLogs=no ProtectKernelModules=no ProtectControlGroups=no ProtectClock=no ProtectHostname=no # Keep these security features as they work in containers LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes RestrictRealtime=yes RestrictSUIDSGID=yes EOF # Create systemd drop-in override for pwaccessd to disable namespace restrictions in test container # These restrictions don't work inside systemd-nspawn which already provides namespace isolation mkdir -p "$CONTAINER_ROOT/etc/systemd/system/pwaccessd.service.d" cat > "$CONTAINER_ROOT/etc/systemd/system/pwaccessd.service.d/test-override.conf" << 'EOF' # Override for integration tests running in systemd-nspawn container # Disable all namespace and sandboxing restrictions that conflict with container isolation [Service] # Configure logging to work inside container StandardOutput=inherit StandardError=inherit # Disable filesystem namespace restrictions PrivateDevices=no PrivateTmp=no ProtectHome=no ProtectSystem=no ProtectProc=default ProcSubset=all ReadWritePaths= # Disable other namespace restrictions RestrictNamespaces=no ProtectKernelTunables=no ProtectKernelLogs=no ProtectKernelModules=no ProtectControlGroups=no ProtectClock=no ProtectHostname=no # Keep these security features as they work in containers LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes RestrictRealtime=yes RestrictSUIDSGID=yes EOF # Create PAM configuration log_info "Creating PAM configuration" cat > "$CONTAINER_ROOT/etc/pam.d/common-auth" << 'EOF' auth required pam_unix_ng.so debug EOF cat > "$CONTAINER_ROOT/etc/pam.d/common-account" << 'EOF' account required pam_unix_ng.so debug EOF cat > "$CONTAINER_ROOT/etc/pam.d/common-password" << 'EOF' password required pam_unix_ng.so debug EOF cat > "$CONTAINER_ROOT/etc/pam.d/common-session" << 'EOF' session required pam_unix_ng.so debug EOF cat > "$CONTAINER_ROOT/etc/pam.d/system-auth" << 'EOF' auth required pam_unix_ng.so account required pam_unix_ng.so password required pam_unix_ng.so session required pam_unix_ng.so EOF cat > "$CONTAINER_ROOT/etc/pam.d/chpasswd" << 'EOF' auth required pam_permit.so account required pam_permit.so password required pam_unix_ng.so EOF # for useradd cat > "$CONTAINER_ROOT/etc/pam.d/newusers" << 'EOF' #%PAM-1.0 auth sufficient pam_rootok.so auth required pam_permit.so account required pam_permit.so password required pam_permit.so session required pam_permit.so EOF cat > "$CONTAINER_ROOT/etc/pam.d/other" << 'EOF' #%PAM-1.0 auth required pam_warn.so auth required pam_deny.so account required pam_warn.so account required pam_deny.so password required pam_warn.so password required pam_deny.so session required pam_warn.so session required pam_deny.so EOF # Copy systemd target files from host log_info "Copying systemd targets" for target in basic.target sysinit.target sockets.target multi-user.target rescue.target emergency.target halt.target poweroff.target reboot.target shutdown.target final.target umount.target; do if [ ! -f "/usr/lib/systemd/system/$target" ]; then log_error "Systemd target not found: /usr/lib/systemd/system/$target" exit 1 fi cp -a "/usr/lib/systemd/system/$target" "$CONTAINER_ROOT/usr/lib/systemd/system/" done # Copy systemd service files log_info "Copying systemd services" for service in systemd-halt.service systemd-poweroff.service systemd-journald.socket systemd-journald.service systemd-journald@.service systemd-journald-dev-log.socket systemd-journald-audit.socket; do if [ ! -f "/usr/lib/systemd/system/$service" ]; then log_error "Systemd service not found: /usr/lib/systemd/system/$service" exit 1 fi cp -a "/usr/lib/systemd/system/$service" "$CONTAINER_ROOT/usr/lib/systemd/system/" done # Copy journald binary and dependencies if [ -f "/usr/lib/systemd/systemd-journald" ]; then cp -a "/usr/lib/systemd/systemd-journald" "$CONTAINER_ROOT/usr/lib/systemd/" copy_deps "/usr/lib/systemd/systemd-journald" fi # Create journal directories mkdir -p "$CONTAINER_ROOT/var/log/journal" mkdir -p "$CONTAINER_ROOT/run/systemd/journal" chmod 755 "$CONTAINER_ROOT/var/log/journal" chmod 755 "$CONTAINER_ROOT/run/systemd/journal" # Create systemd target for container cat > "$CONTAINER_ROOT/usr/lib/systemd/system/container-test.target" << 'EOF' [Unit] Description=Container Test Target Requires=basic.target pwaccessd.socket pwupdd.socket After=basic.target pwaccessd.socket pwupdd.socket [Install] Alias=default.target EOF # Enable default.target mkdir -p "$CONTAINER_ROOT/etc/systemd/system" ln -sf /usr/lib/systemd/system/container-test.target "$CONTAINER_ROOT/etc/systemd/system/default.target" # Enable services mkdir -p "$CONTAINER_ROOT/etc/systemd/system/sockets.target.wants" # Verify our own service sockets exist before enabling if [ ! -f "$CONTAINER_ROOT/usr/lib/systemd/system/pwaccessd.socket" ]; then log_error "pwaccessd.socket not found - meson install may have failed" exit 1 fi if [ ! -f "$CONTAINER_ROOT/usr/lib/systemd/system/pwupdd.socket" ]; then log_error "pwupdd.socket not found - meson install may have failed" exit 1 fi ln -sf /usr/lib/systemd/system/pwaccessd.socket "$CONTAINER_ROOT/etc/systemd/system/sockets.target.wants/" ln -sf /usr/lib/systemd/system/pwupdd.socket "$CONTAINER_ROOT/etc/systemd/system/sockets.target.wants/" # Enable journald sockets ln -sf /usr/lib/systemd/system/systemd-journald.socket "$CONTAINER_ROOT/etc/systemd/system/sockets.target.wants/" ln -sf /usr/lib/systemd/system/systemd-journald-dev-log.socket "$CONTAINER_ROOT/etc/systemd/system/sockets.target.wants/" # Enable journald service mkdir -p "$CONTAINER_ROOT/etc/systemd/system/sysinit.target.wants" ln -sf /usr/lib/systemd/system/systemd-journald.service "$CONTAINER_ROOT/etc/systemd/system/sysinit.target.wants/" log_info "Container setup complete: $CONTAINER_ROOT" log_info "You can start the container with: systemd-nspawn -D $CONTAINER_ROOT -b" account-utils-1.3.0/tests/ci/test-chage.sh000077500000000000000000000400061521474342200204310ustar00rootroot00000000000000#!/bin/bash # Integration tests for chage (change password aging information) # Tests actual shadow field modification via varlink communication with pwupdd service # Running as root bypasses PAM authentication requirements SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" log_info "=========================================" log_info "chage Integration Tests" log_info "=========================================" # Helper function to get shadow field for a user get_shadow_field() { local username="$1" local field_num="${2:-1}" # 1=name, 2=passwd, 3=lstchg, 4=min, 5=max, 6=warn, 7=inact, 8=expire, 9=flag container_exec getent shadow "$username" 2>/dev/null | cut -d: -f"$field_num" } # Helper function to get all shadow fields get_shadow_full() { local username="$1" container_exec getent shadow "$username" 2>/dev/null } # Convert days since epoch to YYYY-MM-DD (approximate for testing) days_to_date() { local days="$1" # Simple conversion: seconds = days * 86400, then use date command container_exec date -d "@$((days * 86400))" +%Y-%m-%d 2>/dev/null || echo "invalid" } # Test 1: chage binary exists and is executable test_chage_binary_exists() { log_test "Testing chage binary availability" container_exec test -x /usr/bin/chage assert_success $? "chage binary exists and is executable" } # Test 2: chage --help works test_chage_help() { log_test "Testing chage --help option" local output=$(container_exec /usr/bin/chage --help 2>&1) local exit_code=$? assert_success "$exit_code" "chage --help exits successfully" assert_contains "$output" "lastday" "Help includes --lastday option" assert_contains "$output" "expiredate" "Help includes --expiredate option" assert_contains "$output" "inactive" "Help includes --inactive option" assert_contains "$output" "mindays" "Help includes --mindays option" assert_contains "$output" "maxdays" "Help includes --maxdays option" assert_contains "$output" "warndays" "Help includes --warndays option" } # Test 3: chage --version works test_chage_version() { log_test "Testing chage --version option" local output=$(container_exec /usr/bin/chage --version 2>&1) local exit_code=$? assert_success "$exit_code" "chage --version exits successfully" assert_contains "$output" "chage" "Version output contains program name" } # Test 4: Create test user with shadow entry test_create_test_user() { log_test "Creating test user with shadow entry" # Create user create_test_user "chagetest1" "TestPass123" # Verify shadow entry exists local shadow=$(get_shadow_full "chagetest1") assert_not_equals "$shadow" "" "Shadow entry exists for test user" # Verify fields are present (9 fields in shadow) local field_count=$(echo "$shadow" | tr -cd ':' | wc -c) assert_equals "$field_count" "8" "Shadow entry has correct field count (9 fields, 8 colons)" } # Test 5: Test pwupdd socket availability test_pwupdd_socket() { log_test "Testing pwupdd socket availability for chage" # chage communicates with pwupdd via varlink container_exec test -S /run/account/pwupd-socket assert_success $? "pwupdd socket exists for chage to use" # Check socket is accessible local perms=$(container_exec stat -c '%a' /run/account/pwupd-socket) assert_equals "$perms" "666" "pwupdd socket is accessible (mode 666)" } # Test 6: chage -m (--mindays) sets minimum password age test_chage_mindays() { log_test "Testing chage -m / --mindays option" # Set minimum days between password changes container_exec /usr/bin/chage -m 7 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -m succeeded" # Verify the change local min=$(get_shadow_field "chagetest1" 4) assert_equals "$min" "7" "Minimum password age set to 7 days" } # Test 7: chage -M (--maxdays) sets maximum password age test_chage_maxdays() { log_test "Testing chage -M / --maxdays option" # Set maximum days before password must be changed container_exec /usr/bin/chage -M 90 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -M succeeded" # Verify the change local max=$(get_shadow_field "chagetest1" 5) assert_equals "$max" "90" "Maximum password age set to 90 days" # Verify mindays unchanged local min=$(get_shadow_field "chagetest1" 4) assert_equals "$min" "7" "Minimum days unchanged after maxdays change" } # Test 8: chage -W (--warndays) sets password warning period test_chage_warndays() { log_test "Testing chage -W / --warndays option" # Set warning days before password expiration container_exec /usr/bin/chage -W 14 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -W succeeded" # Verify the change local warn=$(get_shadow_field "chagetest1" 6) assert_equals "$warn" "14" "Warning period set to 14 days" # Verify other fields unchanged local max=$(get_shadow_field "chagetest1" 5) assert_equals "$max" "90" "Maximum days unchanged after warndays change" } # Test 9: chage -I (--inactive) sets password inactivity period test_chage_inactive() { log_test "Testing chage -I / --inactive option" # Set days after password expiry until account is locked container_exec /usr/bin/chage -I 30 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -I succeeded" # Verify the change local inact=$(get_shadow_field "chagetest1" 7) assert_equals "$inact" "30" "Inactivity period set to 30 days" # Verify other fields unchanged local warn=$(get_shadow_field "chagetest1" 6) assert_equals "$warn" "14" "Warning days unchanged after inactive change" } # Test 10: chage -E (--expiredate) sets account expiration test_chage_expiredate() { log_test "Testing chage -E / --expiredate option" # Set account expiration date container_exec /usr/bin/chage -E 2030-12-31 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -E succeeded" # Verify the change (field 8 is expire date in days since epoch) local expire=$(get_shadow_field "chagetest1" 8) assert_not_equals "$expire" "" "Expiration date was set" # 2030-12-31 is approximately 22280 days since epoch # We just verify it's a reasonable number [ "$expire" -gt 20000 ] && [ "$expire" -lt 25000 ] assert_success $? "Expiration date is reasonable (around 2030)" # Verify other fields unchanged local inact=$(get_shadow_field "chagetest1" 7) assert_equals "$inact" "30" "Inactivity unchanged after expiredate change" } # Test 11: chage -d (--lastday) sets last password change date test_chage_lastday() { log_test "Testing chage -d / --lastday option" # Set last password change date container_exec /usr/bin/chage -d 2024-01-01 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -d succeeded" # Verify the change (field 3 is lstchg - days since epoch) local lstchg=$(get_shadow_field "chagetest1" 3) assert_not_equals "$lstchg" "" "Last change date was set" # 2024-01-01 is approximately 19723 days since epoch [ "$lstchg" -gt 19000 ] && [ "$lstchg" -lt 20000 ] assert_success $? "Last change date is reasonable (around 2024)" } # Test 12: chage with multiple options simultaneously test_chage_multiple_options() { log_test "Testing chage with multiple options at once" # Set multiple aging parameters at once container_exec /usr/bin/chage -m 5 -M 60 -W 7 -I 14 chagetest1 local exit_code=$? assert_success "$exit_code" "chage with multiple options succeeded" # Verify all changes local min=$(get_shadow_field "chagetest1" 4) local max=$(get_shadow_field "chagetest1" 5) local warn=$(get_shadow_field "chagetest1" 6) local inact=$(get_shadow_field "chagetest1" 7) assert_equals "$min" "5" "Minimum days changed in multi-option call" assert_equals "$max" "60" "Maximum days changed in multi-option call" assert_equals "$warn" "7" "Warning days changed in multi-option call" assert_equals "$inact" "14" "Inactivity changed in multi-option call" } # Test 13: chage with -1 (disabled/never) values test_chage_disabled_values() { log_test "Testing chage with -1 (disabled) values" # Set max to -1 (password never expires) container_exec /usr/bin/chage -M -1 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -M -1 succeeded" local max=$(get_shadow_field "chagetest1" 5) assert_equals "$max" "" "Maximum days set to empty (never expires)" # Set warning to -1 (no warning) container_exec /usr/bin/chage -W -1 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -W -1 succeeded" local warn=$(get_shadow_field "chagetest1" 6) assert_equals "$warn" "" "Warning days set to empty (no warning)" } # Test 14: chage -E with special date 1969-12-31 (never expires) test_chage_expiredate_never() { log_test "Testing chage -E with 1969-12-31 (never expires)" # Set expiration to 1969-12-31 which means -1 (never) container_exec /usr/bin/chage -E 1969-12-31 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -E 1969-12-31 succeeded" local expire=$(get_shadow_field "chagetest1" 8) assert_equals "$expire" "" "Expiration date set to empty (never expires)" } # Test 15: chage -l (--list) displays aging information test_chage_list() { log_test "Testing chage -l / --list option" # Reset to known values first container_exec /usr/bin/chage -m 10 -M 90 -W 7 -I 30 -E 2025-12-31 -d 2024-06-01 chagetest1 # List aging information local output=$(container_exec /usr/bin/chage -l chagetest1 2>&1) local exit_code=$? assert_success "$exit_code" "chage -l succeeded" # Output should contain aging information fields assert_contains "$output" "Last password change" "List output contains password change info" assert_contains "$output" "Password expires" "List output contains expiration info" assert_contains "$output" "Minimum password age" "List output contains minimum age" assert_contains "$output" "Maximum password age" "List output contains maximum age" } # Test 16: chage with zero values test_chage_zero_values() { log_test "Testing chage with zero values" # Set minimum to 0 (can change password immediately) container_exec /usr/bin/chage -m 0 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -m 0 succeeded" local min=$(get_shadow_field "chagetest1" 4) assert_equals "$min" "0" "Minimum days set to 0" # Set last change to 0 (force password change on next login) container_exec /usr/bin/chage -d 0 chagetest1 local exit_code=$? assert_success "$exit_code" "chage -d 0 succeeded" local lstchg=$(get_shadow_field "chagetest1" 3) assert_equals "$lstchg" "0" "Last change date set to 0 (force change)" } # Test 17: chage on different user (root changing another user) test_chage_different_user() { log_test "Testing chage on different user" # Create second test user create_test_user "chagetest2" "TestPass456" # Change aging for different user as root container_exec /usr/bin/chage -M 120 chagetest2 local exit_code=$? assert_success "$exit_code" "chage on different user succeeded" local max=$(get_shadow_field "chagetest2" 5) assert_equals "$max" "120" "Different user's maxdays changed by root" # Verify first user unchanged local user1_max=$(get_shadow_field "chagetest1" 5) assert_not_equals "$user1_max" "120" "First user's settings unchanged" # Cleanup delete_test_user "chagetest2" } # Test 18: Verify shadow field structure integrity test_shadow_structure_integrity() { log_test "Testing shadow structure integrity after modifications" # Set all fields to known values container_exec /usr/bin/chage -m 3 -M 45 -W 5 -I 10 -E 2028-06-15 -d 2024-01-15 chagetest1 # Get full shadow entry local shadow=$(get_shadow_full "chagetest1") # Count colons (should be 8 for 9 fields) local colon_count=$(echo "$shadow" | tr -cd ':' | wc -c) assert_equals "$colon_count" "8" "Shadow has correct colon separation" # Verify specific fields local name=$(get_shadow_field "chagetest1" 1) local min=$(get_shadow_field "chagetest1" 4) local max=$(get_shadow_field "chagetest1" 5) local warn=$(get_shadow_field "chagetest1" 6) local inact=$(get_shadow_field "chagetest1" 7) assert_equals "$name" "chagetest1" "Username field correct" assert_equals "$min" "3" "All fields maintain integrity (min)" assert_equals "$max" "45" "All fields maintain integrity (max)" assert_equals "$warn" "5" "All fields maintain integrity (warn)" assert_equals "$inact" "10" "All fields maintain integrity (inact)" } # Test 19: chage with non-existent user (should fail) test_chage_nonexistent_user() { log_test "Testing chage with non-existent user" # Attempt to change aging for non-existent user container_exec /usr/bin/chage -M 60 nonexistentuser99999 2>/dev/null local exit_code=$? assert_failure "$exit_code" "chage fails gracefully with non-existent user" } # Test 20: Test pwaccess dependency test_pwaccess_socket() { log_test "Testing pwaccess socket availability for user lookup" # chage uses pwaccess to get user information container_exec test -S /run/account/pwaccess-socket assert_success $? "pwaccess socket available for chage user lookup" } # Test 21: Test sequential changes test_sequential_changes() { log_test "Testing sequential shadow field changes" # Create fresh user create_test_user "chagetest3" "TestPass789" # Sequential changes container_exec /usr/bin/chage -M 100 chagetest3 local max1=$(get_shadow_field "chagetest3" 5) assert_equals "$max1" "100" "First change applied" container_exec /usr/bin/chage -W 10 chagetest3 local warn1=$(get_shadow_field "chagetest3" 6) assert_equals "$warn1" "10" "Second change applied" local max2=$(get_shadow_field "chagetest3" 5) assert_equals "$max2" "100" "First field preserved after second change" container_exec /usr/bin/chage -M 80 chagetest3 local max3=$(get_shadow_field "chagetest3" 5) assert_equals "$max3" "80" "Third change applied" local warn2=$(get_shadow_field "chagetest3" 6) assert_equals "$warn2" "10" "Second field preserved after third change" # Cleanup delete_test_user "chagetest3" } # Test 22: Test shadow file security test_shadow_file_security() { log_test "Testing shadow file security" # Shadow file should have restricted permissions local shadow_perms=$(container_exec stat -c '%a' /etc/shadow 2>/dev/null || echo "000") # Accept 600 or 000 (000 means file might not exist in test container) if [ "$shadow_perms" != "000" ]; then assert_equals "$shadow_perms" "600" "Shadow file has secure permissions (600)" else log_info "Shadow file permissions check skipped (file not accessible)" fi } # Test 23: Cleanup test users test_cleanup_chage_users() { log_test "Cleaning up chage test users" delete_test_user "chagetest1" # Verify cleanup local user_gone=$(container_exec getent passwd chagetest1 && echo "no" || echo "yes") assert_equals "$user_gone" "yes" "Test user removed successfully" } # Run all tests log_info "Starting chage integration tests" log_info "Running as root - tests actual shadow field modifications" echo "" run_test test_chage_binary_exists run_test test_chage_help run_test test_chage_version run_test test_create_test_user run_test test_pwupdd_socket run_test test_chage_mindays run_test test_chage_maxdays run_test test_chage_warndays run_test test_chage_inactive run_test test_chage_expiredate run_test test_chage_lastday run_test test_chage_multiple_options run_test test_chage_disabled_values run_test test_chage_expiredate_never run_test test_chage_list run_test test_chage_zero_values run_test test_chage_different_user run_test test_shadow_structure_integrity run_test test_chage_nonexistent_user run_test test_pwaccess_socket run_test test_sequential_changes run_test test_shadow_file_security run_test test_cleanup_chage_users # Print summary print_summary account-utils-1.3.0/tests/ci/test-chfn.sh000077500000000000000000000374621521474342200203140ustar00rootroot00000000000000#!/bin/bash # Integration tests for chfn (change GECOS/finger information) # Tests actual GECOS field modification via varlink communication with pwupdd service # Running as root bypasses PAM authentication requirements SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" log_info "=========================================" log_info "chfn Integration Tests" log_info "=========================================" # Helper function to get GECOS field for a user get_gecos_field() { local username="$1" local field_num="${2:-1}" # 1=full_name, 2=room, 3=work_phone, 4=home_phone, 5=other local gecos=$(container_exec getent passwd "$username" | cut -d: -f5) # Special handling for field 5 (other) - it's the last field and can contain commas if [ "$field_num" = "5" ]; then # Get everything after the 4th comma (the "other" field is everything remaining) echo "$gecos" | cut -d, -f5- else # For fields 1-4, extract the specific field echo "$gecos" | cut -d, -f"$field_num" fi } # Helper function to get full GECOS get_gecos_full() { local username="$1" container_exec getent passwd "$username" | cut -d: -f5 } # Test 1: chfn binary exists and is executable test_chfn_binary_exists() { log_test "Testing chfn binary availability" container_exec test -x /usr/bin/chfn assert_success $? "chfn binary exists and is executable" } # Test 2: chfn --help works test_chfn_help() { log_test "Testing chfn --help option" local output=$(container_exec /usr/bin/chfn --help 2>&1) local exit_code=$? assert_success "$exit_code" "chfn --help exits successfully" assert_contains "$output" "full-name" "Help includes --full-name option" assert_contains "$output" "home-phone" "Help includes --home-phone option" assert_contains "$output" "work-phone" "Help includes --work-phone option" assert_contains "$output" "room" "Help includes --room option" assert_contains "$output" "other" "Help includes --other option" } # Test 3: chfn --version works test_chfn_version() { log_test "Testing chfn --version option" local output=$(container_exec /usr/bin/chfn --version 2>&1) local exit_code=$? assert_success "$exit_code" "chfn --version exits successfully" assert_contains "$output" "chfn" "Version output contains program name" } # Test 4: Create test users for GECOS modification tests test_create_test_users() { log_test "Creating test users for chfn tests" # Create primary test user create_test_user "chfntest1" "TestPass123" # Set initial GECOS: Full Name,Room,Work Phone,Home Phone,Other container_exec usermod -c "John Doe,101,555-1234,555-5678,Building A" chfntest1 # Verify GECOS was set local gecos=$(get_gecos_full "chfntest1") assert_contains "$gecos" "John Doe" "Initial GECOS contains full name" local full_name=$(get_gecos_field "chfntest1" 1) assert_equals "$full_name" "John Doe" "Full name field is correct" } # Test 5: Test pwupdd socket availability test_pwupdd_socket() { log_test "Testing pwupdd socket availability for chfn" # chfn communicates with pwupdd via varlink container_exec test -S /run/account/pwupd-socket assert_success $? "pwupdd socket exists for chfn to use" # Check socket is accessible local perms=$(container_exec stat -c '%a' /run/account/pwupd-socket) assert_equals "$perms" "666" "pwupdd socket is accessible (mode 666)" } # Test 6: chfn -f (--full-name) changes full name test_chfn_full_name() { log_test "Testing chfn -f / --full-name option" # Change full name as root (no PAM authentication required) container_exec /usr/bin/chfn -f "Jane Smith" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn -f succeeded" # Verify the change local new_name=$(get_gecos_field "chfntest1" 1) assert_equals "$new_name" "Jane Smith" "Full name was changed successfully" # Verify other fields remain unchanged local room=$(get_gecos_field "chfntest1" 2) assert_equals "$room" "101" "Room field unchanged after full name change" } # Test 7: chfn -r (--room) changes room number test_chfn_room() { log_test "Testing chfn -r / --room option" # Change room number container_exec /usr/bin/chfn -r "202" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn -r succeeded" # Verify the change local new_room=$(get_gecos_field "chfntest1" 2) assert_equals "$new_room" "202" "Room number was changed successfully" # Verify full name remains unchanged local name=$(get_gecos_field "chfntest1" 1) assert_equals "$name" "Jane Smith" "Full name unchanged after room change" } # Test 8: chfn -w (--work-phone) changes work phone test_chfn_work_phone() { log_test "Testing chfn -w / --work-phone option" # Change work phone container_exec /usr/bin/chfn -w "555-9999" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn -w succeeded" # Verify the change local new_work_phone=$(get_gecos_field "chfntest1" 3) assert_equals "$new_work_phone" "555-9999" "Work phone was changed successfully" # Verify other fields remain unchanged local room=$(get_gecos_field "chfntest1" 2) assert_equals "$room" "202" "Room field unchanged after work phone change" } # Test 9: chfn -h (--home-phone) changes home phone test_chfn_home_phone() { log_test "Testing chfn -h / --home-phone option" # Change home phone container_exec /usr/bin/chfn -h "555-8888" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn -h succeeded" # Verify the change local new_home_phone=$(get_gecos_field "chfntest1" 4) assert_equals "$new_home_phone" "555-8888" "Home phone was changed successfully" # Verify other fields remain unchanged local work_phone=$(get_gecos_field "chfntest1" 3) assert_equals "$work_phone" "555-9999" "Work phone unchanged after home phone change" } # Test 10: chfn -o (--other) changes other information test_chfn_other() { log_test "Testing chfn -o / --other option" # Change other information container_exec /usr/bin/chfn -o "Building C" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn -o succeeded" # Verify the change local new_other=$(get_gecos_field "chfntest1" 5) assert_equals "$new_other" "Building C" "Other information was changed successfully" # Verify other fields remain unchanged local home_phone=$(get_gecos_field "chfntest1" 4) assert_equals "$home_phone" "555-8888" "Home phone unchanged after other info change" } # Test 11: chfn with multiple options simultaneously test_chfn_multiple_options() { log_test "Testing chfn with multiple options at once" # Change multiple fields at once container_exec /usr/bin/chfn -f "Bob Johnson" -r "303" -w "555-1111" -h "555-2222" -o "Dept HR" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn with multiple options succeeded" # Verify all changes local full_name=$(get_gecos_field "chfntest1" 1) local room=$(get_gecos_field "chfntest1" 2) local work_phone=$(get_gecos_field "chfntest1" 3) local home_phone=$(get_gecos_field "chfntest1" 4) local other=$(get_gecos_field "chfntest1" 5) assert_equals "$full_name" "Bob Johnson" "Full name changed in multi-option call" assert_equals "$room" "303" "Room changed in multi-option call" assert_equals "$work_phone" "555-1111" "Work phone changed in multi-option call" assert_equals "$home_phone" "555-2222" "Home phone changed in multi-option call" assert_equals "$other" "Dept HR" "Other info changed in multi-option call" } # Test 12: chfn with empty strings to clear fields test_chfn_clear_fields() { log_test "Testing chfn with empty strings to clear fields" # Clear specific fields container_exec /usr/bin/chfn -r "" -o "" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn with empty strings succeeded" # Verify fields were cleared local room=$(get_gecos_field "chfntest1" 2) local other=$(get_gecos_field "chfntest1" 5) assert_equals "$room" "" "Room field cleared" assert_equals "$other" "" "Other field cleared" # Verify other fields remain local full_name=$(get_gecos_field "chfntest1" 1) assert_equals "$full_name" "Bob Johnson" "Full name unchanged when clearing other fields" } # Test 13: chfn with special characters in full name test_chfn_special_chars_name() { log_test "Testing chfn with special characters in name" # Test with hyphen, apostrophe, period container_exec /usr/bin/chfn -f "Mary-Ann O'Connor Jr." chfntest1 local exit_code=$? assert_success "$exit_code" "chfn with special characters succeeded" local full_name=$(get_gecos_field "chfntest1" 1) assert_equals "$full_name" "Mary-Ann O'Connor Jr." "Special characters preserved in name" } # Test 14: chfn with spaces in fields test_chfn_spaces_in_fields() { log_test "Testing chfn with spaces in fields" # Test spaces in various fields container_exec /usr/bin/chfn -r "Suite 3-B" -o "Building A, Floor 2" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn with spaces succeeded" local room=$(get_gecos_field "chfntest1" 2) local other=$(get_gecos_field "chfntest1" 5) assert_equals "$room" "Suite 3-B" "Spaces preserved in room field" assert_equals "$other" "Building A, Floor 2" "Spaces and comma preserved in other field" } # Test 15: chfn on user with initially empty GECOS test_chfn_empty_gecos_initial() { log_test "Testing chfn on user with empty GECOS" # Create user with empty GECOS create_test_user "chfntest2" "TestPass456" # Set full name on empty GECOS container_exec /usr/bin/chfn -f "Alice Williams" chfntest2 local exit_code=$? assert_success "$exit_code" "chfn on empty GECOS succeeded" local full_name=$(get_gecos_field "chfntest2" 1) assert_equals "$full_name" "Alice Williams" "Full name set on previously empty GECOS" # Cleanup delete_test_user "chfntest2" } # Test 16: chfn with long field values test_chfn_long_values() { log_test "Testing chfn with long field values" # Test with long full name local long_name="Alexander Maximilian Christopher Wellington III" container_exec /usr/bin/chfn -f "$long_name" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn with long name succeeded" local retrieved_name=$(get_gecos_field "chfntest1" 1) # Note: may be truncated by system limits, but should succeed assert_contains "$retrieved_name" "Alexander" "Long name was stored (at least partially)" } # Test 17: chfn with phone number formats test_chfn_phone_formats() { log_test "Testing chfn with various phone number formats" # Test different phone formats container_exec /usr/bin/chfn -w "555-1234" -h "(555) 567-8901" chfntest1 local exit_code=$? assert_success "$exit_code" "chfn with formatted phone numbers succeeded" local work_phone=$(get_gecos_field "chfntest1" 3) local home_phone=$(get_gecos_field "chfntest1" 4) assert_equals "$work_phone" "555-1234" "Hyphenated phone format preserved" assert_equals "$home_phone" "(555) 567-8901" "Parenthesized phone format preserved" } # Test 18: chfn targeting different user (root changing another user) test_chfn_different_user() { log_test "Testing chfn with explicit username argument" # Create second test user create_test_user "chfntest3" "TestPass789" # Change GECOS for different user as root container_exec /usr/bin/chfn -f "Charlie Brown" chfntest3 local exit_code=$? assert_success "$exit_code" "chfn on different user succeeded" local full_name=$(get_gecos_field "chfntest3" 1) assert_equals "$full_name" "Charlie Brown" "Different user's GECOS changed by root" # Verify first test user unchanged local user1_name=$(get_gecos_field "chfntest1" 1) assert_contains "$user1_name" "Alexander" "First user's GECOS unchanged" # Cleanup delete_test_user "chfntest3" } # Test 19: Verify complete GECOS structure after changes test_gecos_structure_integrity() { log_test "Testing GECOS structure integrity after modifications" # Set all fields to known values container_exec /usr/bin/chfn -f "Test User" -r "Room1" -w "111-1111" -h "222-2222" -o "Info" chfntest1 # Get full GECOS local gecos=$(get_gecos_full "chfntest1") # Count commas (should be 4 for 5 fields) local comma_count=$(echo "$gecos" | tr -cd ',' | wc -c) assert_equals "$comma_count" "4" "GECOS has correct comma separation" # Verify expected structure assert_equals "$gecos" "Test User,Room1,111-1111,222-2222,Info" "GECOS structure is correct" } # Test 20: Test chfn with non-existent user (should fail) test_chfn_nonexistent_user() { log_test "Testing chfn with non-existent user" # Attempt to change GECOS for non-existent user # Note: Using redirect to file instead of command substitution due to exit code propagation issues local output output=$(container_exec /usr/bin/chfn -f "Nobody" nonexistentuser99999 2>&1) local exit_code=$? assert_failure "$exit_code" "chfn fails gracefully with non-existent user" assert_equals "$exit_code" "61" "chfn returns ENODATA (61) for non-existent user" assert_contains "$output" "user 'nonexistentuser99999' does not exist" "Error message indicates user does not exist" } # Test 21: Test pwaccess dependency test_pwaccess_socket() { log_test "Testing pwaccess socket availability for user lookup" # chfn uses pwaccess_get_account_name() to get current user container_exec test -S /run/account/pwaccess-socket assert_success $? "pwaccess socket available for chfn user lookup" } # Test 22: Test full workflow - multiple sequential changes test_sequential_changes() { log_test "Testing sequential GECOS field changes" # Create fresh user create_test_user "chfntest4" "TestPass000" # Sequential changes container_exec /usr/bin/chfn -f "Initial Name" chfntest4 local name1=$(get_gecos_field "chfntest4" 1) assert_equals "$name1" "Initial Name" "First change applied" container_exec /usr/bin/chfn -r "100" chfntest4 local room1=$(get_gecos_field "chfntest4" 2) assert_equals "$room1" "100" "Second change applied" local name2=$(get_gecos_field "chfntest4" 1) assert_equals "$name2" "Initial Name" "First field preserved after second change" container_exec /usr/bin/chfn -f "Updated Name" chfntest4 local name3=$(get_gecos_field "chfntest4" 1) assert_equals "$name3" "Updated Name" "Third change applied" local room2=$(get_gecos_field "chfntest4" 2) assert_equals "$room2" "100" "Second field preserved after third change" # Cleanup delete_test_user "chfntest4" } # Test 23: Cleanup test users test_cleanup_chfn_users() { log_test "Cleaning up chfn test users" delete_test_user "chfntest1" # Verify cleanup local user_gone=$(container_exec getent passwd chfntest1 && echo "no" || echo "yes") assert_equals "$user_gone" "yes" "Test user removed successfully" } # Run all tests log_info "Starting chfn integration tests" log_info "Running as root - PAM authentication bypassed" echo "" run_test test_chfn_binary_exists run_test test_chfn_help run_test test_chfn_version run_test test_create_test_users run_test test_pwupdd_socket run_test test_chfn_full_name run_test test_chfn_room run_test test_chfn_work_phone run_test test_chfn_home_phone run_test test_chfn_other run_test test_chfn_multiple_options run_test test_chfn_clear_fields run_test test_chfn_special_chars_name run_test test_chfn_spaces_in_fields run_test test_chfn_empty_gecos_initial run_test test_chfn_long_values run_test test_chfn_phone_formats run_test test_chfn_different_user run_test test_gecos_structure_integrity run_test test_chfn_nonexistent_user run_test test_pwaccess_socket run_test test_sequential_changes run_test test_cleanup_chfn_users # Print summary print_summary account-utils-1.3.0/tests/ci/test-chsh.sh000077500000000000000000000331311521474342200203100ustar00rootroot00000000000000#!/bin/bash # Integration tests for chsh (change login shell) # Tests actual shell modification via varlink communication with pwupdd service # Running as root bypasses PAM authentication requirements SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" log_info "=========================================" log_info "chsh Integration Tests" log_info "=========================================" # Helper function to get user's shell get_user_shell() { local username="$1" container_exec getent passwd "$username" | cut -d: -f7 } # Helper function to get passwd entry get_passwd_entry() { local username="$1" container_exec getent passwd "$username" } # Test 1: chsh binary exists and is executable test_chsh_binary_exists() { log_test "Testing chsh binary availability" container_exec test -x /usr/bin/chsh assert_success $? "chsh binary exists and is executable" } # Test 2: chsh --help works test_chsh_help() { log_test "Testing chsh --help option" local output=$(container_exec /usr/bin/chsh --help 2>&1) local exit_code=$? assert_success "$exit_code" "chsh --help exits successfully" assert_contains "$output" "shell" "Help includes --shell option" assert_contains "$output" "list-shells" "Help includes --list-shells option" } # Test 3: chsh --version works test_chsh_version() { log_test "Testing chsh --version option" local output=$(container_exec /usr/bin/chsh --version 2>&1) local exit_code=$? assert_success "$exit_code" "chsh --version exits successfully" assert_contains "$output" "chsh" "Version output contains program name" } # Test 4: Create test users with default shell test_create_test_users() { log_test "Creating test users with default shell" # Create user create_test_user "chshtest1" "TestPass123" # Verify user has a shell local shell=$(get_user_shell "chshtest1") assert_not_equals "$shell" "" "Test user has a shell assigned" log_info "Default shell: $shell" } # Test 5: Test pwupdd socket availability test_pwupdd_socket() { log_test "Testing pwupdd socket availability for chsh" # chsh communicates with pwupdd via varlink container_exec test -S /run/account/pwupd-socket assert_success $? "pwupdd socket exists for chsh to use" # Check socket is accessible local perms=$(container_exec stat -c '%a' /run/account/pwupd-socket) assert_equals "$perms" "666" "pwupdd socket is accessible (mode 666)" } # Test 6: chsh -s (--shell) changes user shell test_chsh_change_shell() { log_test "Testing chsh -s / --shell option" # Get current shell local shell_before=$(get_user_shell "chshtest1") log_info "Shell before: $shell_before" # Determine which shell to switch to local new_shell="/bin/sh" if [ "$shell_before" = "/bin/sh" ]; then new_shell="/bin/bash" fi # Verify new shell exists if ! container_exec test -f "$new_shell" 2>/dev/null; then log_info "Shell $new_shell not available, using /bin/sh" new_shell="/bin/sh" fi # Change shell as root (no PAM authentication required) container_exec /usr/bin/chsh -s "$new_shell" chshtest1 local exit_code=$? assert_success "$exit_code" "chsh -s succeeded" # Verify the change local shell_after=$(get_user_shell "chshtest1") assert_equals "$shell_after" "$new_shell" "Shell was changed successfully" } # Test 7: chsh changes only shell field, not other fields test_chsh_preserves_other_fields() { log_test "Testing chsh preserves other passwd fields" # Get current passwd entry local passwd_before=$(get_passwd_entry "chshtest1") local name_before=$(echo "$passwd_before" | cut -d: -f1) local uid_before=$(echo "$passwd_before" | cut -d: -f3) local gid_before=$(echo "$passwd_before" | cut -d: -f4) local gecos_before=$(echo "$passwd_before" | cut -d: -f5) local home_before=$(echo "$passwd_before" | cut -d: -f6) local shell_before=$(echo "$passwd_before" | cut -d: -f7) # Change shell back local target_shell="/bin/bash" if [ "$shell_before" = "/bin/bash" ]; then target_shell="/bin/sh" fi container_exec /usr/bin/chsh -s "$target_shell" chshtest1 # Get passwd entry after change local passwd_after=$(get_passwd_entry "chshtest1") local name_after=$(echo "$passwd_after" | cut -d: -f1) local uid_after=$(echo "$passwd_after" | cut -d: -f3) local gid_after=$(echo "$passwd_after" | cut -d: -f4) local gecos_after=$(echo "$passwd_after" | cut -d: -f5) local home_after=$(echo "$passwd_after" | cut -d: -f6) local shell_after=$(echo "$passwd_after" | cut -d: -f7) # Verify other fields unchanged assert_equals "$name_after" "$name_before" "Username unchanged" assert_equals "$uid_after" "$uid_before" "UID unchanged" assert_equals "$gid_after" "$gid_before" "GID unchanged" assert_equals "$gecos_after" "$gecos_before" "GECOS unchanged" assert_equals "$home_after" "$home_before" "Home directory unchanged" # Verify shell changed assert_not_equals "$shell_after" "$shell_before" "Shell was changed" assert_equals "$shell_after" "$target_shell" "Shell changed to target" } # Test 8: chsh with absolute path for shell test_chsh_absolute_path() { log_test "Testing chsh with absolute path" # Change to shell with absolute path container_exec /usr/bin/chsh -s /bin/sh chshtest1 local exit_code=$? assert_success "$exit_code" "chsh with absolute path succeeded" local shell=$(get_user_shell "chshtest1") assert_equals "$shell" "/bin/sh" "Absolute path shell set correctly" } # Test 9: chsh on different user (root changing another user) test_chsh_different_user() { log_test "Testing chsh on different user" # Create second test user create_test_user "chshtest2" "TestPass456" local shell_before=$(get_user_shell "chshtest2") log_info "User2 shell before: $shell_before" # Change shell for different user as root container_exec /usr/bin/chsh -s /bin/sh chshtest2 local exit_code=$? assert_success "$exit_code" "chsh on different user succeeded" local shell_after=$(get_user_shell "chshtest2") assert_equals "$shell_after" "/bin/sh" "Different user's shell changed by root" # Verify first user unchanged local user1_shell=$(get_user_shell "chshtest1") assert_equals "$user1_shell" "/bin/sh" "First user's shell unchanged" # Cleanup delete_test_user "chshtest2" } # Test 10: chsh -l (--list-shells) displays available shells test_chsh_list_shells() { log_test "Testing chsh -l / --list-shells option" # List available shells local output=$(container_exec /usr/bin/chsh -l 2>&1) local exit_code=$? assert_success "$exit_code" "chsh -l succeeded" # Output should contain at least some shell path or configuration info log_info "Available shells output: ${output:0:100}..." # Just verify command executed successfully } # Test 11: chsh with valid shell path test_chsh_valid_shell() { log_test "Testing chsh with valid shell path" # Find a valid shell in the container local valid_shell="" for shell in /bin/bash /bin/sh /usr/bin/bash /bin/dash; do if container_exec test -x "$shell" 2>/dev/null; then valid_shell="$shell" break fi done if [ -z "$valid_shell" ]; then valid_shell="/bin/sh" # Fallback fi log_info "Testing with valid shell: $valid_shell" container_exec /usr/bin/chsh -s "$valid_shell" chshtest1 local exit_code=$? assert_success "$exit_code" "chsh with valid shell succeeded" local shell=$(get_user_shell "chshtest1") assert_equals "$shell" "$valid_shell" "Valid shell set correctly" } # Test 12: Test passwd field structure integrity test_passwd_structure_integrity() { log_test "Testing passwd structure integrity after shell change" # Change shell container_exec /usr/bin/chsh -s /bin/bash chshtest1 # Get passwd entry local passwd=$(get_passwd_entry "chshtest1") # Verify field count (7 fields = 6 colons) local colon_count=$(echo "$passwd" | tr -cd ':' | wc -c) assert_equals "$colon_count" "6" "Passwd has correct field count (7 fields, 6 colons)" # Verify all fields are present local name=$(echo "$passwd" | cut -d: -f1) local uid=$(echo "$passwd" | cut -d: -f3) local gid=$(echo "$passwd" | cut -d: -f4) local home=$(echo "$passwd" | cut -d: -f6) local shell=$(echo "$passwd" | cut -d: -f7) assert_equals "$name" "chshtest1" "Name field correct" assert_not_equals "$uid" "" "UID field present" assert_not_equals "$gid" "" "GID field present" assert_not_equals "$home" "" "Home field present" assert_not_equals "$shell" "" "Shell field present" } # Test 13: Test sequential shell changes test_sequential_changes() { log_test "Testing sequential shell changes" # Create fresh user create_test_user "chshtest3" "TestPass789" # First change container_exec /usr/bin/chsh -s /bin/sh chshtest3 local shell1=$(get_user_shell "chshtest3") assert_equals "$shell1" "/bin/sh" "First shell change applied" # Second change container_exec /usr/bin/chsh -s /bin/bash chshtest3 local shell2=$(get_user_shell "chshtest3") assert_equals "$shell2" "/bin/bash" "Second shell change applied" # Third change container_exec /usr/bin/chsh -s /bin/sh chshtest3 local shell3=$(get_user_shell "chshtest3") assert_equals "$shell3" "/bin/sh" "Third shell change applied" # Cleanup delete_test_user "chshtest3" } # Test 14: Test shell is absolute path test_shell_absolute_path() { log_test "Testing shell field contains absolute path" local shell=$(get_user_shell "chshtest1") # Shell should start with / if [[ "$shell" == /* ]]; then assert_success 0 "Shell is an absolute path" else log_warn "Shell is not absolute: $shell" assert_success 1 "Shell should be absolute path" fi } # Test 15: Test multiple users have independent shells test_independent_shells() { log_test "Testing users have independent shell settings" # Create two users create_test_user "chshtest4" "TestPass111" create_test_user "chshtest5" "TestPass222" # Set different shells container_exec /usr/bin/chsh -s /bin/sh chshtest4 container_exec /usr/bin/chsh -s /bin/bash chshtest5 local shell4=$(get_user_shell "chshtest4") local shell5=$(get_user_shell "chshtest5") assert_equals "$shell4" "/bin/sh" "User4 has correct shell" assert_equals "$shell5" "/bin/bash" "User5 has correct shell" assert_not_equals "$shell4" "$shell5" "Users have different shells" # Cleanup delete_test_user "chshtest4" delete_test_user "chshtest5" } # Test 16: Test common shell paths exist test_common_shells_exist() { log_test "Testing common shell paths" # At least /bin/sh should exist (POSIX requirement) container_exec test -f /bin/sh assert_success $? "/bin/sh exists" # Check for bash if container_exec test -f /bin/bash 2>/dev/null; then log_info "/bin/bash exists" elif container_exec test -f /usr/bin/bash 2>/dev/null; then log_info "/usr/bin/bash exists" else log_info "bash not found in common locations" fi } # Test 17: Test chsh with non-existent user (should fail) test_chsh_nonexistent_user() { log_test "Testing chsh with non-existent user" # Attempt to change shell for non-existent user local output output=$(container_exec /usr/bin/chsh -s /bin/sh nonexistentuser99999 2>&1) local exit_code=$? assert_failure "$exit_code" "chsh fails gracefully with non-existent user" assert_equals "$exit_code" "61" "chsh returns ENODATA (61) for non-existent user" assert_contains "$output" "user 'nonexistentuser99999' does not exist" "Error message indicates user does not exist" } # Test 18: Test pwaccess dependency test_pwaccess_socket() { log_test "Testing pwaccess socket availability for user lookup" # chsh uses pwaccess to get user information container_exec test -S /run/account/pwaccess-socket assert_success $? "pwaccess socket available for chsh user lookup" } # Test 19: Test root user has shell test_root_shell() { log_test "Testing root user has shell" local root_shell=$(get_user_shell "root") assert_not_equals "$root_shell" "" "Root user has a shell" log_info "Root shell: $root_shell" # Root shell should be absolute path if [[ "$root_shell" == /* ]]; then assert_success 0 "Root shell is absolute path" else log_warn "Root shell not absolute: $root_shell" fi } # Test 20: Cleanup test users test_cleanup_chsh_users() { log_test "Cleaning up chsh test users" delete_test_user "chshtest1" # Verify cleanup local user_gone=$(container_exec getent passwd chshtest1 && echo "no" || echo "yes") assert_equals "$user_gone" "yes" "Test user removed successfully" } # Run all tests log_info "Starting chsh integration tests" log_info "Running as root - tests actual shell modifications" echo "" run_test test_chsh_binary_exists run_test test_chsh_help run_test test_chsh_version run_test test_create_test_users run_test test_pwupdd_socket run_test test_chsh_change_shell run_test test_chsh_preserves_other_fields run_test test_chsh_absolute_path run_test test_chsh_different_user run_test test_chsh_list_shells run_test test_chsh_valid_shell run_test test_passwd_structure_integrity run_test test_sequential_changes run_test test_shell_absolute_path run_test test_independent_shells run_test test_common_shells_exist run_test test_chsh_nonexistent_user run_test test_pwaccess_socket run_test test_root_shell run_test test_cleanup_chsh_users # Print summary print_summary account-utils-1.3.0/tests/ci/test-expiry.sh000077500000000000000000000305561521474342200207130ustar00rootroot00000000000000#!/bin/bash # Integration tests for expiry (check password expiration) # Tests actual password expiration checking via pwaccess service SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" log_info "=========================================" log_info "expiry Integration Tests" log_info "=========================================" # Helper function to get shadow field for a user get_shadow_field() { local username="$1" local field_num="${2:-1}" # 1=name, 2=passwd, 3=lstchg, 4=min, 5=max, 6=warn, 7=inact, 8=expire, 9=flag container_exec getent shadow "$username" 2>/dev/null | cut -d: -f"$field_num" } # Test 1: expiry binary exists and is executable test_expiry_binary_exists() { log_test "Testing expiry binary availability" container_exec test -x /usr/bin/expiry assert_success $? "expiry binary exists and is executable" } # Test 2: expiry --help works test_expiry_help() { log_test "Testing expiry --help option" local output=$(container_exec /usr/bin/expiry --help 2>&1) local exit_code=$? assert_success "$exit_code" "expiry --help exits successfully" assert_contains "$output" "check" "Help includes --check option" assert_contains "$output" "force" "Help includes --force option" assert_contains "$output" "expiration" "Help describes expiration checking" } # Test 3: expiry --version works test_expiry_version() { log_test "Testing expiry --version option" local output=$(container_exec /usr/bin/expiry --version 2>&1) local exit_code=$? assert_success "$exit_code" "expiry --version exits successfully" assert_contains "$output" "expiry" "Version output contains program name" } # Test 4: Create test users with different expiration states test_create_test_users() { log_test "Creating test users for expiry tests" # Create user with normal password aging create_test_user "expirytest1" "TestPass123" # Verify user has shadow entry local shadow=$(get_shadow_field "expirytest1" 1) assert_equals "$shadow" "expirytest1" "Test user has shadow entry" # Set password aging to reasonable values (not expired) container_exec /usr/bin/chage -M 90 -W 7 expirytest1 2>/dev/null || true } # Test 5: Test pwaccess socket availability test_pwaccess_socket() { log_test "Testing pwaccess socket availability for expiry" # expiry uses pwaccess to check expiration container_exec test -S /run/account/pwaccess-socket assert_success $? "pwaccess socket exists for expiry to use" # Check socket is accessible local perms=$(container_exec stat -c '%a' /run/account/pwaccess-socket) assert_equals "$perms" "666" "pwaccess socket is accessible (mode 666)" } # Test 6: expiry -c (--check) checks non-expired account test_expiry_check_not_expired() { log_test "Testing expiry -c on non-expired account" # Set account to not be expired (far future max days) container_exec /usr/bin/chage -M 99999 expirytest1 2>/dev/null || true # Check expiration status local output=$(container_exec /usr/bin/expiry -c expirytest1 2>&1) local exit_code=$? # Exit code 0 typically means not expired log_info "expiry -c exit code: $exit_code" log_info "expiry -c output: ${output:0:100}" # Just verify command executed # (exact behavior depends on pwaccess implementation) } # Test 7: expiry -c checks specific user test_expiry_check_user() { log_test "Testing expiry -c with username argument" # Create second user create_test_user "expirytest2" "TestPass456" container_exec /usr/bin/chage -M 99999 expirytest2 2>/dev/null || true # Check expiration for specific user local output=$(container_exec /usr/bin/expiry -c expirytest2 2>&1) local exit_code=$? log_info "expiry -c expirytest2 exit code: $exit_code" # Verify command executed # Cleanup delete_test_user "expirytest2" } # Test 8: expiry with expired password (lstchg=0) test_expiry_check_expired_password() { log_test "Testing expiry -c with expired password" # Create user with expired password (lstchg=0 forces change on next login) create_test_user "expirytest3" "TestPass789" container_exec /usr/bin/chage -d 0 expirytest3 2>/dev/null || true # Check expiration status local output=$(container_exec /usr/bin/expiry -c expirytest3 2>&1) local exit_code=$? log_info "Expired password check exit code: $exit_code" log_info "Expired password check output: ${output:0:100}" # Non-zero exit code expected for expired password if [ "$exit_code" -ne 0 ]; then assert_success 0 "expiry detected expired password (non-zero exit)" else log_info "expiry returned 0 for expired password (may vary by implementation)" fi # Cleanup delete_test_user "expirytest3" } # Test 9: expiry with account expiration date test_expiry_account_expiration() { log_test "Testing expiry with account expiration date" # Create user and set account to expire in past create_test_user "expirytest4" "TestPass000" container_exec /usr/bin/chage -E 2020-01-01 expirytest4 2>/dev/null || true # Check expiration status local output=$(container_exec /usr/bin/expiry -c expirytest4 2>&1) local exit_code=$? log_info "Expired account check exit code: $exit_code" log_info "Expired account check output: ${output:0:100}" # Cleanup delete_test_user "expirytest4" } # Test 10: expiry -f (--force) option exists test_expiry_force_option() { log_test "Testing expiry -f / --force option" # Note: -f forces interactive password change if expired # We can't test this in automated tests without PAM interaction # But we can verify the option is recognized local help_output=$(container_exec /usr/bin/expiry --help 2>&1) assert_contains "$help_output" "force" "Help shows -f/--force option" # Try to call -f (will fail or prompt, but should recognize option) # Using non-existent user to avoid actual password prompt container_exec /usr/bin/expiry -f nonexistentuser99999 2>/dev/null local exit_code=$? # Should fail (user doesn't exist) but recognizes the option log_info "expiry -f with invalid user exit code: $exit_code" } # Test 11: expiry rejects conflicting options test_expiry_option_conflict() { log_test "Testing expiry rejects -c and -f together" # expiry should reject using -c and -f simultaneously container_exec /usr/bin/expiry -c -f expirytest1 2>&1 local exit_code=$? # Should fail with error assert_failure "$exit_code" "expiry rejects -c and -f together" } # Test 12: expiry requires option (-c or -f) test_expiry_requires_option() { log_test "Testing expiry requires -c or -f option" # expiry without -c or -f should fail container_exec /usr/bin/expiry expirytest1 2>&1 local exit_code=$? # Should fail (requires option) assert_failure "$exit_code" "expiry requires -c or -f option" } # Test 13: expiry rejects too many arguments test_expiry_too_many_args() { log_test "Testing expiry rejects too many arguments" # expiry accepts maximum 1 username container_exec /usr/bin/expiry -c user1 user2 2>&1 local exit_code=$? # Should fail (too many arguments) assert_failure "$exit_code" "expiry rejects multiple usernames" } # Test 14: expiry with non-existent user test_expiry_nonexistent_user() { log_test "Testing expiry with non-existent user" # Check expiration for non-existent user container_exec /usr/bin/expiry -c nonexistentuser99999 2>&1 local exit_code=$? # Should fail assert_failure "$exit_code" "expiry fails gracefully with non-existent user" } # Test 15: expiry on multiple different users test_expiry_multiple_users() { log_test "Testing expiry on multiple users independently" # Create users with different aging create_test_user "expirytest5" "TestPass111" create_test_user "expirytest6" "TestPass222" # Set different aging for each container_exec /usr/bin/chage -M 60 expirytest5 2>/dev/null || true container_exec /usr/bin/chage -M 90 expirytest6 2>/dev/null || true # Check each independently container_exec /usr/bin/expiry -c expirytest5 2>&1 local exit_code5=$? container_exec /usr/bin/expiry -c expirytest6 2>&1 local exit_code6=$? log_info "User5 expiry check: exit $exit_code5" log_info "User6 expiry check: exit $exit_code6" # Cleanup delete_test_user "expirytest5" delete_test_user "expirytest6" } # Test 16: expiry checks shadow field dependencies test_expiry_shadow_dependencies() { log_test "Testing expiry depends on shadow fields" # expiry checks shadow fields: lstchg, max, warn, inact, expire # Verify these fields exist for test user local lstchg=$(get_shadow_field "expirytest1" 3) local max=$(get_shadow_field "expirytest1" 5) local warn=$(get_shadow_field "expirytest1" 6) local inact=$(get_shadow_field "expirytest1" 7) local expire=$(get_shadow_field "expirytest1" 8) log_info "Shadow fields - lstchg:$lstchg max:$max warn:$warn inact:$inact expire:$expire" # Shadow fields should be accessible assert_success 0 "Shadow fields accessible for expiry checking" } # Test 17: expiry with recently changed password test_expiry_recent_password() { log_test "Testing expiry with recently changed password" # Set password recently changed (today) local today_date=$(container_exec date +%Y-%m-%d) container_exec /usr/bin/chage -d "$today_date" -M 90 expirytest1 2>/dev/null || true # Check expiration local output=$(container_exec /usr/bin/expiry -c expirytest1 2>&1) local exit_code=$? log_info "Recent password check exit code: $exit_code" # Should not be expired (just changed) } # Test 18: expiry with max password age test_expiry_max_password_age() { log_test "Testing expiry with maximum password age set" # Set maximum password age container_exec /usr/bin/chage -M 30 expirytest1 2>/dev/null || true # Verify max was set local max=$(get_shadow_field "expirytest1" 5) log_info "Maximum password age: $max days" # Check expiration considers max age container_exec /usr/bin/expiry -c expirytest1 2>&1 local exit_code=$? log_info "expiry check with max age exit code: $exit_code" } # Test 19: expiry with warning period test_expiry_warning_period() { log_test "Testing expiry with warning period" # Set warning period container_exec /usr/bin/chage -W 14 expirytest1 2>/dev/null || true # Verify warning was set local warn=$(get_shadow_field "expirytest1" 6) log_info "Warning period: $warn days" # Check expiration considers warning period container_exec /usr/bin/expiry -c expirytest1 2>&1 local exit_code=$? log_info "expiry check with warning period exit code: $exit_code" } # Test 20: expiry on root user test_expiry_root_user() { log_test "Testing expiry check on root user" # Root should have shadow entry local root_shadow=$(get_shadow_field "root" 1) assert_equals "$root_shadow" "root" "Root user has shadow entry" # Check root expiration (should not be expired) container_exec /usr/bin/expiry -c root 2>&1 local exit_code=$? log_info "Root expiry check exit code: $exit_code" # Root typically doesn't expire } # Test 21: Cleanup test users test_cleanup_expiry_users() { log_test "Cleaning up expiry test users" delete_test_user "expirytest1" # Verify cleanup local user_gone=$(container_exec getent passwd expirytest1 && echo "no" || echo "yes") assert_equals "$user_gone" "yes" "Test user removed successfully" } # Run all tests log_info "Starting expiry integration tests" log_info "Tests check password expiration status via pwaccess" echo "" run_test test_expiry_binary_exists run_test test_expiry_help run_test test_expiry_version run_test test_create_test_users run_test test_pwaccess_socket run_test test_expiry_check_not_expired run_test test_expiry_check_user run_test test_expiry_check_expired_password run_test test_expiry_account_expiration run_test test_expiry_force_option run_test test_expiry_option_conflict run_test test_expiry_requires_option run_test test_expiry_too_many_args run_test test_expiry_nonexistent_user run_test test_expiry_multiple_users run_test test_expiry_shadow_dependencies run_test test_expiry_recent_password run_test test_expiry_max_password_age run_test test_expiry_warning_period run_test test_expiry_root_user run_test test_cleanup_expiry_users # Print summary print_summary account-utils-1.3.0/tests/ci/test-pam.sh000077500000000000000000000033371521474342200201450ustar00rootroot00000000000000#!/bin/bash # Integration tests for PAM module SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" # Detect system library directory (lib or lib64) case "$(uname -m)" in x86_64|aarch64|ppc64|ppc64le|s390x|riscv64|mips64|mips64el) LIBDIR="lib64" ;; *) LIBDIR="lib" ;; esac PAM_MODULE_PATH="/usr/$LIBDIR/security/pam_unix_ng.so" log_info "=========================================" log_info "PAM Module Integration Tests" log_info "=========================================" log_info "Using library directory: /usr/$LIBDIR" # Test 1: PAM module exists test_pam_module_exists() { log_test "Testing PAM module installation" container_exec test -f "$PAM_MODULE_PATH" assert_success $? "pam_unix_ng.so module exists at $PAM_MODULE_PATH" } # Test 2: PAM module is loadable test_pam_module_loadable() { log_test "Testing PAM module loadability" # Check if module has correct permissions container_exec test -r "$PAM_MODULE_PATH" assert_success $? "pam_unix_ng.so is readable at $PAM_MODULE_PATH" } # Test 3: Verify module library dependencies test_pam_module_dependencies() { log_test "Testing PAM module library dependencies" # Check if pam_unix_ng.so can be loaded (dependencies satisfied) # We can't run ldd in the container easily, but we can check file type local file_type file_type=$(container_exec file "$PAM_MODULE_PATH" | grep -o "shared object") assert_equals "$file_type" "shared object" "PAM module is a valid shared object" } # Run all tests log_info "Starting PAM module tests" echo "" run_test test_pam_module_exists run_test test_pam_module_loadable run_test test_pam_module_dependencies # Print summary print_summary account-utils-1.3.0/tests/ci/test-passwd.sh000077500000000000000000000335461521474342200206760ustar00rootroot00000000000000#!/bin/bash # Integration tests for passwd (change user password) # Tests actual password modification via varlink communication with pwupdd service # Running as root bypasses PAM authentication requirements SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" log_info "=========================================" log_info "passwd Integration Tests" log_info "=========================================" # Helper function to get shadow password field get_shadow_passwd() { local username="$1" container_exec getent shadow "$username" 2>/dev/null | cut -d: -f2 } # Helper function to get shadow last change field get_shadow_lstchg() { local username="$1" container_exec getent shadow "$username" 2>/dev/null | cut -d: -f3 } # Check if password field is locked (starts with !) is_password_locked() { local username="$1" local passwd_field=$(get_shadow_passwd "$username") [[ "$passwd_field" == !* ]] } # Check if password is empty is_password_empty() { local username="$1" local passwd_field=$(get_shadow_passwd "$username") [ -z "$passwd_field" ] || [ "$passwd_field" = "" ] } # Test 1: passwd binary exists and is executable test_passwd_binary_exists() { log_test "Testing passwd binary availability" container_exec test -x /usr/bin/passwd assert_success $? "passwd binary exists and is executable" } # Test 2: passwd --help works test_passwd_help() { log_test "Testing passwd --help option" local output=$(container_exec /usr/bin/passwd --help 2>&1) local exit_code=$? assert_success "$exit_code" "passwd --help exits successfully" assert_contains "$output" "password" "Help mentions password" assert_contains "$output" "delete" "Help includes --delete option" assert_contains "$output" "expire" "Help includes --expire option" assert_contains "$output" "lock" "Help includes --lock option" assert_contains "$output" "unlock" "Help includes --unlock option" } # Test 3: passwd --version works test_passwd_version() { log_test "Testing passwd --version option" local output=$(container_exec /usr/bin/passwd --version 2>&1) local exit_code=$? assert_success "$exit_code" "passwd --version exits successfully" assert_contains "$output" "passwd" "Version output contains program name" } # Test 4: Create test users test_create_test_users() { log_test "Creating test users for passwd tests" # Create user create_test_user "passwdtest1" "TestPass123" # Verify user has password set local passwd_field=$(get_shadow_passwd "passwdtest1") assert_not_equals "$passwd_field" "" "Test user has password field" assert_not_equals "$passwd_field" "!" "Test user is not locked" } # Test 5: Test pwupdd socket availability test_pwupdd_socket() { log_test "Testing pwupdd socket availability for passwd" # passwd communicates with pwupdd via varlink container_exec test -S /run/account/pwupd-socket assert_success $? "pwupdd socket exists for passwd to use" # Check socket is accessible local perms=$(container_exec stat -c '%a' /run/account/pwupd-socket) assert_equals "$perms" "666" "pwupdd socket is accessible (mode 666)" } # Test 6: passwd -s (--stdin) changes password test_passwd_stdin() { log_test "Testing passwd -s / --stdin option" # Get current password hash local passwd_before=$(get_shadow_passwd "passwdtest1") # Change password via stdin as root echo "NewPassword456" | container_exec /usr/bin/passwd -s passwdtest1 2>/dev/null local exit_code=$? assert_success "$exit_code" "passwd -s succeeded" # Get new password hash local passwd_after=$(get_shadow_passwd "passwdtest1") # Password hash should have changed assert_not_equals "$passwd_after" "$passwd_before" "Password was changed" assert_not_equals "$passwd_after" "" "New password is set" } # Test 7: passwd -l (--lock) locks account test_passwd_lock() { log_test "Testing passwd -l / --lock option" # Ensure password is not locked initially local passwd_before=$(get_shadow_passwd "passwdtest1") if [[ "$passwd_before" == !* ]]; then # Unlock first if already locked container_exec /usr/bin/passwd -u passwdtest1 2>/dev/null passwd_before=$(get_shadow_passwd "passwdtest1") fi # Lock the account container_exec /usr/bin/passwd -l passwdtest1 local exit_code=$? assert_success "$exit_code" "passwd -l succeeded" # Verify password is locked (starts with !) local passwd_after=$(get_shadow_passwd "passwdtest1") if [[ "$passwd_after" == !* ]]; then assert_success 0 "Password is locked (starts with !)" else assert_success 1 "Password should start with ! when locked" fi } # Test 8: passwd -u (--unlock) unlocks account test_passwd_unlock() { log_test "Testing passwd -u / --unlock option" # Ensure account is locked first if ! is_password_locked "passwdtest1"; then container_exec /usr/bin/passwd -l passwdtest1 2>/dev/null fi # Unlock the account container_exec /usr/bin/passwd -u passwdtest1 local exit_code=$? assert_success "$exit_code" "passwd -u succeeded" # Verify password is unlocked (does not start with !) local passwd_after=$(get_shadow_passwd "passwdtest1") if [[ "$passwd_after" != !* ]]; then assert_success 0 "Password is unlocked (does not start with !)" else assert_success 1 "Password should not start with ! when unlocked" fi } # Test 9: passwd -d (--delete) deletes password test_passwd_delete() { log_test "Testing passwd -d / --delete option" # Ensure user has a password echo "TestPassword789" | container_exec /usr/bin/passwd -s passwdtest1 2>/dev/null # Delete the password container_exec /usr/bin/passwd -d passwdtest1 local exit_code=$? assert_success "$exit_code" "passwd -d succeeded" # Verify password field is empty or contains empty password indicator local passwd_after=$(get_shadow_passwd "passwdtest1") if [ -z "$passwd_after" ] || [ "$passwd_after" = "" ]; then assert_success 0 "Password deleted (empty field)" else log_info "Password field after delete: '$passwd_after'" # Empty password might be represented differently fi } # Test 10: passwd -e (--expire) expires password test_passwd_expire() { log_test "Testing passwd -e / --expire option" # Set a password first echo "ExpireTest123" | container_exec /usr/bin/passwd -s passwdtest1 2>/dev/null # Expire the password container_exec /usr/bin/passwd -e passwdtest1 local exit_code=$? assert_success "$exit_code" "passwd -e succeeded" # Get last change after expiring local lstchg_after=$(get_shadow_lstchg "passwdtest1") # Last change should be 0 (forces change on next login) assert_equals "$lstchg_after" "0" "Last change set to 0 (password expired)" } # Test 11: passwd -S (--status) displays status test_passwd_status() { log_test "Testing passwd -S / --status option" # Set password first echo "StatusTest123" | container_exec /usr/bin/passwd -s passwdtest1 2>/dev/null # Get status local output=$(container_exec /usr/bin/passwd -S passwdtest1 2>&1) local exit_code=$? assert_success "$exit_code" "passwd -S succeeded" # Output should contain username and status information assert_contains "$output" "passwdtest1" "Status output contains username or shows status" } # Test 12: Test lock/unlock cycle test_lock_unlock_cycle() { log_test "Testing lock/unlock cycle" # Set a password echo "CycleTest123" | container_exec /usr/bin/passwd -s passwdtest1 2>/dev/null local passwd_initial=$(get_shadow_passwd "passwdtest1") # Lock container_exec /usr/bin/passwd -l passwdtest1 local passwd_locked=$(get_shadow_passwd "passwdtest1") assert_not_equals "$passwd_locked" "$passwd_initial" "Password changed when locked" # Unlock container_exec /usr/bin/passwd -u passwdtest1 local passwd_unlocked=$(get_shadow_passwd "passwdtest1") # After unlock, password should be similar to initial (minus the !) if [[ "$passwd_locked" == !* ]]; then local expected_unlocked="${passwd_locked#!}" assert_equals "$passwd_unlocked" "$expected_unlocked" "Unlock removes ! prefix" fi } # Test 13: Test passwd on different user (root changing another user) test_passwd_different_user() { log_test "Testing passwd on different user" # Create second test user create_test_user "passwdtest2" "TestPass456" local passwd_before=$(get_shadow_passwd "passwdtest2") # Change password for different user as root echo "NewPassword999" | container_exec /usr/bin/passwd -s passwdtest2 2>/dev/null local exit_code=$? assert_success "$exit_code" "passwd on different user succeeded" local passwd_after=$(get_shadow_passwd "passwdtest2") assert_not_equals "$passwd_after" "$passwd_before" "Different user's password changed by root" # Cleanup delete_test_user "passwdtest2" } # Test 14: Test sequential password changes test_sequential_changes() { log_test "Testing sequential password changes" # Create fresh user create_test_user "passwdtest3" "TestPass789" # First change echo "Password1" | container_exec /usr/bin/passwd -s passwdtest3 2>/dev/null local passwd1=$(get_shadow_passwd "passwdtest3") # Second change echo "Password2" | container_exec /usr/bin/passwd -s passwdtest3 2>/dev/null local passwd2=$(get_shadow_passwd "passwdtest3") # Third change echo "Password3" | container_exec /usr/bin/passwd -s passwdtest3 2>/dev/null local passwd3=$(get_shadow_passwd "passwdtest3") # All should be different assert_not_equals "$passwd1" "$passwd2" "First and second passwords different" assert_not_equals "$passwd2" "$passwd3" "Second and third passwords different" assert_not_equals "$passwd1" "$passwd3" "First and third passwords different" # Cleanup delete_test_user "passwdtest3" } # Test 15: Test expire then set new password test_expire_then_change() { log_test "Testing expire followed by password change" # Set initial password echo "Initial123" | container_exec /usr/bin/passwd -s passwdtest1 2>/dev/null # Expire it container_exec /usr/bin/passwd -e passwdtest1 local lstchg_expired=$(get_shadow_lstchg "passwdtest1") assert_equals "$lstchg_expired" "0" "Password expired (lstchg=0)" # Set new password echo "AfterExpire456" | container_exec /usr/bin/passwd -s passwdtest1 2>/dev/null # Last change should be updated (not 0 anymore) local lstchg_after=$(get_shadow_lstchg "passwdtest1") assert_not_equals "$lstchg_after" "0" "Last change updated after password change" } # Test 16: Test delete then lock test_delete_then_lock() { log_test "Testing delete followed by lock" # Set password echo "DeleteLock123" | container_exec /usr/bin/passwd -s passwdtest1 2>/dev/null # Delete it container_exec /usr/bin/passwd -d passwdtest1 # Lock it container_exec /usr/bin/passwd -l passwdtest1 local passwd_locked=$(get_shadow_passwd "passwdtest1") # Locked empty password should start with ! if [[ "$passwd_locked" == !* ]]; then assert_success 0 "Locked empty password starts with !" else log_info "Password after delete-then-lock: '$passwd_locked'" fi } # Test 17: Test multiple users independent passwords test_independent_passwords() { log_test "Testing users have independent passwords" # Create two users create_test_user "passwdtest4" "TestPass111" create_test_user "passwdtest5" "TestPass222" # Set different passwords echo "UserFourPass" | container_exec /usr/bin/passwd -s passwdtest4 2>/dev/null echo "UserFivePass" | container_exec /usr/bin/passwd -s passwdtest5 2>/dev/null local passwd4=$(get_shadow_passwd "passwdtest4") local passwd5=$(get_shadow_passwd "passwdtest5") assert_not_equals "$passwd4" "" "User4 has password" assert_not_equals "$passwd5" "" "User5 has password" assert_not_equals "$passwd4" "$passwd5" "Users have different password hashes" # Cleanup delete_test_user "passwdtest4" delete_test_user "passwdtest5" } # Test 18: Test passwd with non-existent user (should fail) test_passwd_nonexistent_user() { log_test "Testing passwd with non-existent user" # Attempt to change password for non-existent user echo "Password" | container_exec /usr/bin/passwd -s nonexistentuser99999 2>/dev/null local exit_code=$? assert_failure "$exit_code" "passwd fails gracefully with non-existent user" } # Test 19: Test pwaccess dependency test_pwaccess_socket() { log_test "Testing pwaccess socket availability for user lookup" # passwd uses pwaccess to get user information container_exec test -S /run/account/pwaccess-socket assert_success $? "pwaccess socket available for passwd user lookup" } # Test 20: Cleanup test users test_cleanup_passwd_users() { log_test "Cleaning up passwd test users" delete_test_user "passwdtest1" # Verify cleanup local user_gone=$(container_exec getent passwd passwdtest1 && echo "no" || echo "yes") assert_equals "$user_gone" "yes" "Test user removed successfully" } # Run all tests log_info "Starting passwd integration tests" log_info "Running as root - tests actual password modifications" echo "" run_test test_passwd_binary_exists run_test test_passwd_help run_test test_passwd_version run_test test_create_test_users run_test test_pwupdd_socket run_test test_passwd_stdin run_test test_passwd_lock run_test test_passwd_unlock run_test test_passwd_delete run_test test_passwd_expire run_test test_passwd_status run_test test_lock_unlock_cycle run_test test_passwd_different_user run_test test_sequential_changes run_test test_expire_then_change run_test test_delete_then_lock run_test test_independent_passwords run_test test_passwd_nonexistent_user run_test test_pwaccess_socket run_test test_cleanup_passwd_users # Print summary print_summary account-utils-1.3.0/tests/ci/test-pwaccessd.sh000077500000000000000000000121051521474342200213350ustar00rootroot00000000000000#!/bin/bash # Integration tests for pwaccessd service SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" log_info "=========================================" log_info "pwaccessd Integration Tests" log_info "=========================================" # Test 1: Socket activation test_socket_activation() { log_test "Testing socket activation" # Socket should exist if container_exec test -S /run/account/pwaccess-socket; then assert_success 0 "pwaccessd socket exists" else assert_failure 0 "pwaccessd socket exists" return 1 fi # Service should not be running initially (socket activated) local is_active=$(container_exec systemctl is-active pwaccessd.service || echo "inactive") log_info "Service state: $is_active" } # Test 2: Basic user lookup test_user_lookup_root() { log_test "Testing root user lookup" # Create a simple test client using varlink CLI if available # For now, we'll verify the socket responds local result=$(container_exec test -S /run/account/pwaccess-socket && echo "success" || echo "failed") assert_equals "$result" "success" "Socket is accessible" } # Test 3: Create test user and verify access test_user_creation_and_lookup() { log_test "Testing user creation and lookup" # Create test user create_test_user "testuser1" "testpass123" # Verify user exists in passwd local user_exists=$(container_exec getent passwd testuser1 >/dev/null 2>&1 && echo "yes" || echo "no") assert_equals "$user_exists" "yes" "Test user exists in passwd" # Cleanup delete_test_user "testuser1" } # Test 4: Multiple users test_multiple_users() { log_test "Testing multiple user management" create_test_user "testuser2" "pass2" 2001 create_test_user "testuser3" "pass3" 2002 # Both users should exist local user2_exists=$(container_exec getent passwd testuser2 >/dev/null 2>&1 && echo "yes" || echo "no") local user3_exists=$(container_exec getent passwd testuser3 >/dev/null 2>&1 && echo "yes" || echo "no") assert_equals "$user2_exists" "yes" "testuser2 exists" assert_equals "$user3_exists" "yes" "testuser3 exists" # Cleanup delete_test_user "testuser2" delete_test_user "testuser3" } # Test 5: Verify socket permissions test_socket_permissions() { log_test "Testing socket permissions" # Socket should be world-accessible (mode 0666) local perms=$(container_exec stat -c '%a' /run/account/pwaccess-socket) assert_equals "$perms" "666" "Socket has correct permissions (666)" } # Test 6: Service starts on socket access test_service_activation() { log_test "Testing service activation on socket access" # First ensure service is inactive container_exec systemctl stop pwaccessd.service 2>/dev/null || true sleep 1 # Access the socket (this would normally trigger activation) # Since we don't have varlink CLI in the container, we'll just test socket availability local socket_ready=$(container_exec test -S /run/account/pwaccess-socket && echo "ready" || echo "not ready") assert_equals "$socket_ready" "ready" "Socket is ready for activation" } # Test 7: Passwd file integrity test_passwd_integrity() { log_test "Testing passwd file integrity" # Root user should always exist local root_entry=$(container_exec grep "^root:" /etc/passwd | cut -d: -f1) assert_equals "$root_entry" "root" "Root user exists in passwd" # Passwd file should be readable container_exec test -r /etc/passwd assert_success $? "Passwd file is readable" } # Test 8: Shadow file security test_shadow_security() { log_test "Testing shadow file security" # Shadow file should have restricted permissions local shadow_perms=$(container_exec stat -c '%a' /etc/shadow) assert_equals "$shadow_perms" "600" "Shadow file has secure permissions (600)" # Shadow file should be owned by root local shadow_owner=$(container_exec stat -c '%U' /etc/shadow) assert_equals "$shadow_owner" "root" "Shadow file is owned by root" } # Test 9: Service logging test_service_logging() { log_test "Testing service logging" # Check if we can read journal for the service container_exec journalctl -u pwaccessd.socket --no-pager -n 5 >/dev/null assert_success $? "Can read service logs" } # Test 10: Socket restart test_socket_restart() { log_test "Testing socket restart" # Restart socket container_exec systemctl restart pwaccessd.socket assert_success $? "Socket restart succeeded" # Wait for socket to be ready sleep 2 # Verify socket is available wait_for_socket "/run/account/pwaccess-socket" 10 assert_success $? "Socket is available after restart" } # Run all tests log_info "Starting pwaccessd tests" echo "" run_test test_socket_activation run_test test_user_lookup_root run_test test_user_creation_and_lookup run_test test_multiple_users run_test test_socket_permissions run_test test_service_activation run_test test_passwd_integrity run_test test_shadow_security run_test test_service_logging run_test test_socket_restart # Print summary print_summary account-utils-1.3.0/tests/ci/test-pwupdd.sh000077500000000000000000000130101521474342200206600ustar00rootroot00000000000000#!/bin/bash # Integration tests for pwupdd service SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" log_info "=========================================" log_info "pwupdd Integration Tests" log_info "=========================================" # Test 1: Socket activation test_socket_activation() { log_test "Testing pwupdd socket activation" # Socket should exist if container_exec test -S /run/account/pwupd-socket; then assert_success 0 "pwupdd socket exists" else assert_failure 0 "pwupdd socket exists" return 1 fi } # Test 2: Socket permissions test_socket_permissions() { log_test "Testing pwupdd socket permissions" # Socket should be world-accessible (mode 0666) local perms=$(container_exec stat -c '%a' /run/account/pwupd-socket) assert_equals "$perms" "666" "Socket has correct permissions (666)" } # Test 3: Accept socket configuration test_accept_socket() { log_test "Testing Accept=yes socket configuration" # Check socket unit configuration local accept_setting=$(container_exec systemctl show pwupdd.socket -p Accept --value) assert_equals "$accept_setting" "yes" "Socket has Accept=yes configured" } # Test 4: Socket directory permissions test_socket_directory() { log_test "Testing socket directory" # Directory should exist container_exec test -d /run/account assert_success $? "/run/account directory exists" # Directory should have correct permissions (0755) local dir_perms=$(container_exec stat -c '%a' /run/account) assert_equals "$dir_perms" "755" "Socket directory has correct permissions (755)" } # Test 5: MaxConnectionsPerSource setting test_max_connections() { log_test "Testing MaxConnectionsPerSource configuration" # Check the setting local max_conn=$(container_exec systemctl show pwupdd.socket -p MaxConnectionsPerSource --value) assert_equals "$max_conn" "16" "MaxConnectionsPerSource is set to 16" } # Test 6: User password change capability test_password_change_setup() { log_test "Testing password change setup" # Create a test user create_test_user "pwtest1" "oldpass123" # Verify user exists local user_exists=$(container_exec getent passwd pwtest1 >/dev/null 2>&1 && echo "yes" || echo "no") assert_equals "$user_exists" "yes" "Test user for password change exists" # Cleanup delete_test_user "pwtest1" } # Test 7: Multiple concurrent connections (simulated) test_concurrent_access() { log_test "Testing socket can handle connection setup" # Since pwupdd is an Accept=yes socket, each connection spawns a new instance # We verify the socket configuration allows this local socket_type=$(container_exec systemctl show pwupdd.socket -p Accept --value) assert_equals "$socket_type" "yes" "Socket configured for concurrent connections" } # Test 8: Service binary exists test_service_binary() { log_test "Testing pwupdd binary availability" container_exec test -x /usr/libexec/pwupdd assert_success $? "pwupdd binary exists and is executable" } # Test 9: Varlink socket readiness test_varlink_readiness() { log_test "Testing varlink socket readiness" # Socket should be a Unix socket local socket_type=$(container_exec stat -c '%F' /run/account/pwupd-socket) assert_equals "$socket_type" "socket" "pwupd-socket is a Unix socket" } # Test 10: Socket restart resilience test_socket_restart() { log_test "Testing socket restart" # Restart socket container_exec systemctl restart pwupdd.socket assert_success $? "Socket restart succeeded" # Wait for socket to be ready sleep 2 # Verify socket is available wait_for_socket "/run/account/pwupd-socket" 10 assert_success $? "Socket is available after restart" } # Test 11: Service logging test_service_logging() { log_test "Testing service logging" # Check if we can read journal for the socket container_exec journalctl -u pwupdd.socket --no-pager -n 5 >/dev/null assert_success $? "Can read socket logs" } # Test 12: Test user shell change preparation test_shell_change_setup() { log_test "Testing shell change capability setup" # Create test user create_test_user "shelltest1" "testpass" # Verify user's shell is recorded local user_shell=$(container_exec getent passwd shelltest1 | cut -d: -f7) log_info "User shell: $user_shell" # Shell field should not be empty assert_not_equals "$user_shell" "" "User has a shell configured" # Cleanup delete_test_user "shelltest1" } # Test 13: Test GECOS field access test_gecos_access() { log_test "Testing GECOS field accessibility" # Create test user with GECOS create_test_user "gecostest1" "testpass" # Read GECOS field local gecos=$(container_exec getent passwd gecostest1 | cut -d: -f5) log_info "GECOS field: '$gecos'" # GECOS field should exist (can be empty) container_exec getent passwd gecostest1 | cut -d: -f5 >/dev/null assert_success $? "GECOS field is accessible" # Cleanup delete_test_user "gecostest1" } # Run all tests log_info "Starting pwupdd tests" echo "" run_test test_socket_activation run_test test_socket_permissions run_test test_accept_socket run_test test_socket_directory run_test test_max_connections run_test test_password_change_setup run_test test_concurrent_access run_test test_service_binary run_test test_varlink_readiness run_test test_socket_restart run_test test_service_logging run_test test_shell_change_setup run_test test_gecos_access # Print summary print_summary account-utils-1.3.0/tests/ci/test-utils.sh000077500000000000000000000177371521474342200205410ustar00rootroot00000000000000#!/bin/bash # Common utilities for integration tests set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Test counters TESTS_RUN=0 TESTS_PASSED=0 TESTS_FAILED=0 # Logging functions log_info() { echo -e "${GREEN}[INFO]${NC} $*" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" } log_error() { echo -e "${RED}[ERROR]${NC} $*" } log_test() { echo -e "${YELLOW}[TEST]${NC} $*" } # Container management # These variables should be set by run-tests.sh # Default values only for backward compatibility (deprecated) if [ -z "$CONTAINER_ROOT" ]; then log_warn "CONTAINER_ROOT not set, using default (insecure)" CONTAINER_ROOT="/tmp/account-utils-test-container" fi if [ -z "$CONTAINER_NAME" ]; then log_warn "CONTAINER_NAME not set, using default" CONTAINER_NAME="account-utils-test" fi container_exec() { # Get the leader PID of the container local leader_pid=$(machinectl show -P Leader "$CONTAINER_NAME" 2>/dev/null) if [ -z "$leader_pid" ] || [ "$leader_pid" = "0" ]; then # Fallback: try to find via pgrep leader_pid=$(pgrep -f "systemd-nspawn.*$CONTAINER_NAME" | head -1) if [ -z "$leader_pid" ]; then log_error "Could not find container PID for $CONTAINER_NAME" return 1 fi fi # For commands that might be bash builtins (like test, [, etc.), wrap in bash -c # This ensures builtins work even if they're not in /usr/bin if [ "$1" = "test" ] || [ "$1" = "[" ]; then # Quote all arguments properly for bash -c local cmd="$1" shift nsenter -t "$leader_pid" -m -p -n -- /usr/bin/bash -c "$cmd $(printf '%q ' "$@")" else # Use nsenter to execute command in container namespace nsenter -t "$leader_pid" -m -p -n -- "$@" fi } container_exec_user() { local user="$1" shift # Get the leader PID of the container local leader_pid=$(machinectl show -P Leader "$CONTAINER_NAME" 2>/dev/null) if [ -z "$leader_pid" ] || [ "$leader_pid" = "0" ]; then leader_pid=$(pgrep -f "systemd-nspawn.*$CONTAINER_NAME" | head -1) if [ -z "$leader_pid" ]; then log_error "Could not find container PID for $CONTAINER_NAME" return 1 fi fi # Use nsenter with su to run as specific user nsenter -t "$leader_pid" -m -p -n -- su -s /bin/bash "$user" -c "$*" } wait_for_service() { local service="$1" local timeout="${2:-10}" local count=0 log_info "Waiting for service: $service" while [ $count -lt "$timeout" ]; do if container_exec systemctl is-active --quiet "$service" 2>/dev/null; then log_info "Service $service is active" return 0 fi sleep 1 count=$((count + 1)) done log_error "Service $service failed to start within ${timeout}s" return 1 } wait_for_socket() { local socket_path="$1" local timeout="${2:-10}" local count=0 log_info "Waiting for socket: $socket_path" while [ $count -lt "$timeout" ]; do # Use ls -la to check if socket exists (socket files show as 's' in permissions) if container_exec ls -la "$socket_path" 2>/dev/null | grep -q "^s"; then log_info "Socket $socket_path is ready" return 0 fi sleep 1 count=$((count + 1)) done log_error "Socket $socket_path not found within ${timeout}s" return 1 } # Test user management create_test_user() { local username="$1" local password="$2" local uid="${3:-}" log_info "Creating test user: $username" if [ -n "$uid" ]; then container_exec useradd -m -u "$uid" "$username" else container_exec useradd -m "$username" fi if [ -n "$password" ]; then echo "$username:$password" | container_exec chpasswd fi } delete_test_user() { local username="$1" log_info "Deleting test user: $username" container_exec userdel -r "$username" 2>/dev/null || true } # Test assertions assert_equals() { TESTS_RUN=$((TESTS_RUN + 1)) local actual="$1" local expected="$2" local message="${3:-Assertion failed}" if [ "$actual" = "$expected" ]; then log_info "✓ $message" TESTS_PASSED=$((TESTS_PASSED + 1)) return 0 else log_error "✗ $message" log_error " Expected: $expected" log_error " Actual: $actual" TESTS_FAILED=$((TESTS_FAILED + 1)) return 1 fi } assert_not_equals() { TESTS_RUN=$((TESTS_RUN + 1)) local actual="$1" local expected="$2" local message="${3:-Assertion failed}" if [ "$actual" != "$expected" ]; then log_info "✓ $message" TESTS_PASSED=$((TESTS_PASSED + 1)) return 0 else log_error "✗ $message" log_error " Should not equal: $expected" TESTS_FAILED=$((TESTS_FAILED + 1)) return 1 fi } assert_success() { TESTS_RUN=$((TESTS_RUN + 1)) local exit_code="$1" local message="${2:-Command should succeed}" if [ "$exit_code" -eq 0 ]; then log_info "✓ $message" TESTS_PASSED=$((TESTS_PASSED + 1)) return 0 else log_error "✗ $message (exit code: $exit_code)" TESTS_FAILED=$((TESTS_FAILED + 1)) return 1 fi } assert_failure() { TESTS_RUN=$((TESTS_RUN + 1)) local exit_code="$1" local message="${2:-Command should fail}" if [ "$exit_code" -ne 0 ]; then log_info "✓ $message" TESTS_PASSED=$((TESTS_PASSED + 1)) return 0 else log_error "✗ $message (command succeeded unexpectedly)" TESTS_FAILED=$((TESTS_FAILED + 1)) return 1 fi } assert_contains() { TESTS_RUN=$((TESTS_RUN + 1)) local haystack="$1" local needle="$2" local message="${3:-String should contain substring}" if echo "$haystack" | grep -qF -- "$needle"; then log_info "✓ $message" TESTS_PASSED=$((TESTS_PASSED + 1)) return 0 else log_error "✗ $message" log_error " Looking for: $needle" log_error " In: $haystack" TESTS_FAILED=$((TESTS_FAILED + 1)) return 1 fi } # Run a test function run_test() { local test_name="$1" log_test "Running: $test_name" if "$test_name"; then log_info "Test $test_name completed" else log_error "Test $test_name failed" fi echo "" } # Print test summary print_summary() { echo "" echo "=========================================" echo "Test Summary" echo "=========================================" echo "Total tests run: $TESTS_RUN" echo -e "${GREEN}Tests passed:${NC} $TESTS_PASSED" echo -e "${RED}Tests failed:${NC} $TESTS_FAILED" echo "=========================================" if [ "$TESTS_FAILED" -eq 0 ]; then echo -e "${GREEN}All tests passed!${NC}" return 0 else echo -e "${RED}Some tests failed!${NC}" return 1 fi } # Cleanup function cleanup_test_users() { log_info "Cleaning up test users" for user in testuser1 testuser2 testuser3 testadmin; do delete_test_user "$user" 2>/dev/null || true done } # Show recent journal logs for debugging show_journal() { local lines="${1:-50}" local unit="${2:-}" log_info "Recent journal logs:" echo "=========================================" if [ -n "$unit" ]; then container_exec journalctl -u "$unit" -n "$lines" --no-pager else container_exec journalctl -n "$lines" --no-pager fi echo "=========================================" } # Show service status for debugging show_service_status() { local service="$1" log_info "Status for service: $service" echo "=========================================" container_exec systemctl status "$service" --no-pager || true echo "" log_info "Recent logs for: $service" container_exec journalctl -u "$service" -n 20 --no-pager || true echo "=========================================" } account-utils-1.3.0/tests/ci/test-varlink-protocol.sh000077500000000000000000000157751521474342200227060ustar00rootroot00000000000000#!/bin/bash # Advanced integration tests for varlink protocol communication SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-utils.sh" log_info "=========================================" log_info "Varlink Protocol Integration Tests" log_info "=========================================" # Test: Verify varlink sockets are accessible test_varlink_sockets_ready() { log_test "Testing varlink socket accessibility" # Both sockets should exist and be Unix sockets container_exec test -S /run/account/pwaccess-socket assert_success $? "pwaccessd varlink socket exists" container_exec test -S /run/account/pwupd-socket assert_success $? "pwupdd varlink socket exists" } # Test: Socket ownership and permissions test_socket_security() { log_test "Testing socket security attributes" # Sockets should be owned by root local pwaccess_owner=$(container_exec stat -c '%U' /run/account/pwaccess-socket) assert_equals "$pwaccess_owner" "root" "pwaccessd socket owned by root" local pwupd_owner=$(container_exec stat -c '%U' /run/account/pwupd-socket) assert_equals "$pwupd_owner" "root" "pwupdd socket owned by root" } # Test: Socket file descriptor naming test_socket_fd_names() { log_test "Testing socket FileDescriptorName configuration" # Check unit configuration for FD names local pwaccess_fd=$(container_exec systemctl show pwaccessd.socket -p FileDescriptorName --value) assert_equals "$pwaccess_fd" "varlink" "pwaccessd socket FD name is 'varlink'" local pwupd_fd=$(container_exec systemctl show pwupdd.socket -p FileDescriptorName --value) assert_equals "$pwupd_fd" "varlink" "pwupdd socket FD name is 'varlink'" } # Test: Varlink GetInfo method test_varlink_getinfo() { log_test "Testing varlink GetInfo method" # Test org.varlink.service.GetInfo on pwaccessd local pwaccess_info=$(container_exec varlinkctl info unix:/run/account/pwaccess-socket) assert_success $? "pwaccessd GetInfo call succeeded" assert_contains "$pwaccess_info" "org.openSUSE.pwaccess" "pwaccessd exposes pwaccess interface" # Test org.varlink.service.GetInfo on pwupdd local pwupd_info=$(container_exec varlinkctl info unix:/run/account/pwupd-socket) assert_success $? "pwupdd GetInfo call succeeded" assert_contains "$pwupd_info" "org.openSUSE.pwupd" "pwupdd exposes pwupd interface" } # Test: Concurrent connection simulation test_concurrent_connection_readiness() { log_test "Testing concurrent connection handling readiness" # pwupdd should support multiple concurrent connections local max_conn=$(container_exec systemctl show pwupdd.socket -p MaxConnectionsPerSource --value) assert_equals "$max_conn" "16" "pwupdd configured for 16 concurrent connections" # Accept=yes means each connection gets its own service instance local accept=$(container_exec systemctl show pwupdd.socket -p Accept --value) assert_equals "$accept" "yes" "pwupdd uses Accept=yes for concurrent instances" log_info "Ready for concurrent connection testing" log_info "Future: Test actual concurrent varlink calls" } # Test: GetUserRecord method test_getuserrecord() { log_test "Testing GetUserRecord method" # Create a test user create_test_user "varlinkuser" "TestPass123" # Test GetUserRecord for existing user local result=$(container_exec varlinkctl call unix:/run/account/pwaccess-socket org.openSUSE.pwaccess.GetUserRecord '{"userName":"varlinkuser"}') assert_success $? "GetUserRecord call succeeded" assert_contains "$result" '"name":"varlinkuser"' "Response contains username" assert_contains "$result" '"UID":' "Response contains UID" assert_contains "$result" '"GID":' "Response contains GID" # Test GetUserRecord for non-existent user local result_notfound=$(container_exec varlinkctl call unix:/run/account/pwaccess-socket org.openSUSE.pwaccess.GetUserRecord '{"userName":"nonexistent"}' 2>&1 || true) assert_contains "$result_notfound" '"Success":false' "GetUserRecord returns error for non-existent user" # Cleanup delete_test_user "varlinkuser" } # Test: VerifyPassword method test_verifypassword() { log_test "Testing VerifyPassword method" # Create a test user create_test_user "pwverifyuser" "CorrectPass123" # Test with correct password local result_correct=$(container_exec varlinkctl call unix:/run/account/pwaccess-socket org.openSUSE.pwaccess.VerifyPassword '{"userName":"pwverifyuser","password":"CorrectPass123"}') assert_success $? "VerifyPassword succeeded with correct password" assert_contains "$result_correct" '"Success":true' "Password verified successfully" # Test with incorrect password local result_wrong=$(container_exec varlinkctl call unix:/run/account/pwaccess-socket org.openSUSE.pwaccess.VerifyPassword '{"userName":"pwverifyuser","password":"WrongPass"}' || true) assert_contains "$result_wrong" '"Success":false' "VerifyPassword returns false with wrong password" # Cleanup delete_test_user "pwverifyuser" } # Test: Chsh method test_chsh() { log_test "Testing Chsh method" # Create a test user create_test_user "shelluser" "ShellPass123" # Change shell to /bin/sh local result=$(container_exec varlinkctl call unix:/run/account/pwupd-socket org.openSUSE.pwupd.Chsh '{"userName":"shelluser","shell":"/bin/sh"}') assert_success $? "Chsh call succeeded" # Verify shell was changed local shell=$(container_exec getent passwd shelluser | cut -d: -f7) assert_equals "$shell" "/bin/sh" "Shell changed to /bin/sh" # Cleanup delete_test_user "shelluser" } # Test: Chfn method test_chfn() { log_test "Testing Chfn method" # Create a test user create_test_user "gecosuser" "GecosPass123" # Change GECOS field local result=$(container_exec varlinkctl call unix:/run/account/pwupd-socket org.openSUSE.pwupd.Chfn '{"userName":"gecosuser","fullName":"Test User","room":"Room 123","workPhone":"555-1234","homePhone":"555-5678"}') assert_success $? "Chfn call succeeded" # Verify GECOS was changed local gecos=$(container_exec getent passwd gecosuser | cut -d: -f5) assert_equals "$gecos" "Test User,Room 123,555-1234,555-5678" "GECOS field updated correctly" # Cleanup delete_test_user "gecosuser" } # Test: Error handling - invalid parameters test_error_handling() { log_test "Testing error handling with invalid parameters" # Test with malformed JSON container_exec varlinkctl call unix:/run/account/pwaccess-socket org.openSUSE.pwaccess.GetUserRecord 'invalid json' 2>&1 assert_not_equals $? 0 "Malformed JSON returns error" # Cleanup delete_test_user "erroruser" } # Run all tests log_info "Starting varlink protocol tests" echo "" run_test test_varlink_sockets_ready run_test test_socket_security run_test test_socket_fd_names run_test test_varlink_getinfo run_test test_concurrent_connection_readiness run_test test_getuserrecord run_test test_verifypassword run_test test_chsh run_test test_chfn run_test test_error_handling # Print summary print_summary account-utils-1.3.0/tests/meson.build000066400000000000000000000022021521474342200176110ustar00rootroot00000000000000# This file builds and runs the unit tests testdir = join_paths(meson.project_source_root(), 'tests/') test_args = ['-DTESTSDIR="' + testdir + '"'] srcdir = include_directories('../src') libdl = cc.find_library('dl') tst_dlopen_exe = executable('tst-dlopen', 'tst-dlopen.c', dependencies : libdl, include_directories : inc) test('tst-dlopen', tst_dlopen_exe, args : ['pam_unix_ng.so']) test('tst-dlopen', tst_dlopen_exe, args : ['pam_debuginfo.so']) tst_update_passwd_exe = executable('tst-update_passwd', 'tst-update_passwd.c', include_directories : [inc, srcdir], dependencies : [libselinux], c_args: test_args) test('tst-update_passwd', tst_update_passwd_exe) tst_read_config_exe = executable('tst-read_config', 'tst-read_config.c', '../libcommon/read_config.c', include_directories : [inc, srcdir], link_with : [libcommon], dependencies : [libeconf], c_args: ['-DTESTSDIR="' + testdir + 'tst-read_config-data' + '"']) test('tst-read_config', tst_read_config_exe) account-utils-1.3.0/tests/tst-dlopen.c000066400000000000000000000021311521474342200177050ustar00rootroot00000000000000/* Copyright (C) Nalin Dahyabhai 2003 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 2 of the License, or (at your option) any later version. */ #include #include #include #include #include "basics.h" /* Simple program to see if dlopen() would succeed. */ int main(int argc, char **argv) { int i; int rc; struct stat st; _cleanup_(freep) char *buf = NULL; for (i = 1; i < argc; i++) { if (dlopen(argv[i], RTLD_NOW)) { fprintf(stdout, "dlopen() of \"%s\" succeeded.\n", argv[i]); } else { rc = asprintf(&buf, "./%s", argv[i]); if (rc >= 0 && (stat(buf, &st) == 0) && dlopen(buf, RTLD_NOW)) { fprintf(stdout, "dlopen() of \"./%s\" " "succeeded.\n", argv[i]); } else { fprintf(stdout, "dlopen() of \"%s\" failed: " "%s\n", argv[i], dlerror()); return 1; } } } return 0; } account-utils-1.3.0/tests/tst-pam_unix_ng.sh000077500000000000000000000102271521474342200211300ustar00rootroot00000000000000#!/bin/bash # --- Configuration --- TEST_USER="pam_testuser" TEST_PASSWD=$(tr -dc A-Za-z0-9 /dev/null; then userdel -r "$TEST_USER" &>/dev/null fi } # Ensure cleanup runs on exit or script failure trap cleanup EXIT function expect_success { if [ $? -eq 0 ]; then echo -e "\033[32mSUCCESS:\033[0m $1" else echo -e "\033[31mFAILURE:\033[0m $1" exit 1 fi } function expect_failure { if [ $? -ne 0 ]; then echo -e "\033[32mSUCCESS:\033[0m $1" else echo -e "\033[31mFAILURE:\033[0m $1" exit 1 fi } # Create Test User echo "--- Creating user '$TEST_USER' and setting password" # user with empty password useradd -m "$TEST_USER" -p '' expect_success "User creation" echo "--- Login with empty password (DISALLOW_NULL_AUTTHOK)" echo '' | pamtester "$PAM_SERVICE" "$TEST_USER" "authenticate(PAM_DISALLOW_NULL_AUTHTOK)" expect_failure "Failure with empty password" echo "--- Login with empty password (nullok)" pamtester "$PAM_SERVICE" "$TEST_USER" authenticate expect_success "Login with empty password" # Set the 12-character password non-interactively echo "$TEST_USER:$TEST_PASSWD" | chpasswd expect_success "Password set to 12 characters" echo "--- Verifying initial login" echo "$TEST_PASSWD" | pamtester "$PAM_SERVICE" "$TEST_USER" authenticate acct_mgmt open_session close_session expect_success "Initial login successful via pamtester" echo "--- Verifying password expiration warning message" # The message pattern we expect depends on EXPIRY_DAYS EXPECTED_WARNING_PATTERN="your password will expire in $EXPIRY_DAYS days" echo "---- Setting password expiry periods" chage -M "$EXPIRY_DAYS" "$TEST_USER" expect_success "Set maximum password age to $EXPIRY_DAYS days" chage -W "$WARNING_DAYS" "$TEST_USER" expect_success "Set password warning period to $WARNING_DAYS days" echo "Current password settings for $TEST_USER:" chage -l "$TEST_USER" # Run pamtester in verbose mode and pipe output to grep if echo "$TEST_PASSWD" | pamtester --verbose "$PAM_SERVICE" "$TEST_USER" authenticate acct_mgmt 2>&1 | grep -q -i "$EXPECTED_WARNING_PATTERN"; then expect_success "Warning message successfully found: '$EXPECTED_WARNING_PATTERN'" else # Try one last time and print the full output for debugging failure echo "Debugging failure: Full pamtester output:" echo "$TEST_PASSWD" | pamtester --verbose "$PAM_SERVICE" "$TEST_USER" authenticate acct_mgmt 2>&1 # Check for the exit code of grep -q, not the full command. expect_success "Warning message verification failed (check output above)" fi echo "--- Verify inactive password" # The message pattern we expect depends on EXPIRY_DAYS EXPECTED_EXPIRED_PASSWORD_MSG="Authentication token expired" echo "---- Setting password expiry periods" chage --lastday "$(date -d "14 days ago" +%Y-%m-%d)" "$TEST_USER" expect_success "Set lastday to 14 days ago" chage --maxdays 7 "$TEST_USER" expect_success "Set maximum password age to $EXPIRY_DAYS days" chage --inactive 4 "$TEST_USER" expect_success "Set password inactive days to 4" echo "Current password settings for $TEST_USER:" chage -l "$TEST_USER" # Run pamtester in verbose mode and pipe output to grep if echo "$TEST_PASSWD" | pamtester --verbose "$PAM_SERVICE" "$TEST_USER" authenticate acct_mgmt 2>&1 | grep -q -i "$EXPECTED_EXPIRED_PASSWORD_MSG"; then expect_success "Expired message successfully found: '$EXPECTED_EXPIRED_PASSWORD_MSG'" else # Try one last time and print the full output for debugging failure echo "Debugging failure: Full pamtester output:" echo "$TEST_PASSWD" | pamtester --verbose "$PAM_SERVICE" "$TEST_USER" authenticate acct_mgmt 2>&1 # Check for the exit code of grep -q, not the full command. expect_success "Warning message verification failed (check output above)" fi echo "--- Script finished successfully" account-utils-1.3.0/tests/tst-read_config-data/000077500000000000000000000000001521474342200214325ustar00rootroot00000000000000account-utils-1.3.0/tests/tst-read_config-data/etc/000077500000000000000000000000001521474342200222055ustar00rootroot00000000000000account-utils-1.3.0/tests/tst-read_config-data/etc/account-utils/000077500000000000000000000000001521474342200247775ustar00rootroot00000000000000account-utils-1.3.0/tests/tst-read_config-data/etc/account-utils/pwaccessd.conf.d/000077500000000000000000000000001521474342200301215ustar00rootroot00000000000000account-utils-1.3.0/tests/tst-read_config-data/etc/account-utils/pwaccessd.conf.d/test0.conf000066400000000000000000000000371521474342200320270ustar00rootroot00000000000000[GetUserRecord] allow= nobody account-utils-1.3.0/tests/tst-read_config.c000066400000000000000000000030671521474342200206750ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include #include "basics.h" #include "read_config.h" static void print_allow(const char *group, uid_t *list) { printf("Group [%s]:", group); if (list == NULL) { fputs(" \n", stdout); return; } for (size_t i = 0; list[i] != 0; i++) printf(" %u", list[i]); fputs("\n", stdout); return; } int main(int argc _unused_, char **argv _unused_) { _cleanup_(struct_config_free) struct config_t cfg = {NULL, NULL, NULL}; econf_err error = read_config(&cfg); if (error != ECONF_SUCCESS) { fprintf(stderr, "read_config failed: %s\n", econf_errString(error)); return error; } print_allow("GetUserRecord", cfg.allow_get_user_record); print_allow("VerifyPassword", cfg.allow_verify_password); print_allow("ExpiredCheck", cfg.allow_expired_check); if (cfg.allow_get_user_record == NULL) { printf("GetUserRecord: UID of nobody not found!\n"); return 1; } if (cfg.allow_get_user_record[0] != 65534) { printf("GetUserRecord: first UID is not nobody but '%u'!\n", cfg.allow_get_user_record[0]); return 1; } if (cfg.allow_get_user_record[1] != 0) { printf("GetUserRecord: second entry is not 0 but '%u'!\n", cfg.allow_get_user_record[1]); return 1; } if (cfg.allow_verify_password != NULL) { printf("VerifyPassword is not NULL!\n"); return 1; } if (cfg.allow_expired_check != NULL) { printf("ExpiredCheck is not NULL!\n"); return 1; } return 0; } account-utils-1.3.0/tests/tst-update_passwd.c000066400000000000000000000012061521474342200212710ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include #include "basics.h" #include "files.h" #include "files.c" #include "string-util-fundamental.c" int main(int argc _unused_, char **argv _unused_) { struct passwd pwd; int r; r = update_passwd(NULL, NULL); if (r != -EINVAL) { fprintf(stderr, "update_passwd(NULL, NULL) did return %i\n", r); return 1; } memset(&pwd, 0, sizeof(pwd)); pwd.pw_name = "test0"; r = update_passwd(&pwd, TESTSDIR"tst-update_passwd/etc"); if (r != 0) { fprintf(stderr, "update_passwd() did return %i\n", r); return 1; } return 0; } account-utils-1.3.0/tests/tst-update_passwd/000077500000000000000000000000001521474342200211265ustar00rootroot00000000000000account-utils-1.3.0/tests/tst-update_passwd/etc/000077500000000000000000000000001521474342200217015ustar00rootroot00000000000000account-utils-1.3.0/tests/tst-update_passwd/etc/passwd000066400000000000000000000000511521474342200231210ustar00rootroot00000000000000test0:x:1001:1001::/home/test0:/bin/bash account-utils-1.3.0/tools/000077500000000000000000000000001521474342200154515ustar00rootroot00000000000000account-utils-1.3.0/tools/Dockerfile000066400000000000000000000016261521474342200174500ustar00rootroot00000000000000#!BuildTag: dump-privs:0.4.0 #!BuildTag: dump-privs:latest #!BuildTag: dump-privs:0.4.0-%RELEASE% #!UseOBSRepositories FROM opensuse/tumbleweed:latest AS build-stage WORKDIR /src RUN zypper clean && zypper ref -f && zypper --non-interactive install --no-recommends gcc libselinux-devel COPY dump-privs.c . RUN gcc -Wall -O2 dump-privs.c -o dump-privs -lselinux FROM opensuse/busybox:latest LABEL maintainer="Thorsten Kukuk " ARG BUILDTIME= ARG VERSION=0.4.0 LABEL org.opencontainers.image.title="dump-privs container" LABEL org.opencontainers.image.description="Container printing all relevant privileges of an application inside the container" LABEL org.opencontainers.image.created=$BUILDTIME LABEL org.opencontainers.image.version=$VERSION COPY --from=build-stage /src/dump-privs /usr/bin RUN chmod u+s,g+s /usr/bin/dump-privs RUN adduser -S -D -H dump-privs USER dump-privs CMD ["dump-privs"] account-utils-1.3.0/tools/dump-privs.c000066400000000000000000000115261521474342200177300ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include #include #include #include #ifdef WITH_SELINUX #include #endif static void print_usage(FILE *stream) { fprintf(stream, "Usage: dump-privs [options]\n"); } static void print_help(void) { fprintf(stdout, "dump-privs - dump process privileges\n\n"); print_usage(stdout); fputs(" -e, --enivronment Print enviornment variables\n", stdout); fputs(" -w, --wait Wait for before exit\n", stdout); fputs(" -h, --help Give this help list\n", stdout); fputs(" -v, --version Print program version\n", stdout); } static void print_error(void) { fprintf (stderr, "Try `dump-privs --help' for more information.\n"); } /* conpare function for qsort */ static int compare_str(const void *a, const void *b) { return strcmp(*(const char **)a, *(const char **)b); } static int agetgroups(int *ngids, gid_t **res) { gid_t *gids; int n; *ngids = 0; *res = NULL; n = getgroups(0, NULL); if (n == -1) return -errno; gids = calloc(n, sizeof(gid_t)); if (gids == NULL) return -ENOMEM; n = getgroups(n, gids); if (n == -1) { int r = errno; free(gids); return -r; } *ngids = n; *res = gids; return 0; } static const char * selinux_status(void) { #ifdef WITH_SELINUX if (is_selinux_enabled() > 0) { int r = security_getenforce(); switch (r) { case 1: return "enforcing"; break; case 0: return "permissive"; break; default: fprintf(stderr, "selinux error: %s\n", strerror(errno)); return "error"; break; } } else return "off"; #else return "not available"; #endif } int main(int argc, char **argv) { bool wait = false; bool printenv = false; int ngids = 0; gid_t *gids = NULL; #ifdef WITH_SELINUX char *secon = NULL; #endif char *cwd = NULL; int no_new_privs = prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0); const char *sestatus = selinux_status(); int r; while (1) { int c; int option_index = 0; static struct option long_options[] = { {"environment", no_argument, NULL, 'e' }, {"help", no_argument, NULL, 'h' }, {"version", no_argument, NULL, 'v' }, {"wait", no_argument, NULL, 'w' }, {NULL, 0, NULL, '\0'} }; c = getopt_long (argc, argv, "ehvw", long_options, &option_index); if (c == (-1)) break; switch (c) { case 'e': printenv = true; break; case 'h': print_help(); return 0; case 'v': printf("dump-privs (%s) %s\n", PACKAGE, VERSION); return 0; case 'w': wait = true; break; default: print_error(); return 1; } } argc -= optind; argv += optind; if (argc > 0) { fprintf(stderr, "dump-privs: Too many arguments.\n"); print_error(); return EINVAL; } r = agetgroups(&ngids, &gids); if (r != 0) fprintf(stderr, "getgroups() failed: %s\n", strerror(-r)); printf("🔎 Process Security Information\n"); printf("-------------------------------\n"); printf("Real UID: %d\n", getuid()); printf("Effective UID: %d\n", geteuid()); printf("Real GID: %d\n", getgid()); printf("Effective GID: %d\n", getegid()); printf("Group memberships:"); for (int i = 0; i < ngids; i++) { if (i != 0) putchar(','); printf(" %jd", (intmax_t) gids[i]); } putchar('\n'); printf("SELinux Status: %s\n", sestatus); #ifdef WITH_SELINUX if (getcon(&secon) == 0) { size_t secon_len = strlen(secon); if (secon[secon_len-1] == '\n') secon[secon_len-1] = '\0'; printf("SELinux Context: %s\n", secon); freecon(secon); /* Free the memory allocated by getcon() */ } else fprintf(stderr, "SELinux Context: %s\n", strerror(errno)); #endif printf("NoNewPrivs Status: %s\n", no_new_privs==0?"off":"on"); cwd = get_current_dir_name(); if (cwd == NULL) fprintf(stderr, "Current directory: %s\n", strerror(errno)); else { printf("Current directory: %s\n", cwd); free(cwd); } if (printenv) { int count = 0; char **envp = environ; while (*envp != NULL) { count++; envp++; } qsort(environ, count, sizeof(char *), compare_str); envp = environ; fputs("Environement variables:\n", stdout); while (*envp != NULL) { printf("%s\n", *envp); envp++; } } if (wait) getchar(); return 0; } account-utils-1.3.0/tools/meson.build000066400000000000000000000004751521474342200176210ustar00rootroot00000000000000executable('dump-privs', 'dump-privs.c', include_directories : inc, dependencies : [libselinux], install : get_option('tools')) executable('scan-caps', 'scan-caps.c', include_directories : inc, dependencies : [libcap], install : get_option('tools')) account-utils-1.3.0/tools/scan-caps.c000066400000000000000000000117221521474342200174700ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-2.0-or-later #include "config.h" #include #include #include #include #include #include #include #include #include #include #include "basics.h" /* paths to check for setuid binaries or binaries with capabilities */ const char *search_paths[] = { "/usr/bin", "/usr/sbin", "/usr/lib", "/usr/lib64", "/usr/libexec", "/usr/local/bin", "/usr/local/libexec", NULL }; static void print_usage(FILE *stream) { fprintf(stream, "Usage: scan-caps [path]\n"); } static void print_help(void) { fprintf(stdout, "scan-caps - scan system for binaries with capabilities\n\n"); print_usage(stdout); fputs(" -h, --help Give this help list\n", stdout); fputs(" -v, --version Print program version\n", stdout); } static void print_error(void) { fprintf (stderr, "Try `scan-caps --help' for more information.\n"); } static void free_cap_text(char **p) { if (p == NULL || *p == NULL) return; cap_free(*p); *p = NULL; } /* Checks a specific file for setuid, setgid, or capabilities */ static int check_file(const char *file, struct stat st) { _cleanup_(free_cap_text) char *caps_text = NULL; cap_t caps; int is_suid = 0; int is_sgid = 0; int has_caps = 0; int r; /* check regular files only */ if (!S_ISREG(st.st_mode)) return 0; if (st.st_mode & S_ISUID) is_suid = 1; if (st.st_mode & S_ISGID) is_sgid = 1; /* cap_get_file returns NULL if the file has no capabilities or on error */ caps = cap_get_file(file); if (caps) { /* We have a cap object, but we need to ensure it's not empty */ caps_text = cap_to_text(caps, NULL); if (caps_text && strlen(caps_text) > 0) has_caps = 1; cap_free(caps); } else if (errno != ENODATA) { r = -errno; fprintf(stderr, "cap_get_file(%s) failed: %s\n", file, strerror(-r)); return r; } if (is_suid || is_sgid || has_caps) { printf("Found: %s [", file); if (is_suid) printf(" SUID "); if (is_sgid) printf(" SGID "); if (has_caps) printf(" CAP: %s ", caps_text); printf("]\n"); } return 0; } static inline void closedirp(DIR **p) { if (*p) { closedir(*p); *p = NULL; } } /* Recursively walks a directory. Ignore most errors, continue with other files and directories, but report error back. */ static int walk_directory(const char *dir_path) { _cleanup_(closedirp) DIR *dir; struct dirent *entry; struct stat st; int retval = 0; // zero or latest reported error int r; if (!(dir = opendir(dir_path))) { r = -errno; fprintf(stderr, "Cannot open directory '%s': %s\n", dir_path, strerror(-r)); return r; } while ((entry = readdir(dir)) != NULL) { _cleanup_free_ char *path = NULL; /* Skip . and .. */ if (streq(entry->d_name, ".") || streq(entry->d_name, "..")) continue; if (asprintf(&path, "%s/%s", dir_path, entry->d_name) < 0) return -ENOMEM; /* Don't follow symlinks */ r = lstat(path, &st); if (r < 0) { r = -errno; fprintf(stderr, "lstat(%s) failed: %s\n", path, strerror(-r)); retval = r; } if (S_ISLNK(st.st_mode)) continue; if (S_ISDIR(st.st_mode)) { r = walk_directory(path); /* Recurse into subdirectory */ if (r < 0) retval = r; } else { r = check_file(path, st); if (r < 0) retval = r; } } return retval; } int main(int argc, char **argv) { int retval = 0; int r; while (1) { int c; int option_index = 0; static struct option long_options[] = { {"help", no_argument, NULL, 'h' }, {"version", no_argument, NULL, 'v' }, {NULL, 0, NULL, '\0'} }; c = getopt_long (argc, argv, "hv", long_options, &option_index); if (c == (-1)) break; switch (c) { case 'h': print_help(); return 0; case 'v': printf("scan-caps (%s) %s\n", PACKAGE, VERSION); return 0; default: print_error(); return 1; } } argc -= optind; argv += optind; printf("🔎 Scanning for binaries with setuid, setgid, or capabilities...\n"); printf("----------------------------------------------------------------\n"); const char **scan_list = (argc > 0) ? (const char **)argv : search_paths; for (int i = 0; scan_list[i] != NULL; i++) { if (argc > 0 && i >= argc) break; r = walk_directory(scan_list[i]); if (r < 0) retval = -r; } printf("----------------------------------------------------------------\n"); if (retval != 0) printf("Scan incomplete due to errors.\n"); else printf("Scan complete.\n"); return retval; } account-utils-1.3.0/units/000077500000000000000000000000001521474342200154535ustar00rootroot00000000000000account-utils-1.3.0/units/meson.build000066400000000000000000000013331521474342200176150ustar00rootroot00000000000000configure_file( input: 'pwaccessd.service.in', output: 'pwaccessd.service', configuration: { 'LIBEXECDIR': libexecdir, }, install: true, install_dir: systemunitdir, ) install_data('pwaccessd.socket', install_dir : systemunitdir) configure_file( input: 'pwupdd@.service.in', output: 'pwupdd@.service', configuration: { 'LIBEXECDIR': libexecdir, }, install: true, install_dir: systemunitdir, ) install_data('pwupdd.socket', install_dir : systemunitdir) configure_file( input: 'newidmapd.service.in', output: 'newidmapd.service', configuration: { 'LIBEXECDIR': libexecdir, }, install: true, install_dir: systemunitdir, ) install_data('newidmapd.socket', install_dir : systemunitdir) account-utils-1.3.0/units/newidmapd.service.in000066400000000000000000000013411521474342200214110ustar00rootroot00000000000000[Unit] Description=Daemon to set map ranges Documentation=man:newidmapd(8) [Service] Type=notify Environment="NEWIDMAPD_OPTS=" EnvironmentFile=-/etc/default/account-utils ExecStart=@LIBEXECDIR@/newidmapd -s $NEWIDMAPD_OPTS IPAddressDeny=any LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes PrivateDevices=yes PrivateNetwork=yes PrivateTmp=yes ProcSubset=pid ProtectClock=yes ProtectControlGroups=yes ProtectHome=yes ProtectHostname=yes ProtectKernelLogs=yes ProtectKernelModules=yes ProtectKernelTunables=yes ProtectProc=invisible ProtectSystem=strict ReadWritePaths=/run/account /proc /etc/default RestrictAddressFamilies=AF_UNIX RestrictNamespaces=yes RestrictRealtime=true RestrictRealtime=yes RestrictSUIDSGID=yes account-utils-1.3.0/units/newidmapd.socket000066400000000000000000000003401521474342200206320ustar00rootroot00000000000000[Unit] Description=newidmap daemon socket Documentation=man:newidmapd(8) [Socket] ListenStream=/run/account/newidmapd-socket FileDescriptorName=varlink SocketMode=0666 DirectoryMode=0755 [Install] WantedBy=sockets.target account-utils-1.3.0/units/pwaccessd.service.in000066400000000000000000000014321521474342200214160ustar00rootroot00000000000000[Unit] Description=Daemon to access passwd, shadow and for authentication Documentation=man:pwaccessd(8) [Service] Type=notify Environment="PWACCESSD_OPTS=" EnvironmentFile=-/etc/default/account-utils ExecStart=@LIBEXECDIR@/pwaccessd -s $PWACCESSD_OPTS LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes PrivateDevices=yes PrivateTmp=yes ProcSubset=pid ProtectClock=yes ProtectControlGroups=yes ProtectHome=yes ProtectHostname=yes ProtectKernelLogs=yes ProtectKernelModules=yes ProtectKernelTunables=yes ProtectProc=invisible ProtectSystem=strict ReadWritePaths=/run/account /etc RestrictNamespaces=yes RestrictRealtime=true RestrictRealtime=yes RestrictSUIDSGID=yes # This don't work with NIS, LDAP, ... #IPAddressDeny=any #PrivateNetwork=yes #RestrictAddressFamilies=AF_UNIX account-utils-1.3.0/units/pwaccessd.socket000066400000000000000000000003371521474342200206440ustar00rootroot00000000000000[Unit] Description=pwaccess daemon socket Documentation=man:pwaccessd(8) [Socket] ListenStream=/run/account/pwaccess-socket FileDescriptorName=varlink SocketMode=0666 DirectoryMode=0755 [Install] WantedBy=sockets.target account-utils-1.3.0/units/pwupdd.socket000066400000000000000000000004421521474342200201700ustar00rootroot00000000000000[Unit] Description=Daemon to update passwd and shadow entries After=local-fs.target Before=sockets.target [Socket] ListenStream=/run/account/pwupd-socket FileDescriptorName=varlink SocketMode=0666 DirectoryMode=0755 Accept=yes MaxConnectionsPerSource=16 [Install] WantedBy=sockets.target account-utils-1.3.0/units/pwupdd@.service.in000066400000000000000000000014111521474342200210420ustar00rootroot00000000000000[Unit] Description=Daemon to update passwd and shadow entries Documentation=man:pwupdd(8) After=local-fs.target [Service] Environment="PWUPDD_OPTS=" EnvironmentFile=-/etc/default/account-utils ExecStart=@LIBEXECDIR@/pwupdd $PWUPDD_OPTS LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes PrivateDevices=yes PrivateTmp=yes ProcSubset=pid ProtectClock=yes ProtectControlGroups=yes ProtectHome=yes ProtectHostname=yes ProtectKernelLogs=yes ProtectKernelModules=yes ProtectKernelTunables=yes ProtectProc=invisible ProtectSystem=strict ReadWritePaths=/run/account /etc RestrictNamespaces=yes RestrictRealtime=true RestrictRealtime=yes RestrictSUIDSGID=yes # This don't work with NIS, LDAP, ... #IPAddressDeny=any #PrivateNetwork=yes #RestrictAddressFamilies=AF_UNIX