Cache user role in session object

There is an async call within the router that leads to a small, but
pervasive performance issue for all queries. Removing that call from the
router has the potential to increase the performance of every
authenticated query, and significantly reduce our dbus traffic for
"simple" operations.

This commit re-implements the role cache in session object that existed
previously many years ago.  Each users role is fetched during
authentication and persisted in session object. Each successive request
can then be matched against the privilege which is there in the
in-memory session object.

This was discussed on below commit
https://gerrit.openbmc-project.xyz/c/openbmc/bmcweb/+/39756

Tested by:
```
POST /redfish/v1/SessionService/Sessions {"UserName":"root", "Password": “0penBmc”}
```

Followed by redfish queries

Get /redfish/v1/AccountService

Tested user role persistency

Redfish service validator passes.

Signed-off-by: Ravi Teja <raviteja28031990@gmail.com>
Signed-off-by: Ed Tanous <edtanous@google.com>
Change-Id: I575599c29358e32849446ce6ee7f62c8eb3885f6
diff --git a/http/http_request.hpp b/http/http_request.hpp
index 5ce434b..4762a9b 100644
--- a/http/http_request.hpp
+++ b/http/http_request.hpp
@@ -32,7 +32,6 @@
 
     std::shared_ptr<persistent_data::UserSession> session;
 
-    std::string userRole{};
     Request(boost::beast::http::request<boost::beast::http::string_body> reqIn,
             std::error_code& ec) :
         req(std::move(reqIn))
diff --git a/include/dbus_privileges.hpp b/include/dbus_privileges.hpp
index fca4a13..07a1216 100644
--- a/include/dbus_privileges.hpp
+++ b/include/dbus_privileges.hpp
@@ -6,9 +6,11 @@
 #include "http_response.hpp"
 #include "logging.hpp"
 #include "routing/baserule.hpp"
+#include "user_role_map.hpp"
 #include "utils/dbus_utils.hpp"
 
 #include <boost/url/format.hpp>
+#include <sdbusplus/bus/match.hpp>
 #include <sdbusplus/unpack_properties.hpp>
 
 #include <memory>
@@ -16,82 +18,6 @@
 
 namespace crow
 {
-// Populate session with user information.
-inline bool
-    populateUserInfo(Request& req,
-                     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
-                     const dbus::utility::DBusPropertiesMap& userInfoMap)
-{
-    const std::string* userRolePtr = nullptr;
-    const bool* remoteUser = nullptr;
-    const bool* passwordExpired = nullptr;
-    const std::vector<std::string>* userGroups = nullptr;
-
-    const bool success = sdbusplus::unpackPropertiesNoThrow(
-        redfish::dbus_utils::UnpackErrorPrinter(), userInfoMap, "UserPrivilege",
-        userRolePtr, "RemoteUser", remoteUser, "UserPasswordExpired",
-        passwordExpired, "UserGroups", userGroups);
-
-    if (!success)
-    {
-        BMCWEB_LOG_ERROR("Failed to unpack user properties.");
-        asyncResp->res.result(
-            boost::beast::http::status::internal_server_error);
-        return false;
-    }
-
-    if (req.session == nullptr)
-    {
-        return false;
-    }
-
-    if (userRolePtr != nullptr)
-    {
-        req.session->userRole = *userRolePtr;
-        BMCWEB_LOG_DEBUG("userName = {} userRole = {}", req.session->username,
-                         *userRolePtr);
-    }
-
-    if (remoteUser == nullptr)
-    {
-        BMCWEB_LOG_ERROR("RemoteUser property missing or wrong type");
-        asyncResp->res.result(
-            boost::beast::http::status::internal_server_error);
-        return false;
-    }
-    bool expired = false;
-    if (passwordExpired == nullptr)
-    {
-        if (!*remoteUser)
-        {
-            BMCWEB_LOG_ERROR("UserPasswordExpired property is expected for"
-                             " local user but is missing or wrong type");
-            asyncResp->res.result(
-                boost::beast::http::status::internal_server_error);
-            return false;
-        }
-    }
-    else
-    {
-        expired = *passwordExpired;
-    }
-
-    // Set isConfigureSelfOnly based on D-Bus results.  This
-    // ignores the results from both pamAuthenticateUser and the
-    // value from any previous use of this session.
-    req.session->isConfigureSelfOnly = expired;
-
-    if (userGroups != nullptr)
-    {
-        // Populate session with user groups.
-        for (const auto& userGroup : *userGroups)
-        {
-            req.session->userGroups.emplace_back(userGroup);
-        }
-    }
-
-    return true;
-}
 
 inline bool
     isUserPrivileged(Request& req,
@@ -128,44 +54,10 @@
         return false;
     }
 
-    req.userRole = req.session->userRole;
     return true;
 }
 
 template <typename CallbackFn>
-void afterGetUserInfo(Request& req,
-                      const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
-                      BaseRule& rule, CallbackFn&& callback,
-                      const boost::system::error_code& ec,
-                      const dbus::utility::DBusPropertiesMap& userInfoMap)
-{
-    if (ec)
-    {
-        BMCWEB_LOG_ERROR("GetUserInfo failed...");
-        asyncResp->res.result(
-            boost::beast::http::status::internal_server_error);
-        return;
-    }
-
-    if (!populateUserInfo(req, asyncResp, userInfoMap))
-    {
-        BMCWEB_LOG_ERROR("Failed to populate user information");
-        asyncResp->res.result(
-            boost::beast::http::status::internal_server_error);
-        return;
-    }
-
-    if (!isUserPrivileged(req, asyncResp, rule))
-    {
-        // User is not privileged
-        BMCWEB_LOG_ERROR("Insufficient Privilege");
-        asyncResp->res.result(boost::beast::http::status::forbidden);
-        return;
-    }
-    callback(req);
-}
-
-template <typename CallbackFn>
 void validatePrivilege(Request& req,
                        const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
                        BaseRule& rule, CallbackFn&& callback)
@@ -175,15 +67,29 @@
         return;
     }
     std::string username = req.session->username;
-    crow::connections::systemBus->async_method_call(
-        [&req, asyncResp, &rule, callback(std::forward<CallbackFn>(callback))](
-            const boost::system::error_code& ec,
-            const dbus::utility::DBusPropertiesMap& userInfoMap) mutable {
-        afterGetUserInfo(req, asyncResp, rule,
-                         std::forward<CallbackFn>(callback), ec, userInfoMap);
-        },
-        "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user",
-        "xyz.openbmc_project.User.Manager", "GetUserInfo", username);
+    UserFields props =
+        UserRoleMap::getInstance().getUserRole(req.session->username);
+    if (props.userRole)
+    {
+        req.session->userRole = props.userRole.value_or("");
+    }
+    if (props.passwordExpired)
+    {
+        req.session->isConfigureSelfOnly = *props.passwordExpired;
+    }
+    if (props.userGroups)
+    {
+        req.session->userGroups = std::move(*props.userGroups);
+    }
+
+    if (!isUserPrivileged(req, asyncResp, rule))
+    {
+        // User is not privileged
+        BMCWEB_LOG_WARNING("Insufficient Privilege");
+        asyncResp->res.result(boost::beast::http::status::forbidden);
+        return;
+    }
+    callback(req);
 }
 
 } // namespace crow
diff --git a/include/sessions.hpp b/include/sessions.hpp
index cb7f78e..90a1de9 100644
--- a/include/sessions.hpp
+++ b/include/sessions.hpp
@@ -2,10 +2,12 @@
 
 #include "logging.hpp"
 #include "ossl_random.hpp"
+#include "user_role_map.hpp"
 #include "utility.hpp"
 #include "utils/ip_utils.hpp"
 
 #include <nlohmann/json.hpp>
+#include <sdbusplus/message.hpp>
 
 #include <algorithm>
 #include <csignal>
@@ -256,11 +258,15 @@
             }
         }
 
+        std::string userRole = crow::UserRoleMap::getInstance()
+                                   .getUserRole(username)
+                                   .userRole.value_or("");
+
         auto session = std::make_shared<UserSession>(UserSession{
             uniqueId, sessionToken, std::string(username), csrfToken, clientId,
             redfish::ip_util::toString(clientIp),
             std::chrono::steady_clock::now(), persistence, false,
-            isConfigureSelfOnly});
+            isConfigureSelfOnly, userRole});
         auto it = authTokens.emplace(sessionToken, session);
         // Only need to write to disk if session isn't about to be destroyed.
         needWrite = persistence == PersistenceType::TIMEOUT;
diff --git a/include/user_role_map.hpp b/include/user_role_map.hpp
new file mode 100644
index 0000000..ce2f97e
--- /dev/null
+++ b/include/user_role_map.hpp
@@ -0,0 +1,278 @@
+#pragma once
+
+#include "dbus_utility.hpp"
+#include "logging.hpp"
+#include "utils/dbus_utils.hpp"
+
+#include <boost/container/flat_map.hpp>
+#include <boost/url/format.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/unpack_properties.hpp>
+
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace crow
+{
+struct UserFields
+{
+    std::optional<std::string> userRole;
+    std::optional<bool> remote;
+    std::optional<bool> passwordExpired;
+    std::optional<std::vector<std::string>> userGroups;
+};
+
+struct UserRoleMap
+{
+  public:
+    static UserRoleMap& getInstance()
+    {
+        static UserRoleMap userRoleMap;
+        return userRoleMap;
+    }
+
+    UserFields getUserRole(std::string_view name)
+    {
+        auto it = roleMap.find(name);
+        if (it == roleMap.end())
+        {
+            BMCWEB_LOG_ERROR("User name {} is not found in the UserRoleMap.",
+                             name);
+            return {};
+        }
+        return it->second;
+    }
+    UserRoleMap(const UserRoleMap&) = delete;
+    UserRoleMap& operator=(const UserRoleMap&) = delete;
+    UserRoleMap(UserRoleMap&&) = delete;
+    UserRoleMap& operator=(UserRoleMap&&) = delete;
+    ~UserRoleMap() = default;
+
+  private:
+    static UserFields extractUserRole(
+        const dbus::utility::DBusInteracesMap& interfacesProperties)
+    {
+        UserFields fields;
+        for (const auto& interface : interfacesProperties)
+        {
+            for (const auto& property : interface.second)
+            {
+                if (property.first == "UserPrivilege")
+                {
+                    const std::string* role =
+                        std::get_if<std::string>(&property.second);
+                    if (role != nullptr)
+                    {
+                        fields.userRole = *role;
+                    }
+                }
+                else if (property.first == "UserGroups")
+                {
+                    const std::vector<std::string>* groups =
+                        std::get_if<std::vector<std::string>>(&property.second);
+                    if (groups != nullptr)
+                    {
+                        fields.userGroups = *groups;
+                    }
+                }
+                else if (property.first == "UserPasswordExpired")
+                {
+                    const bool* expired = std::get_if<bool>(&property.second);
+                    if (expired != nullptr)
+                    {
+                        fields.passwordExpired = *expired;
+                    }
+                }
+                else if (property.first == "RemoteUser")
+                {
+                    const bool* remote = std::get_if<bool>(&property.second);
+                    if (remote != nullptr)
+                    {
+                        fields.remote = *remote;
+                    }
+                }
+            }
+        }
+        return fields;
+    }
+
+    void userAdded(sdbusplus::message::message& m)
+    {
+        BMCWEB_LOG_DEBUG("User Added");
+        sdbusplus::message::object_path objPath;
+        dbus::utility::DBusInteracesMap interfacesProperties;
+
+        try
+        {
+            m.read(objPath, interfacesProperties);
+        }
+        catch (const sdbusplus::exception::SdBusError& e)
+        {
+            BMCWEB_LOG_ERROR(
+                "Failed to parse user add signal.ERROR={}REPLY_SIG={}",
+                e.what(), m.get_signature());
+            return;
+        }
+        BMCWEB_LOG_DEBUG("obj path = {}", objPath.str);
+
+        std::string name = objPath.filename();
+        if (name.empty())
+        {
+            return;
+        }
+        UserFields role = extractUserRole(interfacesProperties);
+
+        // Insert the newly added user name and the role
+        auto res = roleMap.emplace(name, role);
+        if (!res.second)
+        {
+            BMCWEB_LOG_ERROR(
+                "Insertion of the user=\"{}\" in the roleMap failed.", name);
+            return;
+        }
+    }
+
+    void userRemoved(sdbusplus::message::message& m)
+    {
+        BMCWEB_LOG_DEBUG("User Removed");
+        sdbusplus::message::object_path objPath;
+
+        try
+        {
+            m.read(objPath);
+        }
+        catch (const sdbusplus::exception::SdBusError& e)
+        {
+            BMCWEB_LOG_ERROR("Failed to parse user delete signal.");
+            BMCWEB_LOG_ERROR("ERROR={}REPLY_SIG={}", e.what(),
+                             m.get_signature());
+            return;
+        }
+
+        BMCWEB_LOG_DEBUG("obj path = {}", objPath.str);
+
+        std::string name = objPath.filename();
+        if (name.empty())
+        {
+            return;
+        }
+
+        roleMap.erase(name);
+    }
+
+    void userPropertiesChanged(sdbusplus::message::message& m)
+    {
+        BMCWEB_LOG_DEBUG("Properties Changed");
+        std::string interface;
+        dbus::utility::DBusPropertiesMap changedProperties;
+        try
+        {
+            m.read(interface, changedProperties);
+        }
+        catch (const sdbusplus::exception::SdBusError& e)
+        {
+            BMCWEB_LOG_ERROR("Failed to parse user properties changed signal.");
+            BMCWEB_LOG_ERROR("ERROR={}REPLY_SIG={}", e.what(),
+                             m.get_signature());
+            return;
+        }
+        dbus::utility::DBusInteracesMap map;
+        map.emplace_back("xyz.openbmc_project.User.Attributes",
+                         changedProperties);
+        const sdbusplus::message::object_path path(m.get_path());
+
+        BMCWEB_LOG_DEBUG("Object Path = \"{}\"", path.str);
+
+        std::string user = path.filename();
+        if (user.empty())
+        {
+            return;
+        }
+
+        BMCWEB_LOG_DEBUG("User Name = \"{}\"", user);
+
+        UserFields role = extractUserRole(map);
+
+        auto userProps = roleMap.find(user);
+        if (userProps == roleMap.end())
+        {
+            BMCWEB_LOG_CRITICAL("User {} not found", user);
+            return;
+        }
+        if (role.userRole)
+        {
+            userProps->second.userRole = role.userRole;
+        }
+        if (role.remote)
+        {
+            userProps->second.remote = role.remote;
+        }
+        if (role.userGroups)
+        {
+            userProps->second.userGroups = role.userGroups;
+        }
+        if (role.passwordExpired)
+        {
+            userProps->second.passwordExpired = role.passwordExpired;
+        }
+    }
+
+    void onGetManagedObjects(
+        const boost::system::error_code& ec,
+        const dbus::utility::ManagedObjectType& managedObjects)
+    {
+        if (ec)
+        {
+            BMCWEB_LOG_DEBUG("User manager call failed, ignoring");
+            return;
+        }
+
+        for (const auto& managedObj : managedObjects)
+        {
+            std::string name =
+                sdbusplus::message::object_path(managedObj.first).filename();
+            if (name.empty())
+            {
+                continue;
+            }
+            UserFields role = extractUserRole(managedObj.second);
+            roleMap.emplace(name, role);
+        }
+    }
+
+    static constexpr const char* userObjPath = "/xyz/openbmc_project/user";
+
+    UserRoleMap() :
+        userAddedSignal(
+            *crow::connections::systemBus,
+            sdbusplus::bus::match::rules::interfacesAdded(userObjPath),
+            std::bind_front(&UserRoleMap::userAdded, this)),
+        userRemovedSignal(
+            *crow::connections::systemBus,
+            sdbusplus::bus::match::rules::interfacesRemoved(userObjPath),
+            std::bind_front(&UserRoleMap::userRemoved, this)),
+        userPropertiesChangedSignal(
+            *crow::connections::systemBus,
+            sdbusplus::bus::match::rules::propertiesChangedNamespace(
+                userObjPath, "xyz.openbmc_project.User.Attributes"),
+            std::bind_front(&UserRoleMap::userPropertiesChanged, this))
+    {
+        dbus::utility::getManagedObjects(
+            "xyz.openbmc_project.User.Manager", {userObjPath},
+            std::bind_front(&UserRoleMap::onGetManagedObjects, this));
+    }
+
+    // Map of username -> role
+    boost::container::flat_map<std::string, UserFields, std::less<>> roleMap;
+
+    // These MUST be last, otherwise destruction can cause race conditions.
+    sdbusplus::bus::match_t userAddedSignal;
+    sdbusplus::bus::match_t userRemovedSignal;
+    sdbusplus::bus::match_t userPropertiesChangedSignal;
+};
+
+} // namespace crow
diff --git a/src/webserver_main.cpp b/src/webserver_main.cpp
index 67e2aae..34fe18f 100644
--- a/src/webserver_main.cpp
+++ b/src/webserver_main.cpp
@@ -18,6 +18,7 @@
 #include "security_headers.hpp"
 #include "ssl_key_handler.hpp"
 #include "user_monitor.hpp"
+#include "user_role_map.hpp"
 #include "vm_websocket.hpp"
 #include "webassets.hpp"
 
@@ -143,6 +144,9 @@
     crow::hostname_monitor::registerHostnameSignal();
 #endif
 
+    // Init the user role map
+    crow::UserRoleMap::getInstance();
+
     bmcweb::registerUserRemovedSignal();
 
     app.run();