LCOV - code coverage report
Current view: top level - snapdev - as_root.h (source / functions) Hit Total Coverage
Test: coverage.info Lines: 37 64 57.8 %
Date: 2023-05-29 16:11:08 Functions: 7 10 70.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : // Copyright (c) 2016-2023  Made to Order Software Corp.  All Rights Reserved
       2             : //
       3             : // https://snapwebsites.org/project/snapdev
       4             : // contact@m2osw.com
       5             : //
       6             : // This program is free software: you can redistribute it and/or modify
       7             : // it under the terms of the GNU General Public License as published by
       8             : // the Free Software Foundation, either version 3 of the License, or
       9             : // (at your option) any later version.
      10             : //
      11             : // This program is distributed in the hope that it will be useful,
      12             : // but WITHOUT ANY WARRANTY; without even the implied warranty of
      13             : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      14             : // GNU General Public License for more details.
      15             : //
      16             : // You should have received a copy of the GNU General Public License
      17             : // along with this program.  If not, see <https://www.gnu.org/licenses/>.
      18             : #pragma once
      19             : 
      20             : /** \file
      21             :  * \brief Class & Function dealing with changing user ownership.
      22             :  *
      23             :  * The class is used to gain a different user and group ownership for a
      24             :  * short period of time. It is expected to be used on the stack for when
      25             :  * you need to execute some code as a different user. It is called as_root
      26             :  * because in most cases you want to become root.
      27             :  *
      28             :  * The function drop_root_privileges() is used to completely drop root
      29             :  * privileges. This is often done just after a fork() and just before
      30             :  * the following exec() so child processes do not gain root privileges.
      31             :  * It is also used by servers that spawn child processes and do not want
      32             :  * those children to have root access.
      33             :  */
      34             : 
      35             : // self
      36             : //
      37             : #include    <snapdev/not_reached.h>
      38             : #include    <snapdev/not_used.h>
      39             : 
      40             : 
      41             : // libexcept
      42             : //
      43             : #include    "libexcept/exception.h"
      44             : 
      45             : 
      46             : // C++
      47             : //
      48             : #include    <algorithm>
      49             : #include    <string>
      50             : 
      51             : 
      52             : // C
      53             : //
      54             : #include    <grp.h>
      55             : #include    <pwd.h>
      56             : #include    <unistd.h>
      57             : 
      58             : 
      59             : 
      60             : namespace snapdev
      61             : {
      62             : 
      63             : 
      64             : 
      65           0 : DECLARE_MAIN_EXCEPTION(as_root_exception);
      66             : 
      67           0 : DECLARE_EXCEPTION(as_root_exception, still_root);
      68           0 : DECLARE_EXCEPTION(as_root_exception, unknown_user);
      69             : 
      70             : 
      71             : /** \brief A value representing an invalid group number.
      72             :  *
      73             :  * By default, the as_root class does not change the group ID. This value
      74             :  * represents that fact.
      75             :  *
      76             :  * \todo
      77             :  * Determine whether -1 is really not a valid group number. We may need to
      78             :  * handle this differently.
      79             :  */
      80             : constexpr gid_t NO_GROUP = static_cast<gid_t>(-1);
      81             : 
      82             : 
      83             : /** \brief Safely become root (or another user) and back.
      84             :  *
      85             :  * This class defines a way to become a different user (as defined by the
      86             :  * 's' bit of the executable) and then come back as the original user once
      87             :  * your work requiring that user is done.
      88             :  *
      89             :  * The class uses RAII so when an object of that type gets destroyed,
      90             :  * it automatically restore the user. You can also restore the user
      91             :  * sooner if safer in your code.
      92             :  *
      93             :  * This class is used only when a process is given a different user as
      94             :  * the owner of the executable file and when the set user identifier flag
      95             :  * is set on the executable (a.k.a. `chmod u+s /usr/bin/my-tool`).
      96             :  *
      97             :  * The class constructor attempts the change to the specified user (default
      98             :  * is root). If that fails, the is_switched() function returns false. It is
      99             :  * likely important that you verified whether the effective user ID was
     100             :  * changed or not to make sure that you can run the commands you expect to
     101             :  * run as that user.
     102             :  *
     103             :  * The class can be used recursively. So if function A uses as_root,
     104             :  * then calls function B which also uses as_root, the second time
     105             :  * the switch does nothing since you are already that user. Function A
     106             :  * will stil properly restore the user to the normal user instead of
     107             :  * the other user.
     108             :  *
     109             :  * The class is only expected to be used on the stack. If you use it
     110             :  * in an object, then the restore may not work as expected (i.e. it
     111             :  * could happen in the wrong order).
     112             :  *
     113             :  * \note
     114             :  * Make sure to check that the switch happened. A switch could fail because
     115             :  * the user cannot have yet another process under his belt or because the
     116             :  * kernel does not have enough memory to apply the change. It may not always
     117             :  * be because of unavailable permissions.
     118             :  *
     119             :  * \todo
     120             :  * I think that the f_switched boolean flag may need to be changed to a
     121             :  * 3 value type because the switch_on() may be able to change the group
     122             :  * and not the user and same with switch_off(). So we could be in a partial
     123             :  * state and right now that is not properly represented.
     124             :  */
     125             : class as_root
     126             : {
     127             : public:
     128             :     /** \brief Shared pointer.
     129             :      *
     130             :      * If you want to create the object within an if() block, then you
     131             :      * can use this pointer like so:
     132             :      *
     133             :      * \code
     134             :      *     ...
     135             :      *     snapdev::as_root::pointer_t other_user;
     136             :      *     if(<expr>)
     137             :      *     {
     138             :      *         other_user = std::make_shared<snapdev::as_root>(uid);
     139             :      *     }
     140             :      *     ...
     141             :      *     // once end of function is reached, user is restored
     142             :      * \endcode
     143             :      */
     144             :     typedef std::shared_ptr<as_root>    pointer_t;
     145             : 
     146             :     /** \brief Save the current user then switch to another user.
     147             :      *
     148             :      * This function switches us to the \p uid user. If that fails,
     149             :      * then the is_switched() function returns false. You can get
     150             :      * the exact error using the error_number() function. You can
     151             :      * also check the user identifier with getuid() and see whether
     152             :      * it matches what you otherwise expect.
     153             :      *
     154             :      * The destructor automatically reverts back to the original user.
     155             :      *
     156             :      * \param[in] uid  The user identifier you want to become.
     157             :      * \param[in] gid  The group identifier you want to become.
     158             :      */
     159           7 :     as_root(uid_t uid = 0, gid_t gid = NO_GROUP)
     160           7 :         : f_new_uid(uid)
     161           7 :         , f_new_gid(gid)
     162           7 :         , f_user_uid(getuid())
     163           7 :         , f_group_gid(getgid())
     164             :     {
     165           7 :         switch_on();
     166           7 :     }
     167             : 
     168             :     /** \brief Save the current user then switch to another user.
     169             :      *
     170             :      * This function switches us to the named user. If that fails,
     171             :      * then the is_switched() function returns false. You can get
     172             :      * the exact error using the error_number() function.
     173             :      *
     174             :      * If you become a different user other than root, you may want to
     175             :      * define the \p groupname parameter in order to have full permissions
     176             :      * to do a snappdev::chownnm().
     177             :      *
     178             :      * The destructor automatically reverts back to the original user.
     179             :      *
     180             :      * \exception unknown_user
     181             :      * If the named user or group do not exist on this computer, then this
     182             :      * exception is raised. It is also used in case the \p username
     183             :      * parameter is the empty string.
     184             :      *
     185             :      * \param[in] username  The name of the user you want to become.
     186             :      * \param[in] groupname  The name of the group you want to become or an
     187             :      * empty string to not change the group.
     188             :      */
     189             :     as_root(
     190             :               std::string const & username
     191             :             , std::string const & groupname = std::string())
     192             :         : f_user_uid(getuid())
     193             :     {
     194             :         if(username.empty())
     195             :         {
     196             :             throw unknown_user("user name to switch to cannot be an empty string.");
     197             :         }
     198             :         passwd * user(getpwnam(username.c_str()));
     199             :         if(user == nullptr)
     200             :         {
     201             :             unknown_user e(
     202             :                   "user \""
     203             :                 + username
     204             :                 + "\" to switch to was not found on this computer.");
     205             :             e.set_parameter("username", username);
     206             :             throw e;
     207             :         }
     208             :         if(!groupname.empty())
     209             :         {
     210             :             group * grp(getgrnam(groupname.c_str()));
     211             :             if(grp == nullptr)
     212             :             {
     213             :                 unknown_user e(
     214             :                       "group \""
     215             :                     + groupname
     216             :                     + "\" to switch to was not found on this computer.");
     217             :                 e.set_parameter("groupname", groupname);
     218             :                 throw e;
     219             :             }
     220             :             f_new_gid = grp->gr_gid;
     221             :         }
     222             :         f_new_uid = user->pw_uid;
     223             : 
     224             :         switch_on();
     225             :     }
     226             : 
     227             :     /** \brief Restore the user as it was just before construction.
     228             :      *
     229             :      * This function restores the user as found in the construtor. If
     230             :      * the constructor could not switch to the other user, then this
     231             :      * function does nothing.
     232             :      */
     233           7 :     ~as_root()
     234             :     {
     235           7 :         NOT_USED(switch_off());
     236           7 :     }
     237             : 
     238             :     /** \brief Set the effective user ID back to what it was on construction.
     239             :      *
     240             :      * This function reverse the constructor action and returns the effective
     241             :      * user ID back to the user it was set to on construction. The destructor
     242             :      * calls this function automatically, but it is publicly available in
     243             :      * case you wanted to change the effective user ID back sooner.
     244             :      *
     245             :      * If the switch fails, then the error_number() function returns the
     246             :      * errno code returned by the seteuid() function.
     247             :      *
     248             :      * \return true if the switch worked as expected.
     249             :      *
     250             :      * \sa switch_on()
     251             :      * \sa error_number()
     252             :      */
     253           7 :     bool switch_off()
     254             :     {
     255           7 :         if(f_switched)
     256             :         {
     257           1 :             f_errno = 0;
     258           2 :             if(f_group_gid != NO_GROUP
     259           1 :             && setegid(f_group_gid) != 0)
     260             :             {
     261           0 :                 f_errno = errno;
     262             :             }
     263           1 :             if(seteuid(f_user_uid) != 0)
     264             :             {
     265           0 :                 f_errno = errno;
     266             :             }
     267           1 :             if(f_errno != 0)
     268             :             {
     269           0 :                 return false;
     270             :             }
     271           1 :             f_switched = false;
     272             :         }
     273             : 
     274           7 :         return true;
     275             :     }
     276             : 
     277             :     /** \brief Switch the new user back on.
     278             :      *
     279             :      * This function sets the effective user ID back to the one specified
     280             :      * on the constructor. If that is already the current user ID (as per
     281             :      * the f_switched internal flag), then nothing happens.
     282             :      *
     283             :      * If you have part of the code that you want to execute as the new
     284             :      * user, then part as the old user, and then back again with the
     285             :      * new user, you can do so using the switch_on() and switch_off()
     286             :      * functions.
     287             :      *
     288             :      * If an error occurs, the function returns false and you can get
     289             :      * the errno set by the seteuid() function by calling the error_number()
     290             :      * function.
     291             :      *
     292             :      * \note
     293             :      * You cannot change the new effective user ID to this function. This
     294             :      * is because it would be much more difficult to manage that way. If
     295             :      * you need to switch to yet another user, then consider using another
     296             :      * as_root object.
     297             :      *
     298             :      * \return true if the switch succeeded.
     299             :      *
     300             :      * \sa switch_off()
     301             :      * \sa error_number()
     302             :      */
     303           7 :     bool switch_on()
     304             :     {
     305           7 :         if(!f_switched)
     306             :         {
     307           7 :             if(seteuid(f_new_uid) != 0)
     308             :             {
     309           6 :                 f_errno = errno;
     310           6 :                 return false;
     311             :             }
     312           1 :             if(f_new_gid != NO_GROUP)
     313             :             {
     314           0 :                 if(setegid(f_new_gid) != 0)
     315             :                 {
     316             :                     // attempt to restore the previous user on error
     317             :                     //
     318           0 :                     NOT_USED(seteuid(f_user_uid));
     319             : 
     320           0 :                     f_errno = errno;
     321           0 :                     return false;
     322             :                 }
     323             :             }
     324           1 :             f_switched = true;
     325           1 :             f_errno = 0;
     326             :         }
     327           1 :         return true;
     328             :     }
     329             : 
     330             :     /** \brief Check whether the as_root object is considered switched.
     331             :      *
     332             :      * The constructor of the function attempts to switch to a different
     333             :      * user. If the function fails, then the errno value is saved and
     334             :      * the switched flag is set to true. This function returns the value
     335             :      * of the switch flag. If true, then you are switched. If false, you
     336             :      * are not switched.
     337             :      *
     338             :      * If you are not switched, you may check the error number with
     339             :      * the error_number() function to know whether it's an invalid
     340             :      * state or just that you are not the new user at the moment.
     341             :      *
     342             :      * \return true if the constructor worked and thus we are root.
     343             :      *
     344             :      * \sa error_number()
     345             :      */
     346           6 :     bool is_switched() const
     347             :     {
     348           6 :         return f_switched;
     349             :     }
     350             : 
     351             :     /** \brief Get the error number.
     352             :      *
     353             :      * The switch_on() and switch_off() functions saves the error number
     354             :      * set by the seteuid() function when it fails. That error number can
     355             :      * be retrieved using this function.
     356             :      *
     357             :      * If you call the switch_on() and switch_off() functions and they
     358             :      * succeed, then this function returns 0.
     359             :      *
     360             :      * \return The errno as set by the seteuid() on an error, otherwise 0.
     361             :      *
     362             :      * \sa is_switched()
     363             :      * \sa switch_on()
     364             :      * \sa switch_off()
     365             :      */
     366           2 :     int error_number() const
     367             :     {
     368           2 :         return f_errno;
     369             :     }
     370             : 
     371             : private:
     372             :     /** \brief The UID to become.
     373             :      *
     374             :      * This value represents the UID to become. It can be set on the
     375             :      * constructor only. It cannot be changed later. If you need yet
     376             :      * another user, you will have to create a different as_root object
     377             :      * while you are that other user.
     378             :      */
     379             :     uid_t       f_new_uid = 0;
     380             : 
     381             :     /** \brief The GID to become.
     382             :      *
     383             :      * This value represents the GID to become or NO_GROUP. It can be set on
     384             :      * the constructor only. It cannot be changed later. If you need yet
     385             :      * another group, you will have to create a different as_root object
     386             :      * while you are that other user.
     387             :      */
     388             :     gid_t       f_new_gid = NO_GROUP;
     389             : 
     390             :     /** \brief The user UID on entry.
     391             :      *
     392             :      * The constructor saves the current user identifier so we can restore
     393             :      * it on destruction.
     394             :      */
     395             :     uid_t       f_user_uid = 0;
     396             : 
     397             :     /** \brief The group GID on entry.
     398             :      *
     399             :      * The constructor saves the current group identifier so we can restore
     400             :      * it on destruction.
     401             :      */
     402             :     gid_t       f_group_gid = NO_GROUP;
     403             : 
     404             :     /** \brief The error number.
     405             :      *
     406             :      * If the seteuid() found in the constructor fails, then this value is
     407             :      * set to the errno parameter as set by the seteuid() function.
     408             :      *
     409             :      * By default, the value is 0 which means that the seteuid() function
     410             :      * succeeded in changing the user identifier to the root user identifier.
     411             :      */
     412             :     int         f_errno = 0;
     413             : 
     414             :     /** \brief Whether the switch is still in effect.
     415             :      *
     416             :      * This flag is set to true when the constructor succeed in switching
     417             :      * the user to the specified UID.
     418             :      *
     419             :      * You can later switch back and forth and thus the flag may be turned
     420             :      * back on or off.
     421             :      */
     422             :     bool        f_switched = false;
     423             : };
     424             : 
     425             : 
     426             : /** \brief Modes available to the drop_root_privileges() function.
     427             :  *
     428             :  * These modes define how to act in case the caller cannot be switched to
     429             :  * a user other than root.
     430             :  */
     431             : enum class drop_privilege_mode_t
     432             : {
     433             :     /** \brief The default mode is to fail.
     434             :      *
     435             :      * This mode is the default. In this case, a drop failure means that the
     436             :      * function throws a snapdev::still_root exception.
     437             :      */
     438             :     DROP_PRIVILEGE_MODE_FAIL,
     439             : 
     440             :     /** \brief Try swithing to another user.
     441             :      *
     442             :      * This mode allows the process to attempt to switch to the specified
     443             :      * user. By default, that user is "nobody". You can change that to a
     444             :      * different user by changing the nobody parameter of the
     445             :      * drop_root_privileges() function.
     446             :      */
     447             :     DROP_PRIVILEGE_MODE_TRY_NOBODY,
     448             : 
     449             :     /** \brief Allow the root user.
     450             :      *
     451             :      * If this mode is used and the drop of privileges fails to go to a
     452             :      * user other than root, then the function simply returns. That means
     453             :      * you will be running your code as root. If that is not acceptable,
     454             :      * then you do not want to use this option.
     455             :      */
     456             :     DROP_PRIVILEGE_MODE_ALLOW_ROOT,
     457             : };
     458             : 
     459             : 
     460             : 
     461             : /** \brief This functionmakes sure your process cannot become root.
     462             :  *
     463             :  * To make sure your application is not given root privileges, you can call
     464             :  * this function when your main() function starts. This is useful for
     465             :  * services which are not expected to run as root.
     466             :  *
     467             :  * By default, the function attempts to switch to the user that started
     468             :  * the process (`getuid(2)`). If that is still root, then it uses the
     469             :  * \p mode parameter to know how to react next.
     470             :  *
     471             :  * \li drop_privilege_mode_t::DROP_PRIVILEGE_MODE_ALLOW_ROOT
     472             :  *
     473             :  * If it is acceptable to run your software as root anyway, then use this
     474             :  * mode instead of the default.
     475             :  *
     476             :  * \li drop_privilege_mode_t::DROP_PRIVILEGE_MODE_TRY_NOBODY
     477             :  *
     478             :  * Try dropping privileges to the specified \p user_name user. By default,
     479             :  * \p user_name is set to "nobody", hence the name of the mode.
     480             :  *
     481             :  * \li drop_privilege_mode_t::DROP_PRIVILEGE_MODE_FAIL
     482             :  *
     483             :  * When \p mode is set to this value, failure to drop privileges result
     484             :  * in the `still_root` exception.
     485             :  *
     486             :  * \exception still_root
     487             :  * It is important to note that, by default, if the user is still root after
     488             :  * the attempt to drop privileges, the function raises this exception.
     489             :  * You can let your users continue to run as root by changing the mode
     490             :  * parameter.
     491             :  *
     492             :  * \param[in] mode  Defines what to do in case the privileges are not dropped.
     493             :  */
     494           1 : inline void drop_root_privileges(
     495             :       drop_privilege_mode_t const mode = drop_privilege_mode_t::DROP_PRIVILEGE_MODE_FAIL
     496             :     , std::string const & user_name = "nobody")
     497             : {
     498             :     // try dropping privileges
     499             :     //
     500           1 :     int r(setuid(getuid()));
     501           1 :     if(r == 0)
     502             :     {
     503             :         // still root?
     504             :         //
     505           1 :         if(getuid() != 0)
     506             :         {
     507           1 :             return;
     508             :         }
     509             :     }
     510             : 
     511           0 :     switch(mode)
     512             :     {
     513           0 :     case drop_privilege_mode_t::DROP_PRIVILEGE_MODE_ALLOW_ROOT:
     514             :         // user allows root anyway
     515             :         //
     516           0 :         return;
     517             : 
     518           0 :     case drop_privilege_mode_t::DROP_PRIVILEGE_MODE_TRY_NOBODY:
     519             :         {
     520           0 :             passwd * user(getpwnam(user_name.c_str()));
     521           0 :             if(user == nullptr)
     522             :             {
     523           0 :                 unknown_user e(
     524             :                       "user \""
     525           0 :                     + user_name
     526           0 :                     + "\" to drop privileges to was not found on this computer.");
     527           0 :                 e.set_parameter("username", user_name);
     528           0 :                 throw e;
     529           0 :             }
     530             : 
     531           0 :             if(setuid(user->pw_uid) == 0)
     532             :             {
     533           0 :                 if(getuid() == user->pw_uid)
     534             :                 {
     535             :                     // we are a nobody process now, we're safe
     536             :                     //
     537           0 :                     return;
     538             :                 }
     539             :             }
     540             :         }
     541             :         [[fallthrough]];
     542             :     case drop_privilege_mode_t::DROP_PRIVILEGE_MODE_FAIL:
     543           0 :         throw still_root("this process could not drop root privileges.");
     544             : 
     545             :     }
     546           0 :     snapdev::NOT_REACHED();
     547             : }
     548             : 
     549             : 
     550             : 
     551             : } // namespace snapdev
     552             : // vim: ts=4 sw=4 et

Generated by: LCOV version 1.14