blob: 05aa6e73d5a37c9097340f9b911b3cbd2be4beee [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 <boost/process/child.hpp>
#include <boost/process/io.hpp>
#include <phosphor-logging/elog-errors.hpp>
#include <phosphor-logging/elog.hpp>
#include <phosphor-logging/log.hpp>
#include <xyz/openbmc_project/Common/error.hpp>
#include <xyz/openbmc_project/User/Common/error.hpp>
#include <algorithm>
#include <fstream>
#include <numeric>
#include <regex>
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;
// pam modules related
static constexpr const char* pamTally2 = "pam_tally2.so";
static constexpr const char* pamCrackLib = "pam_cracklib.so";
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* pamPasswdConfigFile = "/etc/pam.d/common-password";
static constexpr const char* pamAuthConfigFile = "/etc/pam.d/common-auth";
// 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;
template <typename... ArgTypes>
static std::vector<std::string> executeCmd(const char* path,
ArgTypes&&... tArgs)
{
std::vector<std::string> stdOutput;
boost::process::ipstream stdOutStream;
boost::process::child execProg(path, const_cast<char*>(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<level::ERR>("Command execution failed", entry("PATH=%d", path),
entry("RETURN_CODE:%d", retCode));
elog<InternalFailure>();
}
return stdOutput;
}
static std::string getCSVFromVector(std::vector<std::string> vec)
{
switch (vec.size())
{
case 0:
{
return "";
}
break;
case 1:
{
return std::string{vec[0]};
}
break;
default:
{
return std::accumulate(
std::next(vec.begin()), vec.end(), vec[0],
[](std::string a, std::string b) { return a + ',' + b; });
}
}
}
static bool removeStringFromCSV(std::string& csvStr, const std::string& delStr)
{
std::string::size_type delStrPos = csvStr.find(delStr);
if (delStrPos != std::string::npos)
{
// need to also delete the comma char
if (delStrPos == 0)
{
csvStr.erase(delStrPos, delStr.size() + 1);
}
else
{
csvStr.erase(delStrPos - 1, delStr.size() + 1);
}
return true;
}
return false;
}
bool UserMgr::isUserExist(const std::string& userName)
{
if (userName.empty())
{
log<level::ERR>("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) == false)
{
log<level::ERR>("User does not exist",
entry("USER_NAME=%s", userName.c_str()));
elog<UserNameDoesNotExist>();
}
}
void UserMgr::throwForUserExists(const std::string& userName)
{
if (isUserExist(userName) == true)
{
log<level::ERR>("User already exists",
entry("USER_NAME=%s", userName.c_str()));
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)
{
log<level::ERR>("IPMI user name length limitation",
entry("SIZE=%d", userName.length()));
elog<UserNameGroupFail>(
xyz::openbmc_project::User::Common::UserNameGroupFail::REASON(
"IPMI length"));
}
}
if (userName.length() > systemMaxUserNameLen)
{
log<level::ERR>("User name length limitation",
entry("SIZE=%d", userName.length()));
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]*")))
{
log<level::ERR>("Invalid user name",
entry("USER_NAME=%s", userName.c_str()));
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)
{
log<level::ERR>("IPMI user limit reached");
elog<NoResource>(
xyz::openbmc_project::User::Common::NoResource::REASON(
"ipmi user count reached"));
}
}
else
{
if (usersList.size() > 0 && (usersList.size() - getIpmiUsersCount()) >=
(maxSystemUsers - ipmiMaxUsers))
{
log<level::ERR>("Non-ipmi User limit reached");
elog<NoResource>(
xyz::openbmc_project::User::Common::NoResource::REASON(
"Non-ipmi user count reached"));
}
}
return;
}
void UserMgr::throwForInvalidPrivilege(const std::string& priv)
{
if (!priv.empty() &&
(std::find(privMgr.begin(), privMgr.end(), priv) == privMgr.end()))
{
log<level::ERR>("Invalid privilege");
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())
{
log<level::ERR>("Invalid Group Name listed");
elog<InvalidArgument>(Argument::ARGUMENT_NAME("GroupName"),
Argument::ARGUMENT_VALUE(group.c_str()));
}
}
}
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
{
executeCmd("/usr/sbin/useradd", userName.c_str(), "-G", groups.c_str(),
"-m", "-N", "-s",
(sshRequested ? "/bin/sh" : "/bin/nologin"), "-e",
(enabled ? "" : "1970-01-02"));
}
catch (const InternalFailure& e)
{
log<level::ERR>("Unable to create new user");
elog<InternalFailure>();
}
// Add the users object before sending out the signal
std::string userObj = std::string(usersObjPath) + "/" + userName;
std::sort(groupNames.begin(), groupNames.end());
usersList.emplace(
userName, std::move(std::make_unique<phosphor::user::Users>(
bus, userObj.c_str(), groupNames, priv, enabled, *this)));
log<level::INFO>("User created successfully",
entry("USER_NAME=%s", userName.c_str()));
return;
}
void UserMgr::deleteUser(std::string userName)
{
// All user management lock has to be based on /etc/shadow
// 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<level::ERR>("User delete failed",
entry("USER_NAME=%s", userName.c_str()));
elog<InternalFailure>();
}
usersList.erase(userName);
log<level::INFO>("User deleted successfully",
entry("USER_NAME=%s", userName.c_str()));
return;
}
void UserMgr::renameUser(std::string userName, std::string newUserName)
{
// All user management lock has to be based on /etc/shadow
// 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<level::ERR>("User rename failed",
entry("USER_NAME=%s", userName.c_str()));
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();
std::string newUserObj = std::string(usersObjPath) + "/" + newUserName;
// Special group 'ipmi' needs a way to identify user renamed, in order to
// update encrypted password. It can't rely only on InterfacesRemoved &
// InterfacesAdded. So first send out userRenamed signal.
this->userRenamed(userName, newUserName);
usersList.erase(userName);
usersList.emplace(
newUserName,
std::move(std::make_unique<phosphor::user::Users>(
bus, newUserObj.c_str(), groupNames, priv, enabled, *this)));
return;
}
void UserMgr::updateGroupsAndPriv(const std::string& userName,
const 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
{
executeCmd("/usr/sbin/usermod", userName.c_str(), "-G", groups.c_str(),
"-s", (sshRequested ? "/bin/sh" : "/bin/nologin"));
}
catch (const InternalFailure& e)
{
log<level::ERR>("Unable to modify user privilege / groups");
elog<InternalFailure>();
}
log<level::INFO>("User groups / privilege updated successfully",
entry("USER_NAME=%s", userName.c_str()));
return;
}
uint8_t UserMgr::minPasswordLength(uint8_t value)
{
if (value == AccountPolicyIface::minPasswordLength())
{
return value;
}
if (value < minPasswdLength)
{
return value;
}
if (setPamModuleArgValue(pamCrackLib, minPasswdLenProp,
std::to_string(value)) != success)
{
log<level::ERR>("Unable to set minPasswordLength");
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)
{
log<level::ERR>("Unable to set rememberOldPasswordTimes");
elog<InternalFailure>();
}
return AccountPolicyIface::rememberOldPasswordTimes(value);
}
uint16_t UserMgr::maxLoginAttemptBeforeLockout(uint16_t value)
{
if (value == AccountPolicyIface::maxLoginAttemptBeforeLockout())
{
return value;
}
if (setPamModuleArgValue(pamTally2, maxFailedAttempt,
std::to_string(value)) != success)
{
log<level::ERR>("Unable to set maxLoginAttemptBeforeLockout");
elog<InternalFailure>();
}
return AccountPolicyIface::maxLoginAttemptBeforeLockout(value);
}
uint32_t UserMgr::accountUnlockTimeout(uint32_t value)
{
if (value == AccountPolicyIface::accountUnlockTimeout())
{
return value;
}
if (setPamModuleArgValue(pamTally2, unlockTimeout, std::to_string(value)) !=
success)
{
log<level::ERR>("Unable to set accountUnlockTimeout");
elog<InternalFailure>();
}
return AccountPolicyIface::accountUnlockTimeout(value);
}
int UserMgr::getPamModuleArgValue(const std::string& moduleName,
const std::string& argName,
std::string& argValue)
{
std::string fileName;
if (moduleName == pamTally2)
{
fileName = pamAuthConfigFile;
}
else
{
fileName = pamPasswdConfigFile;
}
std::ifstream fileToRead(fileName, std::ios::in);
if (!fileToRead.is_open())
{
log<level::ERR>("Failed to open pam configuration file",
entry("FILE_NAME=%s", fileName.c_str()));
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::setPamModuleArgValue(const std::string& moduleName,
const std::string& argName,
const std::string& argValue)
{
std::string fileName;
if (moduleName == pamTally2)
{
fileName = pamAuthConfigFile;
}
else
{
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())
{
log<level::ERR>("Failed to open pam configuration /tmp file",
entry("FILE_NAME=%s", fileName.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;
}
}
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
{
executeCmd("/usr/sbin/usermod", userName.c_str(), "-e",
(enabled ? "" : "1970-01-02"));
}
catch (const InternalFailure& e)
{
log<level::ERR>("Unable to modify user enabled state");
elog<InternalFailure>();
}
log<level::INFO>("User enabled/disabled state updated successfully",
entry("USER_NAME=%s", userName.c_str()),
entry("ENABLED=%d", enabled));
return;
}
/**
* pam_tally2 app will provide the user failure count and failure status
* in second line of output with words position [0] - user name,
* [1] - failure count, [2] - latest timestamp, [3] - failure timestamp
* [4] - failure app
**/
static constexpr size_t t2UserIdx = 0;
static constexpr size_t t2FailCntIdx = 1;
static constexpr size_t t2OutputIndex = 1;
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{};
std::vector<std::string> output;
output = executeCmd("/usr/sbin/pam_tally2", "-u", userName.c_str());
std::vector<std::string> splitWords;
boost::algorithm::split(splitWords, output[t2OutputIndex],
boost::algorithm::is_any_of("\t "),
boost::token_compress_on);
try
{
unsigned long tmp = std::stoul(splitWords[t2FailCntIdx], nullptr);
uint16_t value16 = 0;
if (tmp > std::numeric_limits<decltype(value16)>::max())
{
throw std::out_of_range("Out of range");
}
value16 = static_cast<decltype(value16)>(tmp);
if (AccountPolicyIface::maxLoginAttemptBeforeLockout() != 0 &&
value16 >= AccountPolicyIface::maxLoginAttemptBeforeLockout())
{
return true; // User account is locked out
}
return false; // User account is un-locked
}
catch (const std::exception& e)
{
log<level::ERR>("Exception for userLockedForFailedAttempt",
entry("WHAT=%s", e.what()));
throw;
}
}
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{};
std::vector<std::string> output;
if (value == true)
{
return userLockedForFailedAttempt(userName);
}
output = executeCmd("/usr/sbin/pam_tally2", "-u", userName.c_str(), "-r");
std::vector<std::string> splitWords;
boost::algorithm::split(splitWords, output[t2OutputIndex],
boost::algorithm::is_any_of("\t "),
boost::token_compress_on);
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 seconds_per_day = 60 * 60 * 24;
long today = static_cast<long>(time(NULL)) / seconds_per_day;
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)
{
log<level::ERR>("Error opening the passwd file");
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();
}
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
{
log<level::ERR>("Group not found",
entry("GROUP=%s", groupName.c_str()));
// 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)
{
log<level::ERR>("Unable to get the User Service",
entry("WHAT=%s", e.what()));
throw;
}
catch (const sdbusplus::exception::exception& e)
{
log<level::ERR>(
"Failed to excute method", entry("METHOD=%s", "GetManagedObjects"),
entry("PATH=%s", ldapMgrObjBasePath), entry("WHAT=%s", e.what()));
throw;
}
return objects;
}
std::string UserMgr::getLdapGroupName(const std::string& userName)
{
struct passwd pwd
{};
struct passwd* pwdPtr = 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);
gid_t gid = 0;
auto status =
getpwnam_r(userName.c_str(), &pwd, buffer.data(), buflen, &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))
{
gid = pwd.pw_gid;
}
else
{
log<level::ERR>("User does not exist",
entry("USER_NAME=%s", userName.c_str()));
elog<UserNameDoesNotExist>();
}
struct group* groups = nullptr;
std::string ldapGroupName;
while ((groups = getgrent()) != NULL)
{
if (groups->gr_gid == gid)
{
ldapGroupName = groups->gr_name;
break;
}
}
// Call endgrent() to close the group database.
endgrent();
return ldapGroupName;
}
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())
{
log<level::ERR>("Error in mapper call");
elog<InternalFailure>();
}
std::map<std::string, std::vector<std::string>> mapperResponse;
mapperResponseMsg.read(mapperResponse);
if (mapperResponse.begin() == mapperResponse.end())
{
log<level::ERR>("Invalid response from mapper");
elog<InternalFailure>();
}
return mapperResponse.begin()->first;
}
UserInfoMap UserMgr::getUserInfo(std::string userName)
{
UserInfoMap userInfo;
// Check whether the given user is local user or not.
if (isUserExist(userName) == true)
{
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
{
std::string ldapGroupName = getLdapGroupName(userName);
if (ldapGroupName.empty())
{
log<level::ERR>("Unable to get group name",
entry("USER_NAME=%s", userName.c_str()));
elog<InternalFailure>();
}
DbusUserObj objects = getPrivilegeMapperObject();
std::string privilege;
std::string groupName;
std::string ldapConfigPath;
try
{
for (const auto& obj : objects)
{
for (const auto& interface : obj.second)
{
if ((interface.first ==
"xyz.openbmc_project.Object.Enable"))
{
for (const auto& property : interface.second)
{
auto value = std::get<bool>(property.second);
if ((property.first == "Enabled") &&
(value == true))
{
ldapConfigPath = obj.first;
break;
}
}
}
}
if (!ldapConfigPath.empty())
{
break;
}
}
if (ldapConfigPath.empty())
{
return userInfo;
}
for (const auto& obj : objects)
{
for (const auto& interface : obj.second)
{
if ((interface.first ==
"xyz.openbmc_project.User.PrivilegeMapperEntry") &&
(obj.first.str.find(ldapConfigPath) !=
std::string::npos))
{
for (const auto& property : interface.second)
{
auto value = std::get<std::string>(property.second);
if (property.first == "GroupName")
{
groupName = value;
}
else if (property.first == "Privilege")
{
privilege = value;
}
if (groupName == ldapGroupName)
{
userInfo["UserPrivilege"] = privilege;
}
}
}
}
}
auto priv = std::get<std::string>(userInfo["UserPrivilege"]);
if (priv.empty())
{
log<level::ERR>("LDAP group privilege mapping does not exist");
}
}
catch (const std::bad_variant_access& e)
{
log<level::ERR>("Error while accessing variant",
entry("WHAT=%s", e.what()));
elog<InternalFailure>();
}
userInfo.emplace("RemoteUser", true);
}
return userInfo;
}
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;
for (auto& grp : groupsMgr)
{
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.
auto objPath = std::string(usersObjPath) + "/" + user;
std::sort(userGroups.begin(), userGroups.end());
usersList.emplace(user,
std::move(std::make_unique<phosphor::user::Users>(
bus, objPath.c_str(), userGroups, userPriv,
isUserEnabled(user), *this)));
}
}
}
UserMgr::UserMgr(sdbusplus::bus::bus& bus, const char* path) :
Ifaces(bus, path, true), bus(bus), path(path)
{
UserMgrIface::allPrivileges(privMgr);
std::sort(groupsMgr.begin(), groupsMgr.end());
UserMgrIface::allGroups(groupsMgr);
std::string valueStr;
auto value = minPasswdLength;
unsigned long tmp = 0;
if (getPamModuleArgValue(pamCrackLib, 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)
{
log<level::ERR>("Exception for MinPasswordLength",
entry("WHAT=%s", e.what()));
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)
{
log<level::ERR>("Exception for RememberOldPasswordTimes",
entry("WHAT=%s", e.what()));
throw;
}
AccountPolicyIface::rememberOldPasswordTimes(value);
}
valueStr.clear();
if (getPamModuleArgValue(pamTally2, 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)
{
log<level::ERR>("Exception for MaxLoginAttemptBeforLockout",
entry("WHAT=%s", e.what()));
throw;
}
AccountPolicyIface::maxLoginAttemptBeforeLockout(value16);
}
valueStr.clear();
if (getPamModuleArgValue(pamTally2, 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)
{
log<level::ERR>("Exception for AccountUnlockTimeout",
entry("WHAT=%s", e.what()));
throw;
}
AccountPolicyIface::accountUnlockTimeout(value32);
}
initUserObjects();
// emit the signal
this->emit_object_added();
}
} // namespace user
} // namespace phosphor