/* // Copyright (c) 2018 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shadowlock.hpp" #include "file.hpp" #include "user_mgr.hpp" #include "users.hpp" #include "config.h" namespace phosphor { namespace user { static constexpr const char *passwdFileName = "/etc/passwd"; static constexpr size_t ipmiMaxUsers = 15; static constexpr size_t ipmiMaxUserNameLen = 16; static constexpr size_t systemMaxUserNameLen = 30; static constexpr size_t maxSystemUsers = 30; static constexpr const char *grpSsh = "ssh"; using namespace phosphor::logging; using InsufficientPermission = sdbusplus::xyz::openbmc_project::Common::Error::InsufficientPermission; using InternalFailure = sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure; using InvalidArgument = sdbusplus::xyz::openbmc_project::Common::Error::InvalidArgument; using UserNameExists = sdbusplus::xyz::openbmc_project::User::Common::Error::UserNameExists; using UserNameDoesNotExist = sdbusplus::xyz::openbmc_project::User::Common::Error::UserNameDoesNotExist; using UserNameGroupFail = sdbusplus::xyz::openbmc_project::User::Common::Error::UserNameGroupFail; using NoResource = sdbusplus::xyz::openbmc_project::User::Common::Error::NoResource; using Argument = xyz::openbmc_project::Common::InvalidArgument; template static void executeCmd(const char *path, ArgTypes &&... tArgs) { boost::process::child execProg(path, const_cast(tArgs)...); execProg.wait(); int retCode = execProg.exit_code(); if (retCode) { log("Command execution failed", entry("PATH=%s", path), entry("RETURN_CODE:%d", retCode)); elog(); } return; } static std::string getCSVFromVector(std::vector vec) { switch (vec.size()) { case 0: { return ""; } break; case 1: { return std::string{vec[0]}; } break; default: { return std::accumulate( std::next(vec.begin()), vec.end(), vec[0], [](std::string a, std::string b) { return a + ',' + b; }); } } } static bool removeStringFromCSV(std::string &csvStr, const std::string &delStr) { std::string::size_type delStrPos = csvStr.find(delStr); if (delStrPos != std::string::npos) { // need to also delete the comma char if (delStrPos == 0) { csvStr.erase(delStrPos, delStr.size() + 1); } else { csvStr.erase(delStrPos - 1, delStr.size() + 1); } return true; } return false; } bool UserMgr::isUserExist(const std::string &userName) { if (userName.empty()) { log("User name is empty"); elog(Argument::ARGUMENT_NAME("User name"), Argument::ARGUMENT_VALUE("Null")); } if (usersList.find(userName) == usersList.end()) { return false; } return true; } void UserMgr::throwForUserDoesNotExist(const std::string &userName) { if (isUserExist(userName) == false) { log("User does not exist", entry("USER_NAME=%s", userName.c_str())); elog(); } } void UserMgr::throwForUserExists(const std::string &userName) { if (isUserExist(userName) == true) { log("User already exists", entry("USER_NAME=%s", userName.c_str())); elog(); } } void UserMgr::throwForUserNameConstraints( const std::string &userName, const std::vector &groupNames) { if (std::find(groupNames.begin(), groupNames.end(), "ipmi") != groupNames.end()) { if (userName.length() > ipmiMaxUserNameLen) { log("IPMI user name length limitation", entry("SIZE=%d", userName.length())); elog( xyz::openbmc_project::User::Common::UserNameGroupFail::REASON( "IPMI length")); } } if (userName.length() > systemMaxUserNameLen) { log("User name length limitation", entry("SIZE=%d", userName.length())); elog(Argument::ARGUMENT_NAME("User name"), Argument::ARGUMENT_VALUE("Invalid length")); } if (!std::regex_match(userName.c_str(), std::regex("[a-zA-z_][a-zA-Z_0-9]*"))) { log("Invalid user name", entry("USER_NAME=%s", userName.c_str())); elog(Argument::ARGUMENT_NAME("User name"), Argument::ARGUMENT_VALUE("Invalid data")); } } void UserMgr::throwForMaxGrpUserCount( const std::vector &groupNames) { if (std::find(groupNames.begin(), groupNames.end(), "ipmi") != groupNames.end()) { if (getIpmiUsersCount() >= ipmiMaxUsers) { log("IPMI user limit reached"); elog( xyz::openbmc_project::User::Common::NoResource::REASON( "ipmi user count reached")); } } else { if (usersList.size() > 0 && (usersList.size() - getIpmiUsersCount()) >= (maxSystemUsers - ipmiMaxUsers)) { log("Non-ipmi User limit reached"); elog( xyz::openbmc_project::User::Common::NoResource::REASON( "Non-ipmi user count reached")); } } return; } void UserMgr::throwForInvalidPrivilege(const std::string &priv) { if (!priv.empty() && (std::find(privMgr.begin(), privMgr.end(), priv) == privMgr.end())) { log("Invalid privilege"); elog(Argument::ARGUMENT_NAME("Privilege"), Argument::ARGUMENT_VALUE(priv.c_str())); } } void UserMgr::throwForInvalidGroups(const std::vector &groupNames) { for (auto &group : groupNames) { if (std::find(groupsMgr.begin(), groupsMgr.end(), group) == groupsMgr.end()) { log("Invalid Group Name listed"); elog(Argument::ARGUMENT_NAME("GroupName"), Argument::ARGUMENT_VALUE(group.c_str())); } } } void UserMgr::createUser(std::string userName, std::vector groupNames, std::string priv, bool enabled) { throwForInvalidPrivilege(priv); throwForInvalidGroups(groupNames); // All user management lock has to be based on /etc/shadow phosphor::user::shadow::Lock lock(); throwForUserExists(userName); throwForUserNameConstraints(userName, groupNames); throwForMaxGrpUserCount(groupNames); std::string groups = getCSVFromVector(groupNames); bool sshRequested = removeStringFromCSV(groups, grpSsh); // treat privilege as a group - This is to avoid using different file to // store the same. if (groups.size() != 0) { groups += ","; } groups += priv; try { executeCmd("/usr/sbin/useradd", userName.c_str(), "-G", groups.c_str(), "-M", "-N", "-s", (sshRequested ? "/bin/sh" : "/bin/nologin"), "-e", (enabled ? "" : "1970-01-02")); } catch (const InternalFailure &e) { log("Unable to create new user"); elog(); } // Add the users object before sending out the signal std::string userObj = std::string(usersObjPath) + "/" + userName; std::sort(groupNames.begin(), groupNames.end()); usersList.emplace( userName, std::move(std::make_unique( bus, userObj.c_str(), groupNames, priv, enabled, *this))); log("User created successfully", entry("USER_NAME=%s", userName.c_str())); return; } void UserMgr::deleteUser(std::string userName) { // All user management lock has to be based on /etc/shadow phosphor::user::shadow::Lock lock(); throwForUserDoesNotExist(userName); try { executeCmd("/usr/sbin/userdel", userName.c_str()); } catch (const InternalFailure &e) { log("User delete failed", entry("USER_NAME=%s", userName.c_str())); elog(); } usersList.erase(userName); log("User deleted successfully", entry("USER_NAME=%s", userName.c_str())); return; } void UserMgr::renameUser(std::string userName, std::string newUserName) { // All user management lock has to be based on /etc/shadow phosphor::user::shadow::Lock lock(); throwForUserDoesNotExist(userName); throwForUserExists(newUserName); throwForUserNameConstraints(newUserName, usersList[userName].get()->userGroups()); try { executeCmd("/usr/sbin/usermod", "-l", newUserName.c_str(), userName.c_str()); } catch (const InternalFailure &e) { log("User rename failed", entry("USER_NAME=%s", userName.c_str())); elog(); } const auto &user = usersList[userName]; std::string priv = user.get()->userPrivilege(); std::vector groupNames = user.get()->userGroups(); bool enabled = user.get()->userEnabled(); std::string newUserObj = std::string(usersObjPath) + "/" + newUserName; // Special group 'ipmi' needs a way to identify user renamed, in order to // update encrypted password. It can't rely only on InterfacesRemoved & // InterfacesAdded. So first send out userRenamed signal. this->userRenamed(userName, newUserName); usersList.erase(userName); usersList.emplace( newUserName, std::move(std::make_unique( bus, newUserObj.c_str(), groupNames, priv, enabled, *this))); return; } void UserMgr::updateGroupsAndPriv(const std::string &userName, const std::vector &groupNames, const std::string &priv) { throwForInvalidPrivilege(priv); throwForInvalidGroups(groupNames); // All user management lock has to be based on /etc/shadow phosphor::user::shadow::Lock lock(); throwForUserDoesNotExist(userName); const std::vector &oldGroupNames = usersList[userName].get()->userGroups(); std::vector groupDiff; // Note: already dealing with sorted group lists. std::set_symmetric_difference(oldGroupNames.begin(), oldGroupNames.end(), groupNames.begin(), groupNames.end(), std::back_inserter(groupDiff)); if (std::find(groupDiff.begin(), groupDiff.end(), "ipmi") != groupDiff.end()) { throwForUserNameConstraints(userName, groupNames); throwForMaxGrpUserCount(groupNames); } std::string groups = getCSVFromVector(groupNames); bool sshRequested = removeStringFromCSV(groups, grpSsh); // treat privilege as a group - This is to avoid using different file to // store the same. if (groups.size() != 0) { groups += ","; } groups += priv; try { executeCmd("/usr/sbin/usermod", userName.c_str(), "-G", groups.c_str(), "-s", (sshRequested ? "/bin/sh" : "/bin/nologin")); } catch (const InternalFailure &e) { log("Unable to modify user privilege / groups"); elog(); } log("User groups / privilege updated successfully", entry("USER_NAME=%s", userName.c_str())); return; } void UserMgr::userEnable(const std::string &userName, bool enabled) { // All user management lock has to be based on /etc/shadow phosphor::user::shadow::Lock lock(); throwForUserDoesNotExist(userName); try { executeCmd("/usr/sbin/usermod", userName.c_str(), "-e", (enabled ? "" : "1970-01-02")); } catch (const InternalFailure &e) { log("Unable to modify user enabled state"); elog(); } log("User enabled/disabled state updated successfully", entry("USER_NAME=%s", userName.c_str()), entry("ENABLED=%d", enabled)); return; } UserSSHLists UserMgr::getUserAndSshGrpList() { // All user management lock has to be based on /etc/shadow phosphor::user::shadow::Lock lock(); std::vector userList; std::vector sshUsersList; struct passwd pw, *pwp = nullptr; std::array buffer{}; phosphor::user::File passwd(passwdFileName, "r"); if ((passwd)() == NULL) { log("Error opening the passwd file"); elog(); } while (true) { auto r = fgetpwent_r((passwd)(), &pw, buffer.data(), buffer.max_size(), &pwp); if ((r != 0) || (pwp == NULL)) { // Any error, break the loop. break; } // All users whose UID >= 1000 and < 65534 if ((pwp->pw_uid >= 1000) && (pwp->pw_uid < 65534)) { std::string userName(pwp->pw_name); userList.emplace_back(userName); // ssh doesn't have separate group. Check login shell entry to // get all users list which are member of ssh group. std::string loginShell(pwp->pw_shell); if (loginShell == "/bin/sh") { sshUsersList.emplace_back(userName); } } } endpwent(); return std::make_pair(std::move(userList), std::move(sshUsersList)); } size_t UserMgr::getIpmiUsersCount() { std::vector userList = getUsersInGroup("ipmi"); return userList.size(); } bool UserMgr::isUserEnabled(const std::string &userName) { // All user management lock has to be based on /etc/shadow phosphor::user::shadow::Lock lock(); std::array buffer{}; struct spwd spwd; struct spwd *resultPtr = nullptr; int status = getspnam_r(userName.c_str(), &spwd, buffer.data(), buffer.max_size(), &resultPtr); if (!status && (&spwd == resultPtr)) { if (resultPtr->sp_expire >= 0) { return false; // user locked out } return true; } return false; // assume user is disabled for any error. } std::vector UserMgr::getUsersInGroup(const std::string &groupName) { std::vector usersInGroup; // Should be more than enough to get the pwd structure. std::array buffer{}; struct group grp; struct group *resultPtr = nullptr; int status = getgrnam_r(groupName.c_str(), &grp, buffer.data(), buffer.max_size(), &resultPtr); if (!status && (&grp == resultPtr)) { for (; *(grp.gr_mem) != NULL; ++(grp.gr_mem)) { usersInGroup.emplace_back(*(grp.gr_mem)); } } else { log("Group not found", entry("GROUP=%s", groupName.c_str())); // Don't throw error, just return empty userList - fallback } return usersInGroup; } void UserMgr::initUserObjects(void) { // All user management lock has to be based on /etc/shadow phosphor::user::shadow::Lock lock(); std::vector userNameList; std::vector sshGrpUsersList; UserSSHLists userSSHLists = getUserAndSshGrpList(); userNameList = std::move(userSSHLists.first); sshGrpUsersList = std::move(userSSHLists.second); if (!userNameList.empty()) { std::map> groupLists; for (auto &grp : groupsMgr) { if (grp == grpSsh) { groupLists.emplace(grp, sshGrpUsersList); } else { std::vector grpUsersList = getUsersInGroup(grp); groupLists.emplace(grp, grpUsersList); } } for (auto &grp : privMgr) { std::vector grpUsersList = getUsersInGroup(grp); groupLists.emplace(grp, grpUsersList); } for (auto &user : userNameList) { std::vector userGroups; std::string userPriv; for (const auto &grp : groupLists) { std::vector tempGrp = grp.second; if (std::find(tempGrp.begin(), tempGrp.end(), user) != tempGrp.end()) { if (std::find(privMgr.begin(), privMgr.end(), grp.first) != privMgr.end()) { userPriv = grp.first; } else { userGroups.emplace_back(grp.first); } } } // Add user objects to the Users path. auto objPath = std::string(usersObjPath) + "/" + user; std::sort(userGroups.begin(), userGroups.end()); usersList.emplace(user, std::move(std::make_unique( bus, objPath.c_str(), userGroups, userPriv, isUserEnabled(user), *this))); } } } UserMgr::UserMgr(sdbusplus::bus::bus &bus, const char *path) : UserMgrIface(bus, path), bus(bus), path(path) { UserMgrIface::allPrivileges(privMgr); std::sort(groupsMgr.begin(), groupsMgr.end()); UserMgrIface::allGroups(groupsMgr); initUserObjects(); } } // namespace user } // namespace phosphor