blob: 9cc68e194b4a731c736673a58f6dd6164b657804 [file] [log] [blame]
/*
// 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 <grp.h>
#include <pwd.h>
#include <shadow.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <boost/algorithm/string/split.hpp>
#include <phosphor-logging/elog-errors.hpp>
#include <phosphor-logging/elog.hpp>
#include <phosphor-logging/lg2.hpp>
#include <xyz/openbmc_project/Common/error.hpp>
#include <xyz/openbmc_project/User/Common/error.hpp>
#include <algorithm>
#include <array>
#include <ctime>
#include <fstream>
#include <numeric>
#include <regex>
#include <span>
#include <string>
#include <string_view>
#include <vector>
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* pamPWHistory = "pam_pwhistory.so";
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* defaultPamPasswdConfigFile =
"/etc/pam.d/common-password";
static constexpr const char* defaultFaillockConfigFile =
"/etc/security/faillock.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<const char*, 5> predefinedGroups = {
"web", "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<const char*, 4> 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<InvalidArgument>(Argument::ARGUMENT_NAME("Group Name"),
Argument::ARGUMENT_VALUE(groupName.c_str()));
}
}
} // namespace
std::string getCSVFromVector(std::span<const std::string> 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<InvalidArgument>(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<UserNameDoesNotExist>();
}
}
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<InvalidArgument>(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<UserNameExists>();
}
}
void UserMgr::throwForUserNameConstraints(
const std::string& userName, const std::vector<std::string>& 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<UserNameGroupFail>(
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<InvalidArgument>(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<InvalidArgument>(Argument::ARGUMENT_NAME("User name"),
Argument::ARGUMENT_VALUE("Invalid data"));
}
}
void UserMgr::throwForMaxGrpUserCount(
const std::vector<std::string>& groupNames)
{
if (std::find(groupNames.begin(), groupNames.end(), "ipmi") !=
groupNames.end())
{
if (getIpmiUsersCount() >= ipmiMaxUsers)
{
lg2::error("IPMI user limit reached");
elog<NoResource>(
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<NoResource>(
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<InvalidArgument>(Argument::ARGUMENT_NAME("Privilege"),
Argument::ARGUMENT_VALUE(priv.c_str()));
}
}
void UserMgr::throwForInvalidGroups(const std::vector<std::string>& groupNames)
{
for (auto& group : groupNames)
{
if (std::find(groupsMgr.begin(), groupsMgr.end(), group) ==
groupsMgr.end())
{
lg2::error("Invalid Group Name '{GROUPNAME}'", "GROUPNAME", group);
elog<InvalidArgument>(Argument::ARGUMENT_NAME("GroupName"),
Argument::ARGUMENT_VALUE(group.c_str()));
}
}
}
std::vector<std::string> UserMgr::readAllGroupsOnSystem()
{
std::vector<std::string> 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<std::string> 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<InternalFailure>();
}
// 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<phosphor::user::Users>(
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
{
executeUserDelete(userName.c_str());
// Clear user fail records
executeUserClearFailRecords(userName.c_str());
}
catch (const InternalFailure& e)
{
lg2::error("Delete User '{USERNAME}' failed", "USERNAME", userName);
elog<InternalFailure>();
}
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<GroupNameDoesNotExists>();
}
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<InternalFailure>();
}
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<GroupNameExists>();
}
checkAndThrowForDisallowedGroupCreation(groupName);
if (groupsMgr.size() >= maxSystemGroupCount)
{
lg2::error("Group limit reached");
elog<NoResource>(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<InternalFailure>();
}
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<InternalFailure>();
}
const auto& user = usersList[userName];
std::string priv = user.get()->userPrivilege();
std::vector<std::string> 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<phosphor::user::Users>(
bus, newUserObj.c_str(), groupNames,
priv, enabled, *this));
return;
}
void UserMgr::updateGroupsAndPriv(const std::string& userName,
std::vector<std::string> 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<std::string>& oldGroupNames =
usersList[userName].get()->userGroups();
std::vector<std::string> 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<InternalFailure>();
}
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<InvalidArgument>(
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<InternalFailure>();
}
return AccountPolicyIface::minPasswordLength(value);
}
uint8_t UserMgr::rememberOldPasswordTimes(uint8_t value)
{
if (value == AccountPolicyIface::rememberOldPasswordTimes())
{
return value;
}
if (setPamModuleArgValue(pamPWHistory, remOldPasswdCount,
std::to_string(value)) != success)
{
lg2::error("Unable to set rememberOldPasswordTimes to {VALUE}", "VALUE",
value);
elog<InternalFailure>();
}
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<InternalFailure>();
}
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<InternalFailure>();
}
return AccountPolicyIface::accountUnlockTimeout(value);
}
int UserMgr::getPamModuleArgValue(const std::string& moduleName,
const std::string& argName,
std::string& argValue)
{
std::string fileName = pamPasswdConfigFile;
std::ifstream fileToRead(fileName, std::ios::in);
if (!fileToRead.is_open())
{
lg2::error("Failed to open pam configuration file {FILENAME}",
"FILENAME", fileName);
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 (line.find(moduleName) != std::string::npos)
{
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::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::setPamModuleArgValue(const std::string& moduleName,
const std::string& argName,
const std::string& argValue)
{
std::string fileName = pamPasswdConfigFile;
std::string tmpFileName = fileName + "_tmp";
std::ifstream fileToRead(fileName, std::ios::in);
std::ofstream fileToWrite(tmpFileName, std::ios::out);
if (!fileToRead.is_open() || !fileToWrite.is_open())
{
lg2::error("Failed to open pam configuration file {FILENAME}",
"FILENAME", fileName);
// Delete the unused tmp file
std::remove(tmpFileName.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 (line.find(moduleName) != std::string::npos)
{
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(tmpFileName.c_str(), fileName.c_str()) == 0)
{
return success;
}
}
// No changes, so delete the unused tmp file
std::remove(tmpFileName.c_str());
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<InternalFailure>();
}
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<std::string>& 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<InternalFailure>();
}
time_t failTimestamp = std::mktime(&tmStruct);
lastFailedAttempt = std::max(failTimestamp, lastFailedAttempt);
}
if (failAttempts < AccountPolicyIface::maxLoginAttemptBeforeLockout())
{
return false;
}
if (lastFailedAttempt +
static_cast<time_t>(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<std::string> output;
try
{
output = getFailedAttempt(userName.c_str());
}
catch (const InternalFailure& e)
{
lg2::error("Unable to read login failure counter");
elog<InternalFailure>();
}
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<InternalFailure>();
}
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 < -1)
{
// Use a default size if there is no hard limit suggested by sysconf()
buflen = 1024;
}
std::vector<char> 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<long>(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<std::string> userList;
std::vector<std::string> sshUsersList;
struct passwd pw, *pwp = nullptr;
std::array<char, 1024> buffer{};
phosphor::user::File passwd(passwdFileName, "r");
if ((passwd)() == NULL)
{
lg2::error("Error opening {FILENAME}", "FILENAME", passwdFileName);
elog<InternalFailure>();
}
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<std::string> userList = getUsersInGroup("ipmi");
return userList.size();
}
size_t UserMgr::getNonIpmiUsersCount()
{
std::vector<std::string> 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<char, 4096> 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<std::string> UserMgr::getUsersInGroup(const std::string& groupName)
{
std::vector<std::string> usersInGroup;
// Should be more than enough to get the pwd structure.
std::array<char, 4096> 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 excute 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::string>({std::move(intf)}));
auto mapperResponseMsg = bus.call(mapperCall);
if (mapperResponseMsg.is_method_error())
{
lg2::error("Error in mapper call");
elog<InternalFailure>();
}
std::map<std::string, std::vector<std::string>> mapperResponse;
mapperResponseMsg.read(mapperResponse);
if (mapperResponse.begin() == mapperResponse.end())
{
lg2::error("Invalid response from mapper");
elog<InternalFailure>();
}
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<char> 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<UserNameDoesNotExist>();
}
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<char> 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<bool>(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<std::string>(propValue);
}
else if (propName == "Privilege")
{
privilege = std::get<std::string>(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<InternalFailure>();
}
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<decltype(value)>::max())
{
throw std::out_of_range("Out of range");
}
value = static_cast<decltype(value)>(tmp);
}
catch (const std::exception& e)
{
lg2::error("Exception for MinPasswordLength: {ERR}", "ERR", e);
throw;
}
AccountPolicyIface::minPasswordLength(value);
}
valueStr.clear();
if (getPamModuleArgValue(pamPWHistory, remOldPasswdCount, valueStr) !=
success)
{
AccountPolicyIface::rememberOldPasswordTimes(0);
}
else
{
value = 0;
try
{
tmp = std::stoul(valueStr, nullptr);
if (tmp > std::numeric_limits<decltype(value)>::max())
{
throw std::out_of_range("Out of range");
}
value = static_cast<decltype(value)>(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<decltype(value16)>::max())
{
throw std::out_of_range("Out of range");
}
value16 = static_cast<decltype(value16)>(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<decltype(value32)>::max())
{
throw std::out_of_range("Out of range");
}
value32 = static_cast<decltype(value32)>(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<std::string> userNameList;
std::vector<std::string> sshGrpUsersList;
UserSSHLists userSSHLists = getUserAndSshGrpList();
userNameList = std::move(userSSHLists.first);
sshGrpUsersList = std::move(userSSHLists.second);
if (!userNameList.empty())
{
std::map<std::string, std::vector<std::string>> 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<std::string> grpUsersList = getUsersInGroup(grp);
groupLists.emplace(grp, grpUsersList);
}
}
for (auto& grp : privMgr)
{
std::vector<std::string> grpUsersList = getUsersInGroup(grp);
groupLists.emplace(grp, grpUsersList);
}
for (auto& user : userNameList)
{
std::vector<std::string> userGroups;
std::string userPriv;
for (const auto& grp : groupLists)
{
std::vector<std::string> 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<phosphor::user::Users>(
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),
pamPasswdConfigFile(defaultPamPasswdConfigFile),
faillockConfigFile(defaultFaillockConfigFile),
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<std::string> UserMgr::getFailedAttempt(const char* userName)
{
return executeCmd("/usr/sbin/faillock", "--user", userName);
}
} // namespace user
} // namespace phosphor