/* // 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 "config.h" #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 #include #include namespace phosphor { namespace user { static constexpr const char* passwdFileName = "/etc/passwd"; static constexpr size_t ipmiMaxUserNameLen = 16; static constexpr size_t systemMaxUserNameLen = 30; static constexpr const char* grpSsh = "ssh"; static constexpr int success = 0; static constexpr int failure = -1; // pam modules related static constexpr const char* minPasswdLenProp = "minlen"; static constexpr const char* remOldPasswdCount = "remember"; static constexpr const char* maxFailedAttempt = "deny"; static constexpr const char* unlockTimeout = "unlock_time"; static constexpr const char* defaultFaillockConfigFile = "/etc/security/faillock.conf"; static constexpr const char* defaultPWHistoryConfigFile = "/etc/security/pwhistory.conf"; static constexpr const char* defaultPWQualityConfigFile = "/etc/security/pwquality.conf"; // Object Manager related static constexpr const char* ldapMgrObjBasePath = "/xyz/openbmc_project/user/ldap"; // Object Mapper related static constexpr const char* objMapperService = "xyz.openbmc_project.ObjectMapper"; static constexpr const char* objMapperPath = "/xyz/openbmc_project/object_mapper"; static constexpr const char* objMapperInterface = "xyz.openbmc_project.ObjectMapper"; 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; using GroupNameExists = sdbusplus::xyz::openbmc_project::User::Common::Error::GroupNameExists; using GroupNameDoesNotExists = sdbusplus::xyz::openbmc_project::User::Common::Error::GroupNameDoesNotExist; namespace { // The hardcoded groups in OpenBMC projects constexpr std::array predefinedGroups = { "redfish", "ipmi", "ssh", "hostconsole"}; // These prefixes are for Dynamic Redfish authorization. See // https://github.com/openbmc/docs/blob/master/designs/redfish-authorization.md // Base role and base privileges are added by Redfish implementation (e.g., // BMCWeb) at compile time constexpr std::array allowedGroupPrefix = { "openbmc_rfr_", // OpenBMC Redfish Base Role "openbmc_rfp_", // OpenBMC Redfish Base Privileges "openbmc_orfr_", // OpenBMC Redfish OEM Role "openbmc_orfp_", // OpenBMC Redfish OEM Privileges }; void checkAndThrowsForGroupChangeAllowed(const std::string& groupName) { bool allowed = false; for (std::string_view prefix : allowedGroupPrefix) { if (groupName.starts_with(prefix)) { allowed = true; break; } } if (!allowed) { lg2::error("Group name '{GROUP}' is not in the allowed list", "GROUP", groupName); elog(Argument::ARGUMENT_NAME("Group Name"), Argument::ARGUMENT_VALUE(groupName.c_str())); } } } // namespace std::string getCSVFromVector(std::span vec) { if (vec.empty()) { return ""; } return std::accumulate(std::next(vec.begin()), vec.end(), vec[0], [](std::string&& val, std::string_view element) { val += ','; val += element; return val; }); } 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()) { lg2::error("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)) { lg2::error("User '{USERNAME}' does not exist", "USERNAME", userName); elog(); } } void UserMgr::checkAndThrowForDisallowedGroupCreation( const std::string& groupName) { if (groupName.size() > maxSystemGroupNameLength || !std::regex_match(groupName.c_str(), std::regex("[a-zA-Z_][a-zA-Z_0-9]*"))) { lg2::error("Invalid group name '{GROUP}'", "GROUP", groupName); elog(Argument::ARGUMENT_NAME("Group Name"), Argument::ARGUMENT_VALUE(groupName.c_str())); } checkAndThrowsForGroupChangeAllowed(groupName); } void UserMgr::throwForUserExists(const std::string& userName) { if (isUserExist(userName)) { lg2::error("User '{USERNAME}' already exists", "USERNAME", userName); 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) { lg2::error("User '{USERNAME}' exceeds IPMI username length limit " "({LENGTH} > {LIMIT})", "USERNAME", userName, "LENGTH", userName.length(), "LIMIT", ipmiMaxUserNameLen); elog( xyz::openbmc_project::User::Common::UserNameGroupFail::REASON( "IPMI length")); } } if (userName.length() > systemMaxUserNameLen) { lg2::error("User '{USERNAME}' exceeds system username length limit " "({LENGTH} > {LIMIT})", "USERNAME", userName, "LENGTH", userName.length(), "LIMIT", systemMaxUserNameLen); 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]*"))) { lg2::error("Invalid username '{USERNAME}'", "USERNAME", userName); 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) { lg2::error("IPMI user limit reached"); elog( xyz::openbmc_project::User::Common::NoResource::REASON( "IPMI user limit reached")); } } else { if (usersList.size() > 0 && (usersList.size() - getIpmiUsersCount()) >= (maxSystemUsers - ipmiMaxUsers)) { lg2::error("Non-ipmi User limit reached"); elog( xyz::openbmc_project::User::Common::NoResource::REASON( "Non-ipmi user limit reached")); } } return; } void UserMgr::throwForInvalidPrivilege(const std::string& priv) { if (!priv.empty() && (std::find(privMgr.begin(), privMgr.end(), priv) == privMgr.end())) { lg2::error("Invalid privilege '{PRIVILEGE}'", "PRIVILEGE", priv); 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()) { lg2::error("Invalid Group Name '{GROUPNAME}'", "GROUPNAME", group); elog(Argument::ARGUMENT_NAME("GroupName"), Argument::ARGUMENT_VALUE(group.c_str())); } } } std::vector UserMgr::readAllGroupsOnSystem() { std::vector allGroups = {predefinedGroups.begin(), predefinedGroups.end()}; // rewinds to the beginning of the group database setgrent(); struct group* gr = getgrent(); while (gr != nullptr) { std::string group(gr->gr_name); for (std::string_view prefix : allowedGroupPrefix) { if (group.starts_with(prefix)) { allGroups.push_back(gr->gr_name); } } gr = getgrent(); } // close the group database endgrent(); return allGroups; } 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 // 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 { executeUserAdd(userName.c_str(), groups.c_str(), sshRequested, enabled); } catch (const InternalFailure& e) { lg2::error("Unable to create new user '{USERNAME}'", "USERNAME", userName); elog(); } // Add the users object before sending out the signal sdbusplus::message::object_path tempObjPath(usersObjPath); tempObjPath /= userName; std::string userObj(tempObjPath); std::sort(groupNames.begin(), groupNames.end()); usersList.emplace( userName, std::make_unique( bus, userObj.c_str(), groupNames, priv, enabled, *this)); lg2::info("User '{USERNAME}' created successfully", "USERNAME", userName); return; } 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 { // Clear user fail records executeUserClearFailRecords(userName.c_str()); executeUserDelete(userName.c_str()); } catch (const InternalFailure& e) { lg2::error("Delete User '{USERNAME}' failed", "USERNAME", userName); elog(); } usersList.erase(userName); lg2::info("User '{USERNAME}' deleted successfully", "USERNAME", userName); return; } void UserMgr::checkDeleteGroupConstraints(const std::string& groupName) { if (std::find(groupsMgr.begin(), groupsMgr.end(), groupName) == groupsMgr.end()) { lg2::error("Group '{GROUP}' already exists", "GROUP", groupName); elog(); } checkAndThrowsForGroupChangeAllowed(groupName); } void UserMgr::deleteGroup(std::string groupName) { checkDeleteGroupConstraints(groupName); try { executeGroupDeletion(groupName.c_str()); } catch (const InternalFailure& e) { lg2::error("Failed to delete group '{GROUP}'", "GROUP", groupName); elog(); } groupsMgr.erase(std::find(groupsMgr.begin(), groupsMgr.end(), groupName)); UserMgrIface::allGroups(groupsMgr); lg2::info("Successfully deleted group '{GROUP}'", "GROUP", groupName); } void UserMgr::checkCreateGroupConstraints(const std::string& groupName) { if (std::find(groupsMgr.begin(), groupsMgr.end(), groupName) != groupsMgr.end()) { lg2::error("Group '{GROUP}' already exists", "GROUP", groupName); elog(); } checkAndThrowForDisallowedGroupCreation(groupName); if (groupsMgr.size() >= maxSystemGroupCount) { lg2::error("Group limit reached"); elog(xyz::openbmc_project::User::Common::NoResource::REASON( "Group limit reached")); } } void UserMgr::createGroup(std::string groupName) { checkCreateGroupConstraints(groupName); try { executeGroupCreation(groupName.c_str()); } catch (const InternalFailure& e) { lg2::error("Failed to create group '{GROUP}'", "GROUP", groupName); elog(); } groupsMgr.push_back(groupName); UserMgrIface::allGroups(groupsMgr); } 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 { executeUserRename(userName.c_str(), newUserName.c_str()); } catch (const InternalFailure& e) { lg2::error("Rename '{USERNAME}' to '{NEWUSERNAME}' failed", "USERNAME", userName, "NEWUSERNAME", newUserName); elog(); } const auto& user = usersList[userName]; std::string priv = user.get()->userPrivilege(); std::vector groupNames = user.get()->userGroups(); bool enabled = user.get()->userEnabled(); sdbusplus::message::object_path tempObjPath(usersObjPath); tempObjPath /= newUserName; std::string newUserObj(tempObjPath); // 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::make_unique( bus, newUserObj.c_str(), groupNames, priv, enabled, *this)); return; } void UserMgr::updateGroupsAndPriv(const std::string& userName, std::vector groupNames, const std::string& priv) { 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(); 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 (!priv.empty()) { if (groups.size() != 0) { groups += ","; } groups += priv; } try { executeUserModify(userName.c_str(), groups.c_str(), sshRequested); } catch (const InternalFailure& e) { lg2::error( "Unable to modify user privilege / groups for user '{USERNAME}'", "USERNAME", userName); elog(); } std::sort(groupNames.begin(), groupNames.end()); usersList[userName]->setUserGroups(groupNames); usersList[userName]->setUserPrivilege(priv); lg2::info("User '{USERNAME}' groups / privilege updated successfully", "USERNAME", userName); } uint8_t UserMgr::minPasswordLength(uint8_t value) { if (value == AccountPolicyIface::minPasswordLength()) { return value; } if (value < minPasswdLength) { lg2::error("Attempting to set minPasswordLength to {VALUE}, less than " "{MINVALUE}", "VALUE", value, "MINVALUE", minPasswdLength); elog( Argument::ARGUMENT_NAME("minPasswordLength"), Argument::ARGUMENT_VALUE(std::to_string(value).c_str())); } if (setPamModuleConfValue(pwQualityConfigFile, minPasswdLenProp, std::to_string(value)) != success) { lg2::error("Unable to set minPasswordLength to {VALUE}", "VALUE", value); elog(); } return AccountPolicyIface::minPasswordLength(value); } uint8_t UserMgr::rememberOldPasswordTimes(uint8_t value) { if (value == AccountPolicyIface::rememberOldPasswordTimes()) { return value; } if (setPamModuleConfValue(pwHistoryConfigFile, remOldPasswdCount, std::to_string(value)) != success) { lg2::error("Unable to set rememberOldPasswordTimes to {VALUE}", "VALUE", value); elog(); } return AccountPolicyIface::rememberOldPasswordTimes(value); } uint16_t UserMgr::maxLoginAttemptBeforeLockout(uint16_t value) { if (value == AccountPolicyIface::maxLoginAttemptBeforeLockout()) { return value; } if (setPamModuleConfValue(faillockConfigFile, maxFailedAttempt, std::to_string(value)) != success) { lg2::error("Unable to set maxLoginAttemptBeforeLockout to {VALUE}", "VALUE", value); elog(); } return AccountPolicyIface::maxLoginAttemptBeforeLockout(value); } uint32_t UserMgr::accountUnlockTimeout(uint32_t value) { if (value == AccountPolicyIface::accountUnlockTimeout()) { return value; } if (setPamModuleConfValue(faillockConfigFile, unlockTimeout, std::to_string(value)) != success) { lg2::error("Unable to set accountUnlockTimeout to {VALUE}", "VALUE", value); elog(); } return AccountPolicyIface::accountUnlockTimeout(value); } int UserMgr::getPamModuleConfValue(const std::string& confFile, const std::string& argName, std::string& argValue) { std::ifstream fileToRead(confFile, std::ios::in); if (!fileToRead.is_open()) { lg2::error("Failed to open pam configuration file {FILENAME}", "FILENAME", confFile); return failure; } std::string line; auto argSearch = argName + "="; size_t startPos = 0; size_t endPos = 0; while (getline(fileToRead, line)) { // skip comments section starting with # if ((startPos = line.find('#')) != std::string::npos) { if (startPos == 0) { continue; } // skip comments after meaningful section and process those line = line.substr(0, startPos); } if ((startPos = line.find(argSearch)) != std::string::npos) { if ((endPos = line.find(' ', startPos)) == std::string::npos) { endPos = line.size(); } startPos += argSearch.size(); argValue = line.substr(startPos, endPos - startPos); return success; } } return failure; } int UserMgr::setPamModuleConfValue(const std::string& confFile, const std::string& argName, const std::string& argValue) { std::string tmpConfFile = confFile + "_tmp"; std::ifstream fileToRead(confFile, std::ios::in); std::ofstream fileToWrite(tmpConfFile, std::ios::out); if (!fileToRead.is_open() || !fileToWrite.is_open()) { lg2::error("Failed to open pam configuration file {FILENAME}", "FILENAME", confFile); // Delete the unused tmp file std::remove(tmpConfFile.c_str()); return failure; } std::string line; auto argSearch = argName + "="; size_t startPos = 0; size_t endPos = 0; bool found = false; while (getline(fileToRead, line)) { // skip comments section starting with # if ((startPos = line.find('#')) != std::string::npos) { if (startPos == 0) { fileToWrite << line << std::endl; continue; } // skip comments after meaningful section and process those line = line.substr(0, startPos); } if ((startPos = line.find(argSearch)) != std::string::npos) { if ((endPos = line.find(' ', startPos)) == std::string::npos) { endPos = line.size(); } startPos += argSearch.size(); fileToWrite << line.substr(0, startPos) << argValue << line.substr(endPos, line.size() - endPos) << std::endl; found = true; continue; } fileToWrite << line << std::endl; } fileToWrite.close(); fileToRead.close(); if (found) { if (std::rename(tmpConfFile.c_str(), confFile.c_str()) == 0) { return success; } } // No changes, so delete the unused tmp file std::remove(tmpConfFile.c_str()); return failure; } 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 { executeUserModifyUserEnable(userName.c_str(), enabled); } catch (const InternalFailure& e) { lg2::error("Unable to modify user enabled state for '{USERNAME}'", "USERNAME", userName); elog(); } usersList[userName]->setUserEnabled(enabled); lg2::info("User '{USERNAME}' has been {STATUS}", "USERNAME", userName, "STATUS", enabled ? "Enabled" : "Disabled"); } /** * faillock app will provide the user failed login list with when the attempt * was made, the type, the source, and if it's valid. * * Valid in this case means that the attempt was made within the fail_interval * time. So, we can check this list for the number of valid entries (lines * ending with 'V') compared to the maximum allowed to determine if the user is * locked out. * * This data is only refreshed when an attempt is made, so if the user appears * to be locked out, we must also check if the most recent attempt was older * than the unlock_time to know if the user has since been unlocked. **/ bool UserMgr::parseFaillockForLockout( const std::vector& faillockOutput) { uint16_t failAttempts = 0; time_t lastFailedAttempt{}; for (const std::string& line : faillockOutput) { if (!line.ends_with("V")) { continue; } // Count this failed attempt failAttempts++; // Update the last attempt time // First get the "when" which is the first two words (date and time) size_t pos = line.find(" "); if (pos == std::string::npos) { continue; } pos = line.find(" ", pos + 1); if (pos == std::string::npos) { continue; } std::string failDateTime = line.substr(0, pos); // NOTE: Cannot use std::get_time() here as the implementation of %y in // libstdc++ does not match POSIX strptime() before gcc 12.1.0 // https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=a8d3c98746098e2784be7144c1ccc9fcc34a0888 std::tm tmStruct = {}; if (!strptime(failDateTime.c_str(), "%F %T", &tmStruct)) { lg2::error("Failed to parse latest failure date/time"); elog(); } time_t failTimestamp = std::mktime(&tmStruct); lastFailedAttempt = std::max(failTimestamp, lastFailedAttempt); } if (failAttempts < AccountPolicyIface::maxLoginAttemptBeforeLockout()) { return false; } if (lastFailedAttempt + static_cast(AccountPolicyIface::accountUnlockTimeout()) <= std::time(NULL)) { return false; } return true; } bool UserMgr::userLockedForFailedAttempt(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{}; if (AccountPolicyIface::maxLoginAttemptBeforeLockout() == 0) { return false; } std::vector output; try { output = getFailedAttempt(userName.c_str()); } catch (const InternalFailure& e) { lg2::error("Unable to read login failure counter"); elog(); } return parseFaillockForLockout(output); } bool UserMgr::userLockedForFailedAttempt(const std::string& userName, const bool& value) { // All user management lock has to be based on /etc/shadow // TODO phosphor-user-manager#10 phosphor::user::shadow::Lock lock{}; if (value == true) { return userLockedForFailedAttempt(userName); } try { // Clear user fail records executeUserClearFailRecords(userName.c_str()); } catch (const InternalFailure& e) { lg2::error("Unable to reset login failure counter"); elog(); } return userLockedForFailedAttempt(userName); } bool UserMgr::userPasswordExpired(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{}; struct spwd spwd {}; struct spwd* spwdPtr = nullptr; auto buflen = sysconf(_SC_GETPW_R_SIZE_MAX); if (buflen <= 0) { // Use a default size if there is no hard limit suggested by sysconf() buflen = 1024; } std::vector buffer(buflen); auto status = getspnam_r(userName.c_str(), &spwd, buffer.data(), buflen, &spwdPtr); // On success, getspnam_r() returns zero, and sets *spwdPtr to spwd. // If no matching password record was found, these functions return 0 // and store NULL in *spwdPtr if ((status == 0) && (&spwd == spwdPtr)) { // Determine password validity per "chage" docs, where: // spwd.sp_lstchg == 0 means password is expired, and // spwd.sp_max == -1 means the password does not expire. constexpr long secondsPerDay = 60 * 60 * 24; long today = static_cast(time(NULL)) / secondsPerDay; if ((spwd.sp_lstchg == 0) || ((spwd.sp_max != -1) && ((spwd.sp_max + spwd.sp_lstchg) < today))) { return true; } } else { // User entry is missing in /etc/shadow, indicating no SHA password. // Treat this as new user without password entry in /etc/shadow // TODO: Add property to indicate user password was not set yet // https://github.com/openbmc/phosphor-user-manager/issues/8 return false; } return false; } 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) { lg2::error("Error opening {FILENAME}", "FILENAME", passwdFileName); 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)); } size_t UserMgr::getIpmiUsersCount() { std::vector userList = getUsersInGroup("ipmi"); return userList.size(); } size_t UserMgr::getNonIpmiUsersCount() { std::vector ipmiUsers = getUsersInGroup("ipmi"); return usersList.size() - ipmiUsers.size(); } 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. } 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 { lg2::error("Group '{GROUPNAME}' not found", "GROUPNAME", groupName); // Don't throw error, just return empty userList - fallback } return usersInGroup; } DbusUserObj UserMgr::getPrivilegeMapperObject(void) { DbusUserObj objects; try { std::string basePath = "/xyz/openbmc_project/user/ldap/openldap"; std::string interface = "xyz.openbmc_project.User.Ldap.Config"; auto ldapMgmtService = getServiceName(std::move(basePath), std::move(interface)); auto method = bus.new_method_call( ldapMgmtService.c_str(), ldapMgrObjBasePath, "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); auto reply = bus.call(method); reply.read(objects); } catch (const InternalFailure& e) { lg2::error("Unable to get the User Service: {ERR}", "ERR", e); throw; } catch (const sdbusplus::exception_t& e) { lg2::error("Failed to execute GetManagedObjects at {PATH}: {ERR}", "PATH", ldapMgrObjBasePath, "ERR", e); throw; } return objects; } std::string UserMgr::getServiceName(std::string&& path, std::string&& intf) { auto mapperCall = bus.new_method_call(objMapperService, objMapperPath, objMapperInterface, "GetObject"); mapperCall.append(std::move(path)); mapperCall.append(std::vector({std::move(intf)})); auto mapperResponseMsg = bus.call(mapperCall); if (mapperResponseMsg.is_method_error()) { lg2::error("Error in mapper call"); elog(); } std::map> mapperResponse; mapperResponseMsg.read(mapperResponse); if (mapperResponse.begin() == mapperResponse.end()) { lg2::error("Invalid response from mapper"); elog(); } return mapperResponse.begin()->first; } gid_t UserMgr::getPrimaryGroup(const std::string& userName) const { static auto buflen = sysconf(_SC_GETPW_R_SIZE_MAX); if (buflen <= 0) { // Use a default size if there is no hard limit suggested by sysconf() buflen = 1024; } struct passwd pwd; struct passwd* pwdPtr = nullptr; std::vector buffer(buflen); auto status = getpwnam_r(userName.c_str(), &pwd, buffer.data(), buffer.size(), &pwdPtr); // On success, getpwnam_r() returns zero, and set *pwdPtr to pwd. // If no matching password record was found, these functions return 0 // and store NULL in *pwdPtr if (!status && (&pwd == pwdPtr)) { return pwd.pw_gid; } lg2::error("User {USERNAME} does not exist", "USERNAME", userName); elog(); } bool UserMgr::isGroupMember(const std::string& userName, gid_t primaryGid, const std::string& groupName) const { static auto buflen = sysconf(_SC_GETGR_R_SIZE_MAX); if (buflen <= 0) { // Use a default size if there is no hard limit suggested by sysconf() buflen = 1024; } struct group grp; struct group* grpPtr = nullptr; std::vector buffer(buflen); auto status = getgrnam_r(groupName.c_str(), &grp, buffer.data(), buffer.size(), &grpPtr); // Groups with a lot of members may require a buffer of bigger size than // suggested by _SC_GETGR_R_SIZE_MAX. // 32K should be enough for about 2K members. constexpr auto maxBufferLength = 32 * 1024; while (status == ERANGE && buflen < maxBufferLength) { buflen *= 2; buffer.resize(buflen); lg2::debug("Increase buffer for getgrnam_r() to {SIZE}", "SIZE", buflen); status = getgrnam_r(groupName.c_str(), &grp, buffer.data(), buffer.size(), &grpPtr); } // On success, getgrnam_r() returns zero, and set *grpPtr to grp. // If no matching group record was found, these functions return 0 // and store NULL in *grpPtr if (!status && (&grp == grpPtr)) { if (primaryGid == grp.gr_gid) { return true; } for (auto i = 0; grp.gr_mem && grp.gr_mem[i]; ++i) { if (userName == grp.gr_mem[i]) { return true; } } } else if (status == ERANGE) { lg2::error("Group info of {GROUP} requires too much memory", "GROUP", groupName); } else { lg2::error("Group {GROUP} does not exist", "GROUP", groupName); } return false; } void UserMgr::executeGroupCreation(const char* groupName) { executeCmd("/usr/sbin/groupadd", groupName); } void UserMgr::executeGroupDeletion(const char* groupName) { executeCmd("/usr/sbin/groupdel", groupName); } UserInfoMap UserMgr::getUserInfo(std::string userName) { UserInfoMap userInfo; // Check whether the given user is local user or not. if (isUserExist(userName)) { const auto& user = usersList[userName]; userInfo.emplace("UserPrivilege", user.get()->userPrivilege()); userInfo.emplace("UserGroups", user.get()->userGroups()); userInfo.emplace("UserEnabled", user.get()->userEnabled()); userInfo.emplace("UserLockedForFailedAttempt", user.get()->userLockedForFailedAttempt()); userInfo.emplace("UserPasswordExpired", user.get()->userPasswordExpired()); userInfo.emplace("RemoteUser", false); } else { auto primaryGid = getPrimaryGroup(userName); DbusUserObj objects = getPrivilegeMapperObject(); std::string ldapConfigPath; std::string userPrivilege; try { for (const auto& [path, interfaces] : objects) { auto it = interfaces.find("xyz.openbmc_project.Object.Enable"); if (it != interfaces.end()) { auto propIt = it->second.find("Enabled"); if (propIt != it->second.end() && std::get(propIt->second)) { ldapConfigPath = path.str + '/'; break; } } } if (ldapConfigPath.empty()) { return userInfo; } for (const auto& [path, interfaces] : objects) { if (!path.str.starts_with(ldapConfigPath)) { continue; } auto it = interfaces.find( "xyz.openbmc_project.User.PrivilegeMapperEntry"); if (it != interfaces.end()) { std::string privilege; std::string groupName; for (const auto& [propName, propValue] : it->second) { if (propName == "GroupName") { groupName = std::get(propValue); } else if (propName == "Privilege") { privilege = std::get(propValue); } } if (!groupName.empty() && !privilege.empty() && isGroupMember(userName, primaryGid, groupName)) { userPrivilege = privilege; break; } } if (!userPrivilege.empty()) { break; } } if (!userPrivilege.empty()) { userInfo.emplace("UserPrivilege", userPrivilege); } else { lg2::warning("LDAP group privilege mapping does not exist, " "default \"priv-user\" is used"); userInfo.emplace("UserPrivilege", "priv-user"); } } catch (const std::bad_variant_access& e) { lg2::error("Error while accessing variant: {ERR}", "ERR", e); elog(); } userInfo.emplace("RemoteUser", true); } return userInfo; } void UserMgr::initializeAccountPolicy() { std::string valueStr; auto value = minPasswdLength; unsigned long tmp = 0; if (getPamModuleConfValue(pwQualityConfigFile, minPasswdLenProp, valueStr) != success) { AccountPolicyIface::minPasswordLength(minPasswdLength); } else { try { tmp = std::stoul(valueStr, nullptr); if (tmp > std::numeric_limits::max()) { throw std::out_of_range("Out of range"); } value = static_cast(tmp); } catch (const std::exception& e) { lg2::error("Exception for MinPasswordLength: {ERR}", "ERR", e); throw; } AccountPolicyIface::minPasswordLength(value); } valueStr.clear(); if (getPamModuleConfValue(pwHistoryConfigFile, remOldPasswdCount, valueStr) != success) { AccountPolicyIface::rememberOldPasswordTimes(0); } else { value = 0; try { tmp = std::stoul(valueStr, nullptr); if (tmp > std::numeric_limits::max()) { throw std::out_of_range("Out of range"); } value = static_cast(tmp); } catch (const std::exception& e) { lg2::error("Exception for RememberOldPasswordTimes: {ERR}", "ERR", e); throw; } AccountPolicyIface::rememberOldPasswordTimes(value); } valueStr.clear(); if (getPamModuleConfValue(faillockConfigFile, maxFailedAttempt, valueStr) != success) { AccountPolicyIface::maxLoginAttemptBeforeLockout(0); } else { uint16_t value16 = 0; try { tmp = std::stoul(valueStr, nullptr); if (tmp > std::numeric_limits::max()) { throw std::out_of_range("Out of range"); } value16 = static_cast(tmp); } catch (const std::exception& e) { lg2::error("Exception for MaxLoginAttemptBeforLockout: {ERR}", "ERR", e); throw; } AccountPolicyIface::maxLoginAttemptBeforeLockout(value16); } valueStr.clear(); if (getPamModuleConfValue(faillockConfigFile, unlockTimeout, valueStr) != success) { AccountPolicyIface::accountUnlockTimeout(0); } else { uint32_t value32 = 0; try { tmp = std::stoul(valueStr, nullptr); if (tmp > std::numeric_limits::max()) { throw std::out_of_range("Out of range"); } value32 = static_cast(tmp); } catch (const std::exception& e) { lg2::error("Exception for AccountUnlockTimeout: {ERR}", "ERR", e); throw; } AccountPolicyIface::accountUnlockTimeout(value32); } } 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); if (!userNameList.empty()) { std::map> groupLists; // We only track users that are in the |predefinedGroups| // The other groups don't contain real BMC users. for (const char* grp : predefinedGroups) { 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. sdbusplus::message::object_path tempObjPath(usersObjPath); tempObjPath /= user; std::string objPath(tempObjPath); std::sort(userGroups.begin(), userGroups.end()); usersList.emplace(user, std::make_unique( bus, objPath.c_str(), userGroups, userPriv, isUserEnabled(user), *this)); } } } UserMgr::UserMgr(sdbusplus::bus_t& bus, const char* path) : Ifaces(bus, path, Ifaces::action::defer_emit), bus(bus), path(path), faillockConfigFile(defaultFaillockConfigFile), pwHistoryConfigFile(defaultPWHistoryConfigFile), pwQualityConfigFile(defaultPWQualityConfigFile) { UserMgrIface::allPrivileges(privMgr); groupsMgr = readAllGroupsOnSystem(); std::sort(groupsMgr.begin(), groupsMgr.end()); UserMgrIface::allGroups(groupsMgr); initializeAccountPolicy(); initUserObjects(); // emit the signal this->emit_object_added(); } void UserMgr::executeUserAdd(const char* userName, const char* groups, bool sshRequested, bool enabled) { // 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, "-G", groups, "-m", "-N", "-s", (sshRequested ? "/bin/sh" : "/sbin/nologin"), "-e", (enabled ? "" : "1970-01-01")); } void UserMgr::executeUserDelete(const char* userName) { executeCmd("/usr/sbin/userdel", userName, "-r"); } void UserMgr::executeUserClearFailRecords(const char* userName) { executeCmd("/usr/sbin/faillock", "--user", userName, "--reset"); } void UserMgr::executeUserRename(const char* userName, const char* newUserName) { std::string newHomeDir = "/home/"; newHomeDir += newUserName; executeCmd("/usr/sbin/usermod", "-l", newUserName, userName, "-d", newHomeDir.c_str(), "-m"); } void UserMgr::executeUserModify(const char* userName, const char* newGroups, bool sshRequested) { executeCmd("/usr/sbin/usermod", userName, "-G", newGroups, "-s", (sshRequested ? "/bin/sh" : "/sbin/nologin")); } void UserMgr::executeUserModifyUserEnable(const char* userName, bool enabled) { // 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, "-e", (enabled ? "" : "1970-01-01")); } std::vector UserMgr::getFailedAttempt(const char* userName) { return executeCmd("/usr/sbin/faillock", "--user", userName); } } // namespace user } // namespace phosphor