Basic support for User manager service

Basic support for User Manager service methods
are implemented.

Change-Id: Id42432ec6dd421b99971268add931dcd70876f7c
Signed-off-by: Richard Marian Thomaiyar <richard.marian.thomaiyar@linux.intel.com>
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