From 9f630d9eb0ce1c103532a4304ea080066199094e Mon Sep 17 00:00:00 2001 From: Richard Marian Thomaiyar Date: Thu, 24 May 2018 10:49:10 +0530 Subject: Basic support for User manager service Basic support for User Manager service methods are implemented. Change-Id: Id42432ec6dd421b99971268add931dcd70876f7c Signed-off-by: Richard Marian Thomaiyar --- user_mgr.cpp | 598 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 user_mgr.cpp (limited to 'user_mgr.cpp') diff --git a/user_mgr.cpp b/user_mgr.cpp new file mode 100644 index 0000000..3987088 --- /dev/null +++ b/user_mgr.cpp @@ -0,0 +1,598 @@ +/* +// 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 -- cgit v1.2.1