blob: ebed493bab07deb40ad765433cfa2bc2569ffa9a [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 <shadow.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fstream>
#include <grp.h>
#include <pwd.h>
#include <regex>
#include <algorithm>
#include <numeric>
#include <boost/process/child.hpp>
#include <xyz/openbmc_project/Common/error.hpp>
#include <xyz/openbmc_project/User/Common/error.hpp>
#include <phosphor-logging/log.hpp>
#include <phosphor-logging/elog.hpp>
#include <phosphor-logging/elog-errors.hpp>
#include "shadowlock.hpp"
#include "file.hpp"
#include "user_mgr.hpp"
#include "users.hpp"
#include "config.h"
namespace phosphor
{
namespace user
{
static constexpr const char *passwdFileName = "/etc/passwd";
static constexpr size_t ipmiMaxUsers = 15;
static constexpr size_t ipmiMaxUserNameLen = 16;
static constexpr size_t systemMaxUserNameLen = 30;
static constexpr size_t maxSystemUsers = 30;
static constexpr const char *grpSsh = "ssh";
using namespace phosphor::logging;
using InsufficientPermission =
sdbusplus::xyz::openbmc_project::Common::Error::InsufficientPermission;
using InternalFailure =
sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure;
using InvalidArgument =
sdbusplus::xyz::openbmc_project::Common::Error::InvalidArgument;
using UserNameExists =
sdbusplus::xyz::openbmc_project::User::Common::Error::UserNameExists;
using UserNameDoesNotExist =
sdbusplus::xyz::openbmc_project::User::Common::Error::UserNameDoesNotExist;
using UserNameGroupFail =
sdbusplus::xyz::openbmc_project::User::Common::Error::UserNameGroupFail;
using NoResource =
sdbusplus::xyz::openbmc_project::User::Common::Error::NoResource;
using Argument = xyz::openbmc_project::Common::InvalidArgument;
template <typename... ArgTypes>
static void executeCmd(const char *path, ArgTypes &&... tArgs)
{
boost::process::child execProg(path, const_cast<char *>(tArgs)...);
execProg.wait();
int retCode = execProg.exit_code();
if (retCode)
{
log<level::ERR>("Command execution failed", entry("PATH=%s", path),
entry("RETURN_CODE:%d", retCode));
elog<InternalFailure>();
}
return;
}
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
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
phosphor::user::shadow::Lock lock();
throwForUserDoesNotExist(userName);
try
{
executeCmd("/usr/sbin/userdel", userName.c_str());
}
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
phosphor::user::shadow::Lock lock();
throwForUserDoesNotExist(userName);
throwForUserExists(newUserName);
throwForUserNameConstraints(newUserName,
usersList[userName].get()->userGroups());
try
{
executeCmd("/usr/sbin/usermod", "-l", newUserName.c_str(),
userName.c_str());
}
catch (const InternalFailure &e)
{
log<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
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;
}
void UserMgr::userEnable(const std::string &userName, bool enabled)
{
// All user management lock has to be based on /etc/shadow
phosphor::user::shadow::Lock lock();
throwForUserDoesNotExist(userName);
try
{
executeCmd("/usr/sbin/usermod", userName.c_str(), "-e",
(enabled ? "" : "1970-01-02"));
}
catch (const InternalFailure &e)
{
log<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;
}
UserSSHLists UserMgr::getUserAndSshGrpList()
{
// All user management lock has to be based on /etc/shadow
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;
}
// All users whose UID >= 1000 and < 65534
if ((pwp->pw_uid >= 1000) && (pwp->pw_uid < 65534))
{
std::string userName(pwp->pw_name);
userList.emplace_back(userName);
// ssh doesn't have separate group. Check login shell entry to
// get all users list which are member of ssh group.
std::string loginShell(pwp->pw_shell);
if (loginShell == "/bin/sh")
{
sshUsersList.emplace_back(userName);
}
}
}
endpwent();
return std::make_pair(std::move(userList), std::move(sshUsersList));
}
size_t UserMgr::getIpmiUsersCount()
{
std::vector<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
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;
}
void UserMgr::initUserObjects(void)
{
// All user management lock has to be based on /etc/shadow
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) :
UserMgrIface(bus, path), bus(bus), path(path)
{
UserMgrIface::allPrivileges(privMgr);
std::sort(groupsMgr.begin(), groupsMgr.end());
UserMgrIface::allGroups(groupsMgr);
initUserObjects();
}
} // namespace user
} // namespace phosphor