From d03ec9b688a5d93f8f44e775eb74358d30d9d314 Mon Sep 17 00:00:00 2001 From: Radivoje Jovanovic Date: Mon, 2 Jul 2018 19:23:25 -0700 Subject: [PATCH] Added suport for multiple user manager services Support added for SSSD service implementation Signed-off-by: Alberto Salazar Perez Signed-off-by: Radivoje Jovanovic Signed-off-by: Richard Marian Thomaiyar Signed-off-by: Arun P. Mohanan --- Makefile.am | 5 +- mainapp.cpp | 90 +++++- user_mgr.cpp | 305 ++---------------- user_mgr.hpp | 9 +- user_service.cpp | 789 +++++++++++++++++++++++++++++++++++++++++++++++ user_service.hpp | 233 ++++++++++++++ 6 files changed, 1150 insertions(+), 281 deletions(-) create mode 100644 user_service.cpp create mode 100644 user_service.hpp diff --git a/Makefile.am b/Makefile.am index 1dbd594..fe47aaf 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,11 +1,12 @@ bin_PROGRAMS = phosphor-user-manager -noinst_HEADERS = user_mgr.hpp users.hpp +noinst_HEADERS = user_mgr.hpp users.hpp user_service.hpp phosphor_user_manager_SOURCES = \ mainapp.cpp \ user_mgr.cpp \ - users.cpp + users.cpp \ + user_service.cpp phosphor_user_manager_LDFLAGS = $(SDBUSPLUS_LIBS) \ $(PHOSPHOR_DBUS_INTERFACES_LIBS) \ diff --git a/mainapp.cpp b/mainapp.cpp index e08da61..f4b7f8c 100644 --- a/mainapp.cpp +++ b/mainapp.cpp @@ -16,18 +16,106 @@ #include "config.h" #include "user_mgr.hpp" +#include "user_service.hpp" +#include + +#include #include // D-Bus root for user manager constexpr auto USER_MANAGER_ROOT = "/xyz/openbmc_project/user"; +void printUsage() +{ + std::string usage = + R"(Usage: + phosphor-user-manager [OPTIONS] + +Backend DBUS service for OpenBMC User Management. +If no OPTIONS are specified, shadow file will be used. + +Options: + -s, --service={shadow|sssd} + Specify the authentication service to use: + 'shadow' will use the /etc/shadow file. + 'sssd' will use the sssd service domains. + -h, --help Displays this help message. +)"; + std::cerr << usage; +} + +void parseArgs(int argc, char** argv, + phosphor::user::UserService::ServiceType& srvc) +{ + const std::string shortOpts{"s:h"}; + const struct option longOpts[] = {{"service", 1, nullptr, 's'}, + {"help", 0, nullptr, 'h'}, + {nullptr, 0, nullptr, 0}}; + + while (true) + { + const auto opt = + getopt_long(argc, argv, shortOpts.c_str(), longOpts, nullptr); + + if (opt == -1) + { + if (srvc == phosphor::user::UserService::ServiceType::none) + { + srvc = phosphor::user::UserService::ServiceType::shadow; + } + break; + } + + switch (opt) + { + case 's': + { + std::string srvcStr{optarg}; + if (!srvcStr.compare("shadow")) + { + srvc = phosphor::user::UserService::ServiceType::shadow; + } + else if (!srvcStr.compare("sssd")) + { + srvc = phosphor::user::UserService::ServiceType::sssd; + } + else + { + std::cerr << "Error. '" << srvcStr << "' is not a valid" + << " authentication service." << std::endl; + printUsage(); + exit(1); + } + } + break; + + case 'h': + { + printUsage(); + exit(0); + } + + default: + { + printUsage(); + exit(1); + } + } + } +} + int main(int argc, char** argv) { + // Check command line options. Exit if error. + phosphor::user::UserService::ServiceType srvc = + phosphor::user::UserService::ServiceType::none; + parseArgs(argc, argv, srvc); + auto bus = sdbusplus::bus::new_default(); sdbusplus::server::manager::manager objManager(bus, USER_MANAGER_ROOT); - phosphor::user::UserMgr userMgr(bus, USER_MANAGER_ROOT); + phosphor::user::UserMgr userMgr(bus, USER_MANAGER_ROOT, srvc); // Claim the bus now bus.request_name(USER_MANAGER_BUSNAME); diff --git a/user_mgr.cpp b/user_mgr.cpp index 8fc899f..acc16b0 100644 --- a/user_mgr.cpp +++ b/user_mgr.cpp @@ -18,43 +18,34 @@ #include "user_mgr.hpp" -#include "file.hpp" #include "shadowlock.hpp" #include "users.hpp" #include #include -#include -#include -#include #include -#include #include -#include -#include #include #include #include #include #include -#include +#include #include -#include #include +#include 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"; static constexpr uint8_t minPasswdLength = 8; static constexpr int success = 0; static constexpr int failure = -1; @@ -100,79 +91,6 @@ using NoResource = using Argument = xyz::openbmc_project::Common::InvalidArgument; -template -static std::vector executeCmd(const char* path, - ArgTypes&&... tArgs) -{ - std::vector stdOutput; - boost::process::ipstream stdOutStream; - boost::process::child execProg(path, const_cast(tArgs)..., - boost::process::std_out > stdOutStream); - std::string stdOutLine; - - while (stdOutStream && std::getline(stdOutStream, stdOutLine) && - !stdOutLine.empty()) - { - stdOutput.emplace_back(stdOutLine); - } - - execProg.wait(); - - int retCode = execProg.exit_code(); - if (retCode) - { - log("Command execution failed", entry("PATH=%s", path), - entry("RETURN_CODE=%d", retCode)); - elog(); - } - - return stdOutput; -} - -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()) @@ -299,44 +217,15 @@ void UserMgr::createUser(std::string userName, { throwForInvalidPrivilege(priv); throwForInvalidGroups(groupNames); - // All user management lock has to be based on /etc/shadow - // TODO phosphor-user-manager#10 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 (!priv.empty()) - { - if (groups.size() != 0) - { - groups += ","; - } - groups += priv; - } - try - { - // set EXPIRE_DATE to 0 to disable user, PAM takes 0 as expire on - // 1970-01-01, that's an implementation-defined behavior - executeCmd("/usr/sbin/useradd", userName.c_str(), "-G", groups.c_str(), - "-m", "-N", "-s", - (sshRequested ? "/bin/sh" : "/bin/nologin"), "-e", - (enabled ? "" : "1970-01-01")); - } - catch (const InternalFailure& e) - { - log("Unable to create new user"); - elog(); - } + // Tell the User Service to create a new user with the info provided. + userSrvc->createUser(userName, groupNames, priv, enabled); - // Add the users object before sending out the signal - sdbusplus::message::object_path tempObjPath(usersObjPath); - tempObjPath /= userName; - std::string userObj(tempObjPath); + // Add the users to the local list 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( @@ -349,19 +238,11 @@ void UserMgr::createUser(std::string userName, void UserMgr::deleteUser(std::string userName) { - // All user management lock has to be based on /etc/shadow - // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; throwForUserDoesNotExist(userName); - try - { - executeCmd("/usr/sbin/userdel", userName.c_str(), "-r"); - } - catch (const InternalFailure& e) - { - log("User delete failed", - entry("USER_NAME=%s", userName.c_str())); - elog(); - } + + // Tell the User Service to delete user + userSrvc->deleteUser(userName); + // Then delete user from local list usersList.erase(userName); @@ -372,24 +253,13 @@ void UserMgr::deleteUser(std::string userName) void UserMgr::renameUser(std::string userName, std::string newUserName) { - // All user management lock has to be based on /etc/shadow - // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; throwForUserDoesNotExist(userName); throwForUserExists(newUserName); throwForUserNameConstraints(newUserName, usersList[userName].get()->userGroups()); - try - { - std::string newHomeDir = "/home/" + newUserName; - executeCmd("/usr/sbin/usermod", "-l", newUserName.c_str(), - userName.c_str(), "-d", newHomeDir.c_str(), "-m"); - } - catch (const InternalFailure& e) - { - log("User rename failed", - entry("USER_NAME=%s", userName.c_str())); - elog(); - } + // Call The User Service to rename user on the system + userSrvc->renameUser(userName, newUserName); + // Update local list to reflect the name change const auto& user = usersList[userName]; std::string priv = user.get()->userPrivilege(); std::vector groupNames = user.get()->userGroups(); @@ -415,8 +285,6 @@ void UserMgr::updateGroupsAndPriv(const std::string& userName, { throwForInvalidPrivilege(priv); throwForInvalidGroups(groupNames); - // All user management lock has to be based on /etc/shadow - // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; throwForUserDoesNotExist(userName); const std::vector& oldGroupNames = usersList[userName].get()->userGroups(); @@ -432,29 +300,8 @@ void UserMgr::updateGroupsAndPriv(const std::string& userName, 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 (!priv.empty()) - { - 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(); - } + // Call The User Service to update user groups and priv on the system + userSrvc->updateGroupsAndPriv(userName, groupNames, priv); log("User groups / privilege updated successfully", entry("USER_NAME=%s", userName.c_str())); @@ -650,21 +497,9 @@ int UserMgr::setPamModuleArgValue(const std::string& moduleName, void UserMgr::userEnable(const std::string& userName, bool enabled) { - // All user management lock has to be based on /etc/shadow - // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; throwForUserDoesNotExist(userName); - try - { - // set EXPIRE_DATE to 0 to disable user, PAM takes 0 as expire on - // 1970-01-01, that's an implementation-defined behavior - executeCmd("/usr/sbin/usermod", userName.c_str(), "-e", - (enabled ? "" : "1970-01-01")); - } - catch (const InternalFailure& e) - { - log("Unable to modify user enabled state"); - elog(); - } + // Call The User Service to update user groups and priv on the system + userSrvc->updateUserStatus(userName, enabled); log("User enabled/disabled state updated successfully", entry("USER_NAME=%s", userName.c_str()), @@ -787,54 +622,8 @@ bool UserMgr::userPasswordExpired(const std::string& userName) UserSSHLists UserMgr::getUserAndSshGrpList() { - // All user management lock has to be based on /etc/shadow - // TODO phosphor-user-manager#10 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; - } -#ifdef ENABLE_ROOT_USER_MGMT - // Add all users whose UID >= 1000 and < 65534 - // and special UID 0. - if ((pwp->pw_uid == 0) || - ((pwp->pw_uid >= 1000) && (pwp->pw_uid < 65534))) -#else - // Add all users whose UID >=1000 and < 65534 - if ((pwp->pw_uid >= 1000) && (pwp->pw_uid < 65534)) -#endif - { - 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)); + // Call The User Service to get the User and SSUsers lists + return std::move(userSrvc->getUserAndSshGrpList()); } size_t UserMgr::getIpmiUsersCount() @@ -845,49 +634,14 @@ size_t UserMgr::getIpmiUsersCount() bool UserMgr::isUserEnabled(const std::string& userName) { - // All user management lock has to be based on /etc/shadow - // TODO phosphor-user-manager#10 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. + // Call The User Service to verify if user is enabled + return userSrvc->isUserEnabled(userName); } 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; + // Call The User Service to get the users that belong to a group + return std::move(userSrvc->getUsersInGroup(groupName)); } DbusUserObj UserMgr::getPrivilegeMapperObject(void) @@ -1114,11 +868,9 @@ void UserMgr::initUserObjects(void) { // All user management lock has to be based on /etc/shadow // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; - std::vector userNameList; - std::vector sshGrpUsersList; UserSSHLists userSSHLists = getUserAndSshGrpList(); - userNameList = std::move(userSSHLists.first); - sshGrpUsersList = std::move(userSSHLists.second); + std::vector userNameList = std::move(userSSHLists.first); + std::vector sshGrpUsersList = std::move(userSSHLists.second); if (!userNameList.empty()) { @@ -1175,8 +927,10 @@ void UserMgr::initUserObjects(void) } } -UserMgr::UserMgr(sdbusplus::bus::bus& bus, const char* path) : - Ifaces(bus, path, true), bus(bus), path(path) +UserMgr::UserMgr(sdbusplus::bus::bus& bus, const char* path, + UserService::ServiceType srvc) : + Ifaces(bus, path, true), + bus(bus), path(path) { UserMgrIface::allPrivileges(privMgr); std::sort(groupsMgr.begin(), groupsMgr.end()); @@ -1284,6 +1038,7 @@ UserMgr::UserMgr(sdbusplus::bus::bus& bus, const char* path) : } AccountPolicyIface::accountUnlockTimeout(value32); } + userSrvc = std::make_unique(srvc, groupsMgr, privMgr); initUserObjects(); // emit the signal diff --git a/user_mgr.hpp b/user_mgr.hpp index f5aac22..5d5ca99 100644 --- a/user_mgr.hpp +++ b/user_mgr.hpp @@ -14,6 +14,7 @@ // limitations under the License. */ #pragma once +#include "user_service.hpp" #include "users.hpp" #include @@ -30,8 +31,6 @@ namespace user { using UserMgrIface = sdbusplus::xyz::openbmc_project::User::server::Manager; -using UserSSHLists = - std::pair, std::vector>; using AccountPolicyIface = sdbusplus::xyz::openbmc_project::User::server::AccountPolicy; @@ -77,8 +76,10 @@ class UserMgr : public Ifaces * * @param[in] bus - sdbusplus handler * @param[in] path - D-Bus path + * @param[in] srvc - User service to be used */ - UserMgr(sdbusplus::bus::bus& bus, const char* path); + UserMgr(sdbusplus::bus::bus& bus, const char* path, + UserService::ServiceType srvc); /** @brief create user method. * This method creates a new user as requested @@ -194,6 +195,8 @@ class UserMgr : public Ifaces /** @brief object path */ const std::string path; + /** @brief user service to be used */ + std::unique_ptr userSrvc; /** @brief privilege manager container */ std::vector privMgr = {"priv-admin", "priv-operator", "priv-user", "priv-noaccess"}; diff --git a/user_service.cpp b/user_service.cpp new file mode 100644 index 0000000..6e11755 --- /dev/null +++ b/user_service.cpp @@ -0,0 +1,789 @@ +/* +// 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 "user_service.hpp" + +#include "file.hpp" +#include "shadowlock.hpp" + +#include +#include + +#include +#include +#include + +#include + +/* anonymous namespace for User Service interface implementations. +// Each class inside this namespace implements a special service +// to be used for the User Manager class. This can be extended to use +// other user management services and it should be as simple as +// adding a new class which inherits from phosphor::user::UserServiceInterface +*/ + +namespace +{ + +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; }); + } + } +} + +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; +} + +class ShadowService : public phosphor::user::UserServiceInterface +{ + public: + ShadowService() = default; + + ~ShadowService() = default; + + phosphor::user::UserSSHLists getUserAndSshGrpList() const override + { + // All user management lock has to be based on /etc/shadow + // TODO phosphor-user-manager#10 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) + { + phosphor::logging::log( + "Error opening the passwd file"); + phosphor::logging::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; + } +#ifdef ENABLE_ROOT_USER_MGMT + // Add all users whose UID >= 1000 and < 65534 + // and special UID 0. + if ((pwp->pw_uid == 0) || + ((pwp->pw_uid >= 1000) && (pwp->pw_uid < 65534))) +#else + // Add all users whose UID >=1000 and < 65534 + if ((pwp->pw_uid >= 1000) && (pwp->pw_uid < 65534)) +#endif + { + 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)); + } + + std::vector + getUsersInGroup(const std::string& groupName) const override + { + std::vector usersInGroup; + // Should be more than enough to get the pwd structure. + std::array buffer{}; + struct group grp; + struct group* grpPtr = &grp; + struct group* resultPtr; + + int status = getgrnam_r(groupName.c_str(), grpPtr, buffer.data(), + buffer.max_size(), &resultPtr); + + if (!status && (grpPtr == resultPtr)) + { + for (; *(grp.gr_mem) != NULL; ++(grp.gr_mem)) + { + usersInGroup.emplace_back(*(grp.gr_mem)); + } + } + else + { + phosphor::logging::log( + "Group not found", + phosphor::logging::entry("GROUP=%s", groupName.c_str())); + // Don't throw error, just return empty usersInGroup - fallback + } + return usersInGroup; + } + + void createUser(const std::string& userName, + const std::vector& groupNames, + const std::string& priv, const bool& enabled) const override + { + // All user management lock has to be based on /etc/shadow + // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; + + std::string groups = getCSVFromVector(groupNames); + bool sshRequested = removeStringFromCSV(groups, phosphor::user::grpSsh); + + // treat privilege as a group - This is to avoid using different file to + // store the same + if (!priv.empty()) + { + if (groups.size() != 0) + { + groups.append(","); + } + groups.append(priv); + } + + try + { + phosphor::user::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 phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to create new user"); + phosphor::logging::elog(); + } + } + + void renameUser(const std::string& userName, + const std::string& newUserName) const override + { + // All user management lock has to be based on /etc/shadow + // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; + try + { + std::string newHomeDir = "/home/" + newUserName; + phosphor::user::executeCmd("/usr/sbin/usermod", "-l", + newUserName.c_str(), userName.c_str(), + "-d", newHomeDir.c_str(), "-m"); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "User rename failed", + phosphor::logging::entry("USER_NAME=%s", userName.c_str())); + phosphor::logging::elog(); + } + } + + void deleteUser(const std::string& userName) const override + { + // All user management lock has to be based on /etc/shadow + // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; + + try + { + phosphor::user::executeCmd("/usr/sbin/userdel", userName.c_str(), + "-r"); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "User delete failed", + phosphor::logging::entry("USER_NAME=%s", userName.c_str())); + phosphor::logging::elog(); + } + } + + void updateGroupsAndPriv(const std::string& userName, + const std::vector& groupNames, + const std::string& priv) const override + { + // All user management lock has to be based on /etc/shadow + // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; + + std::string groups = getCSVFromVector(groupNames); + bool sshRequested = removeStringFromCSV(groups, phosphor::user::grpSsh); + + // treat privilege as a group - This is to avoid using different file to + // store the same. + if (!priv.empty()) + { + if (groups.size() != 0) + { + groups += ","; + } + groups += priv; + } + + try + { + phosphor::user::executeCmd( + "/usr/sbin/usermod", userName.c_str(), "-G", groups.c_str(), + "-s", (sshRequested ? "/bin/sh" : "/bin/nologin")); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to modify user privilege / groups"); + phosphor::logging::elog(); + } + } + + void updateUserStatus(const std::string& userName, + const bool& enabled) const override + { + // All user management lock has to be based on /etc/shadow + // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; + try + { + phosphor::user::executeCmd("/usr/sbin/usermod", userName.c_str(), + "-e", (enabled ? "" : "1970-01-02")); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to modify user enabled state"); + phosphor::logging::elog(); + } + } + + bool isUserEnabled(const std::string& userName) const override + { + // All user management lock has to be based on /etc/shadow + // TODO phosphor-user-manager#10 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 + getUserGroups(const std::string& userName) const override + { + phosphor::logging::log( + "ShadowService::getUserGroups not implemented!"); + phosphor::logging::elog(); + return std::vector(); + } + + void createGroup(const std::string& groupName) const override + { + phosphor::logging::log( + "ShadowService::createGroup not implemented!"); + phosphor::logging::elog(); + } + + private: + static constexpr const char* passwdFileName = "/etc/passwd"; +}; + +class SSSDService : public phosphor::user::UserServiceInterface +{ + public: + SSSDService(const std::vector& groups, + const std::vector& privs) + { + + createGroup(lockedGrp); + for (const auto& g : groups) + { + createGroup(g); + } + for (const auto& p : privs) + { + createGroup(p); + } + } + + ~SSSDService() = default; + + phosphor::user::UserSSHLists getUserAndSshGrpList() const override + { + std::vector users; + std::vector sshGroup; + std::vector exeOutput; + + try + { + exeOutput = phosphor::user::executeCmd("/usr/bin/getent", "-s", + "sss", "passwd"); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to get users information " + "from sssd service"); + phosphor::logging::elog(); + } + + for (const auto& userLine : exeOutput) + { + std::vector userInfo; + boost::algorithm::split(userInfo, userLine, + boost::algorithm::is_any_of(":")); + // At this point userInfo is a vector containing the passwd + // info for the user, so we know the correct positions: + // 0: User name. + // 1: Encrypted password. + // 2: User ID number (UID) + // 3: User's group ID number (GID) + // 4: Full name of the user (GECOS) + // 5: User home directory. + // 6: Login shell. + users.emplace_back(userInfo[0]); + + // ssh doesn't have separate group. Check login shell entry to + // get all users list which are member of ssh group. + if (userInfo[6] == "/bin/sh") + { + sshGroup.emplace_back(userInfo[0]); + } + } + + return std::make_pair(std::move(users), std::move(sshGroup)); + } + + std::vector + getUsersInGroup(const std::string& groupName) const override + { + std::vector userList; + std::vector exeOutput; + + try + { + exeOutput = phosphor::user::executeCmd("/usr/sbin/sss_groupshow", + groupName.c_str()); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to get group users from sssd service"); + // Don't throw error, just return empty usersInGroup - return + return userList; + } + // exeOutput should have 5 entries + // 0: Group + // 1: GID number + // 2: Member users + // 3: Is a member of + // 4: Member groups + exeOutput[2].erase( + exeOutput[2].begin(), + std::find(exeOutput[2].begin(), exeOutput[2].end(), ':')); + boost::algorithm::trim_left(exeOutput[2]); + boost::algorithm::split(userList, exeOutput[2], + boost::algorithm::is_any_of(",")); + return userList; + } + + void createUser(const std::string& userName, + const std::vector& groupNames, + const std::string& priv, const bool& enabled) const override + { + std::string groups = getCSVFromVector(groupNames); + bool sshRequested = removeStringFromCSV(groups, phosphor::user::grpSsh); + // treat privilege as a group - This is to avoid using different file to + // store the same + if (!priv.empty()) + { + if (groups.size() != 0) + { + groups += ","; + } + groups += priv; + } + + try + { + phosphor::user::executeCmd( + "/usr/sbin/sss_useradd", "-m", "-G", groups.c_str(), "-s", + (sshRequested ? "/bin/sh" : "/bin/nologin"), userName.c_str()); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to create new user in sssd service"); + phosphor::logging::elog(); + } + + // Sometimes the SSSD service needs some time to actually + // reflect the changes to the local DB to the NSS service, + // that is why we have this sleep here ... + std::this_thread::sleep_for(std::chrono::seconds(1)); + // update user status (locked/unlocked) + updateUserStatus(userName, enabled); + } + + void renameUser(const std::string& userName, + const std::string& newUserName) const override + { + std::vector exeOutput; + // Local Domain for sssd doesn't have a rename feature + // so we need to first create a new user and then delete + // the old one. + // The only issue with this is that the password for the + // user will have to be reseted since it is a new user being created. + + // Get original user groups + std::vector groups = getUserGroups(userName); + // Check if it has a "ssh" group by looking for the shell login + try + { + exeOutput = phosphor::user::executeCmd( + "/usr/bin/getent", "-s", "sss", "passwd", userName.c_str()); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to get information for user"); + phosphor::logging::elog(); + } + if (exeOutput[0].find("/bin/sh")) + { + groups.emplace_back(phosphor::user::grpSsh); + } + // Call create user with the new user names and previous groups + // Priv is already part of the groups so that can be empty. + createUser(newUserName, groups, "", isUserEnabled(userName)); + + // Now delete original user + deleteUser(userName); + } + + void deleteUser(const std::string& userName) const override + { + try + { + phosphor::user::executeCmd("/usr/sbin/sss_userdel", "-r", + userName.c_str()); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to delete user from sssd service"); + phosphor::logging::elog(); + } + } + + void updateGroupsAndPriv(const std::string& userName, + const std::vector& groupNames, + const std::string& priv) const override + { + // local domain sssd do not allow to update all list of groups, + // so we will remove all groups first (except for the user one) + // and then all all the ones that were passed + std::string oldGroups = getCSVFromVector(getUserGroups(userName)); + std::string groups = getCSVFromVector(groupNames); + bool sshRequested = removeStringFromCSV(groups, phosphor::user::grpSsh); + // treat privilege as a group - This is to avoid using different file to + // store the same + if (!priv.empty()) + { + if (groups.size() != 0) + { + groups += ","; + } + groups += priv; + } + try + { + phosphor::user::executeCmd( + "/usr/sbin/sss_usermod", "-r", oldGroups.c_str(), "-a", + groups.c_str(), "-s", + (sshRequested ? "/bin/sh" : "/bin/nologin"), userName.c_str()); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to update user groups and " + "priv from sssd service"); + phosphor::logging::elog(); + } + } + + void updateUserStatus(const std::string& userName, + const bool& enabled) const override + { + std::string enabledStr; + std::string lockedStr; + if (isUserEnabled(userName) == enabled) + { + return; + } + if (enabled) + { + enabledStr = "-r"; + lockedStr = "-U"; + } + else + { + enabledStr = "-a"; + lockedStr = "-L"; + } + try + { + // We will add a special locked group to identify the users + // that have been locked out of the system. + // TODO: sss_usermod is not locking user accounts for the + // LOCAL domain, need to find the correct PAM configuration + // to actually lockout users for SSSD. + // As a workaround we are using the pam module pam_listfile.so + // to lockout all users that belong to the locked group. + phosphor::user::executeCmd("/usr/sbin/sss_usermod", + enabledStr.c_str(), lockedGrp.c_str(), + lockedStr.c_str(), userName.c_str()); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to update user status from sssd service"); + phosphor::logging::elog(); + } + } + + bool isUserEnabled(const std::string& userName) const override + { + std::vector userGrps = getUserGroups(userName); + return std::find(userGrps.begin(), userGrps.end(), lockedGrp) == + userGrps.end(); + } + + std::vector + getUserGroups(const std::string& userName) const override + { + std::vector exeOutput; + try + { + exeOutput = + phosphor::user::executeCmd("/usr/bin/groups", userName.c_str()); + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to get groups for user"); + phosphor::logging::elog(); + } + + std::vector groups; + boost::algorithm::split(groups, exeOutput[0], + boost::algorithm::is_any_of(" ")); + // Delete group that equals user name if it exists + auto userNameGroup = std::find(groups.begin(), groups.end(), userName); + if (userNameGroup != groups.end()) + { + groups.erase(userNameGroup); + } + return groups; + } + + void createGroup(const std::string& groupName) const override + { + try + { + if (!groupExists(groupName)) + { + phosphor::user::executeCmd("/usr/sbin/sss_groupadd", + groupName.c_str()); + } + } + catch (const phosphor::user::InternalFailure& e) + { + phosphor::logging::log( + "Unable to create group"); + phosphor::logging::elog(); + } + } + + private: + static const std::string lockedGrp; + + bool groupExists(const std::string& groupName) const + { + try + { + phosphor::user::executeCmd("/usr/sbin/sss_groupshow", + groupName.c_str()); + } + catch (const phosphor::user::InternalFailure& e) + { + return false; + } + return true; + } +}; + +const std::string SSSDService::lockedGrp = "sssd_locked"; +} // anonymous namespace + +namespace phosphor +{ +namespace user +{ + +UserService::UserService(const ServiceType& srvcType, + const std::vector& groups, + const std::vector& privs) +{ + setServiceImpl(srvcType, groups, privs); +} + +void UserService::updateServiceType(const ServiceType& srvcType, + const std::vector& groups, + const std::vector& privs) +{ + usrSrvcImpl.reset(); + setServiceImpl(srvcType, groups, privs); +} + +void UserService::setServiceImpl(const ServiceType& srvcType, + const std::vector& groups, + const std::vector& privs) +{ + switch (srvcType) + { + case ServiceType::shadow: + { + usrSrvcImpl = std::make_unique(); + } + break; + + case ServiceType::sssd: + { + usrSrvcImpl = std::make_unique(groups, privs); + } + break; + + case ServiceType::none: + default: + { + phosphor::logging::log( + "Invalid service type initialization!"); + phosphor::logging::elog(); + } + break; + } +} + +UserService::~UserService() +{} + +phosphor::user::UserSSHLists UserService::getUserAndSshGrpList() const +{ + return usrSrvcImpl->getUserAndSshGrpList(); +} + +std::vector + UserService::getUsersInGroup(const std::string& groupName) const +{ + return usrSrvcImpl->getUsersInGroup(groupName); +} + +void UserService::createUser(const std::string& userName, + const std::vector& groupNames, + const std::string& priv, const bool& enabled) const +{ + usrSrvcImpl->createUser(userName, groupNames, priv, enabled); +} + +void UserService::renameUser(const std::string& userName, + const std::string& newUserName) const +{ + usrSrvcImpl->renameUser(userName, newUserName); +} + +void UserService::deleteUser(const std::string& userName) const +{ + usrSrvcImpl->deleteUser(userName); +} + +void UserService::updateGroupsAndPriv( + const std::string& userName, const std::vector& groupNames, + const std::string& priv) const +{ + usrSrvcImpl->updateGroupsAndPriv(userName, groupNames, priv); +} + +void UserService::updateUserStatus(const std::string& userName, + const bool& enabled) const +{ + usrSrvcImpl->updateUserStatus(userName, enabled); +} + +bool UserService::isUserEnabled(const std::string& userName) const +{ + return usrSrvcImpl->isUserEnabled(userName); +} + +std::vector + UserService::getUserGroups(const std::string& userName) const +{ + return usrSrvcImpl->getUserGroups(userName); +} + +} // namespace user +} // namespace phosphor diff --git a/user_service.hpp b/user_service.hpp new file mode 100644 index 0000000..50ee4db --- /dev/null +++ b/user_service.hpp @@ -0,0 +1,233 @@ +/* +// 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. +*/ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace phosphor +{ +namespace user +{ + +using UserSSHLists = + std::pair, std::vector>; +using InternalFailure = + sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure; +using InsufficientPermission = + sdbusplus::xyz::openbmc_project::Common::Error::InsufficientPermission; + +const std::string grpSsh = "ssh"; + +template +std::vector executeCmd(const char* path, ArgTypes&&... tArgs) +{ + std::vector stdOutput; + boost::process::ipstream stdOutStream; + boost::process::child execProg(path, const_cast(tArgs)..., + boost::process::std_out > stdOutStream); + std::string stdOutLine; + + while (stdOutStream && std::getline(stdOutStream, stdOutLine) && + !stdOutLine.empty()) + { + stdOutput.emplace_back(stdOutLine); + } + + execProg.wait(); + + int retCode = execProg.exit_code(); + if (retCode) + { + phosphor::logging::log( + "Command execution failed", + phosphor::logging::entry("PATH=%d", path), + phosphor::logging::entry("RETURN_CODE:%d", retCode)); + phosphor::logging::elog(); + } + + return stdOutput; +} + +/** @class UserServiceInterface + * @brief Interface class for methods provided by the implemmentations + * of the user service. Provides the same methods as the UserService + * class. + */ +class UserServiceInterface +{ + public: + UserServiceInterface() = default; + virtual ~UserServiceInterface() = default; + virtual UserSSHLists getUserAndSshGrpList() const = 0; + virtual std::vector + getUsersInGroup(const std::string& groupName) const = 0; + virtual void createUser(const std::string& userName, + const std::vector& groupNames, + const std::string& priv, + const bool& enabled) const = 0; + virtual void renameUser(const std::string& userName, + const std::string& newUserName) const = 0; + virtual void deleteUser(const std::string& userName) const = 0; + virtual void updateGroupsAndPriv(const std::string& userName, + const std::vector& groupNames, + const std::string& priv) const = 0; + virtual void updateUserStatus(const std::string& userName, + const bool& enabled) const = 0; + virtual bool isUserEnabled(const std::string& userName) const = 0; + virtual std::vector + getUserGroups(const std::string& userName) const = 0; + virtual void createGroup(const std::string& groupName) const = 0; +}; + +/** @class UserService + * @brief Responsible for managing the user service for the user manager. + * This service is the one responsible to actually change the user information + * of the application. It can support sevaral services, currently the ones + * supported are: + * + * 1) Shadow: Which uses the /etc/shadow file for updating the users + * 2) SSSD: Which uses the sssd service for a LOCAL domain only right now. + */ +class UserService +{ + public: + UserService() = delete; + UserService(const UserService&) = delete; + UserService& operator=(const UserService&) = delete; + UserService(UserService&&) = delete; + UserService& operator=(UserService&&) = delete; + + // Service Types implemented. None is used to validate. + enum class ServiceType + { + none, + shadow, + sssd + }; + + UserService(const ServiceType& srvcType, + const std::vector& groups, + const std::vector& privs); + ~UserService(); + + /** @brief update the current Service type of the instance. + * This function is used to update in real time the service + * being used for the user management without restarting the + * whole service. + * + * @param[in] srvcType + * @param[in] groups + * @param[in] privs + */ + void updateServiceType(const ServiceType& srvcType, + const std::vector& groups, + const std::vector& privs); + + /** @brief get user list and SSH group members list + * This method gets the list of users from the service. + * If the userlist reference is empty, all the users will be added + * and DBus notified about them. If the list is not empty, the function + * will only update list adding the missing ones to it. It will not remove + * any extra users on the list that are not part of the service! + * + */ + UserSSHLists getUserAndSshGrpList() const; + + /** @brief Get users in group. + * This method creates a new user as requested + * + * @param[in] groupName - Name of the group which has to be queried + */ + std::vector + getUsersInGroup(const std::string& groupName) const; + + /** @brief create user method. + * This method creates a new user as requested + * + * @param[in] userName - Name of the user which has to be created + * @param[in] groupNames - Group names list, to which user has to be added. + * @param[in] priv - Privilege of the user. + * @param[in] enabled - State of the user enabled / disabled. + */ + void createUser(const std::string& userName, + const std::vector& groupNames, + const std::string& priv, const bool& enabled) const; + + /** @brief rename user method. + * This method renames the user as requested + * + * @param[in] userName - current name of the user + * @param[in] userName - user name to which it has to be renamed. + */ + void renameUser(const std::string& userName, + const std::string& newUserName) const; + + /** @brief delete user method. + * This method deletes the user as requested + * + * @param[in] userName - Name of the user which has to be deleted + */ + void deleteUser(const std::string& userName) const; + + /** @brief Updates user Groups and Privilege. + * + * @param[in] userName - Name of the user which has to be modified + * @param[in] groupNames - Group names list for user. + * @param[in] priv - Privilege of the user. + */ + void updateGroupsAndPriv(const std::string& userName, + const std::vector& groupNames, + const std::string& priv) const; + + /** @brief Updates user status + * If enabled = false: User will be disabled + * If enabled = true : User will be enabled + * + * @param[in] userName - Name of the user + * @param[in] enabled - Status of the user: enabled / disabled? + */ + void updateUserStatus(const std::string& userName, + const bool& enabled) const; + + /** @brief Verify if user is enabled or not + * If enabled returns true + * If not enabled returns false + * + * @param[in] userName - Name of the user + */ + bool isUserEnabled(const std::string& userName) const; + + /** @brief Get the list of groups a user belongs to + * + * @param[in] userName - Name of the user + */ + std::vector getUserGroups(const std::string& userName) const; + + private: + // User service implementation. + void setServiceImpl(const ServiceType& srvcType, + const std::vector& groups, + const std::vector& privs); + std::unique_ptr usrSrvcImpl; +}; + +} // namespace user +} // namespace phosphor -- 2.17.1