summaryrefslogtreecommitdiffstats
path: root/user_mgr.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'user_mgr.cpp')
-rw-r--r--user_mgr.cpp598
1 files changed, 598 insertions, 0 deletions
diff --git a/user_mgr.cpp b/user_mgr.cpp
new file mode 100644
index 0000000..3987088
--- /dev/null
+++ b/user_mgr.cpp
@@ -0,0 +1,598 @@
+/*
+// 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 (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 (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
OpenPOWER on IntegriCloud