#pragma once #include #include #include #include #include #include #include #include #include #include "crow/logging.h" namespace crow { namespace persistent_data { enum class PersistenceType { TIMEOUT, // User session times out after a predetermined amount of time SINGLE_REQUEST // User times out once this request is completed. }; constexpr char const* userService = "xyz.openbmc_project.User.Manager"; constexpr char const* userObjPath = "/xyz/openbmc_project/user"; constexpr char const* userAttrIface = "xyz.openbmc_project.User.Attributes"; constexpr char const* dbusPropertiesIface = "org.freedesktop.DBus.Properties"; struct UserRoleMap { using GetManagedPropertyType = boost::container::flat_map>; using InterfacesPropertiesType = boost::container::flat_map; using GetManagedObjectsType = std::vector< std::pair>; static UserRoleMap& getInstance() { static UserRoleMap userRoleMap; return userRoleMap; } UserRoleMap(const UserRoleMap&) = delete; UserRoleMap& operator=(const UserRoleMap&) = delete; std::string getUserRole(std::string_view name) { auto it = roleMap.find(std::string(name)); if (it == roleMap.end()) { BMCWEB_LOG_ERROR << "User name " << name << " is not found in the UserRoleMap."; return ""; } return it->second; } std::string extractUserRole(const InterfacesPropertiesType& interfacesProperties) { auto iface = interfacesProperties.find(userAttrIface); if (iface == interfacesProperties.end()) { return {}; } auto& properties = iface->second; auto property = properties.find("UserPrivilege"); if (property == properties.end()) { return {}; } const std::string* role = std::get_if(&property->second); if (role == nullptr) { BMCWEB_LOG_ERROR << "UserPrivilege property value is null"; return {}; } return *role; } private: void userAdded(sdbusplus::message::message& m) { sdbusplus::message::object_path objPath; InterfacesPropertiesType interfacesProperties; try { m.read(objPath, interfacesProperties); } catch (const sdbusplus::exception::SdBusError& e) { BMCWEB_LOG_ERROR << "Failed to parse user add signal." << "ERROR=" << e.what() << "REPLY_SIG=" << m.get_signature(); return; } BMCWEB_LOG_DEBUG << "obj path = " << objPath.str; std::size_t lastPos = objPath.str.rfind("/"); if (lastPos == std::string::npos) { return; }; std::string name = objPath.str.substr(lastPos + 1); std::string role = this->extractUserRole(interfacesProperties); // Insert the newly added user name and the role auto res = roleMap.emplace(name, role); if (res.second == false) { BMCWEB_LOG_ERROR << "Insertion of the user=\"" << name << "\" in the roleMap failed."; return; } } void userRemoved(sdbusplus::message::message& m) { 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=" << e.what() << "REPLY_SIG=" << m.get_signature(); return; } BMCWEB_LOG_DEBUG << "obj path = " << objPath.str; std::size_t lastPos = objPath.str.rfind("/"); if (lastPos == std::string::npos) { return; }; // User name must be atleast 1 char in length. if ((lastPos + 1) >= objPath.str.length()) { return; } std::string name = objPath.str.substr(lastPos + 1); roleMap.erase(name); } void userPropertiesChanged(sdbusplus::message::message& m) { std::string interface; GetManagedPropertyType changedProperties; m.read(interface, changedProperties); const std::string path = m.get_path(); BMCWEB_LOG_DEBUG << "Object Path = \"" << path << "\""; std::size_t lastPos = path.rfind("/"); if (lastPos == std::string::npos) { return; }; // User name must be at least 1 char in length. if ((lastPos + 1) == path.length()) { return; } std::string user = path.substr(lastPos + 1); BMCWEB_LOG_DEBUG << "User Name = \"" << user << "\""; auto index = changedProperties.find("UserPrivilege"); if (index == changedProperties.end()) { return; } const std::string* role = std::get_if(&index->second); if (role == nullptr) { return; } BMCWEB_LOG_DEBUG << "Role = \"" << *role << "\""; auto it = roleMap.find(user); if (it == roleMap.end()) { BMCWEB_LOG_ERROR << "User Name = \"" << user << "\" is not found. But, received " "propertiesChanged signal"; return; } it->second = *role; } UserRoleMap() : userAddedSignal( *crow::connections::systemBus, sdbusplus::bus::match::rules::interfacesAdded(userObjPath), [this](sdbusplus::message::message& m) { BMCWEB_LOG_DEBUG << "User Added"; this->userAdded(m); }), userRemovedSignal( *crow::connections::systemBus, sdbusplus::bus::match::rules::interfacesRemoved(userObjPath), [this](sdbusplus::message::message& m) { BMCWEB_LOG_DEBUG << "User Removed"; this->userRemoved(m); }), userPropertiesChangedSignal( *crow::connections::systemBus, sdbusplus::bus::match::rules::path_namespace(userObjPath) + sdbusplus::bus::match::rules::type::signal() + sdbusplus::bus::match::rules::member("PropertiesChanged") + sdbusplus::bus::match::rules::interface(dbusPropertiesIface) + sdbusplus::bus::match::rules::argN(0, userAttrIface), [this](sdbusplus::message::message& m) { BMCWEB_LOG_DEBUG << "Properties Changed"; this->userPropertiesChanged(m); }) { crow::connections::systemBus->async_method_call( [this](boost::system::error_code ec, GetManagedObjectsType& managedObjects) { if (ec) { BMCWEB_LOG_DEBUG << "User manager call failed, ignoring"; return; } for (auto& managedObj : managedObjects) { std::size_t lastPos = managedObj.first.str.rfind("/"); if (lastPos == std::string::npos) { continue; }; std::string name = managedObj.first.str.substr(lastPos + 1); std::string role = extractUserRole(managedObj.second); roleMap.emplace(name, role); } }, userService, userObjPath, "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); } boost::container::flat_map roleMap; sdbusplus::bus::match_t userAddedSignal; sdbusplus::bus::match_t userRemovedSignal; sdbusplus::bus::match_t userPropertiesChangedSignal; }; struct UserSession { std::string uniqueId; std::string sessionToken; std::string username; std::string csrfToken; std::chrono::time_point lastUpdated; PersistenceType persistence; /** * @brief Fills object with data from UserSession's JSON representation * * This replaces nlohmann's from_json to ensure no-throw approach * * @param[in] j JSON object from which data should be loaded * * @return a shared pointer if data has been loaded properly, nullptr * otherwise */ static std::shared_ptr fromJson(const nlohmann::json& j) { std::shared_ptr userSession = std::make_shared(); for (const auto& element : j.items()) { const std::string* thisValue = element.value().get_ptr(); if (thisValue == nullptr) { BMCWEB_LOG_ERROR << "Error reading persistent store. Property " << element.key() << " was not of type string"; return nullptr; } if (element.key() == "unique_id") { userSession->uniqueId = *thisValue; } else if (element.key() == "session_token") { userSession->sessionToken = *thisValue; } else if (element.key() == "csrf_token") { userSession->csrfToken = *thisValue; } else if (element.key() == "username") { userSession->username = *thisValue; } else { BMCWEB_LOG_ERROR << "Got unexpected property reading persistent file: " << element.key(); return nullptr; } } // For now, sessions that were persisted through a reboot get their idle // timer reset. This could probably be overcome with a better // understanding of wall clock time and steady timer time, possibly // persisting values with wall clock time instead of steady timer, but // the tradeoffs of all the corner cases involved are non-trivial, so // this is done temporarily userSession->lastUpdated = std::chrono::steady_clock::now(); userSession->persistence = PersistenceType::TIMEOUT; return userSession; } }; class Middleware; class SessionStore { public: std::shared_ptr generateUserSession( const std::string_view username, PersistenceType persistence = PersistenceType::TIMEOUT) { // TODO(ed) find a secure way to not generate session identifiers if // persistence is set to SINGLE_REQUEST static constexpr std::array alphanum = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; // entropy: 30 characters, 62 possibilities. log2(62^30) = 178 bits of // entropy. OWASP recommends at least 60 // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy std::string sessionToken; sessionToken.resize(20, '0'); std::uniform_int_distribution dist(0, alphanum.size() - 1); for (size_t i = 0; i < sessionToken.size(); ++i) { sessionToken[i] = alphanum[dist(rd)]; } // Only need csrf tokens for cookie based auth, token doesn't matter std::string csrfToken; csrfToken.resize(20, '0'); for (size_t i = 0; i < csrfToken.size(); ++i) { csrfToken[i] = alphanum[dist(rd)]; } std::string uniqueId; uniqueId.resize(10, '0'); for (size_t i = 0; i < uniqueId.size(); ++i) { uniqueId[i] = alphanum[dist(rd)]; } auto session = std::make_shared(UserSession{ uniqueId, sessionToken, std::string(username), csrfToken, std::chrono::steady_clock::now(), persistence}); auto it = authTokens.emplace(std::make_pair(sessionToken, session)); // Only need to write to disk if session isn't about to be destroyed. needWrite = persistence == PersistenceType::TIMEOUT; return it.first->second; } std::shared_ptr loginSessionByToken(const std::string_view token) { applySessionTimeouts(); auto sessionIt = authTokens.find(std::string(token)); if (sessionIt == authTokens.end()) { return nullptr; } std::shared_ptr userSession = sessionIt->second; userSession->lastUpdated = std::chrono::steady_clock::now(); return userSession; } std::shared_ptr getSessionByUid(const std::string_view uid) { applySessionTimeouts(); // TODO(Ed) this is inefficient auto sessionIt = authTokens.begin(); while (sessionIt != authTokens.end()) { if (sessionIt->second->uniqueId == uid) { return sessionIt->second; } sessionIt++; } return nullptr; } void removeSession(std::shared_ptr session) { authTokens.erase(session->sessionToken); needWrite = true; } std::vector getUniqueIds( bool getAll = true, const PersistenceType& type = PersistenceType::SINGLE_REQUEST) { applySessionTimeouts(); std::vector ret; ret.reserve(authTokens.size()); for (auto& session : authTokens) { if (getAll || type == session.second->persistence) { ret.push_back(&session.second->uniqueId); } } return ret; } bool needsWrite() { return needWrite; } int64_t getTimeoutInSeconds() const { return std::chrono::seconds(timeoutInMinutes).count(); }; // Persistent data middleware needs to be able to serialize our authTokens // structure, which is private friend Middleware; static SessionStore& getInstance() { static SessionStore sessionStore; return sessionStore; } SessionStore(const SessionStore&) = delete; SessionStore& operator=(const SessionStore&) = delete; private: SessionStore() : timeoutInMinutes(60) { } void applySessionTimeouts() { auto timeNow = std::chrono::steady_clock::now(); if (timeNow - lastTimeoutUpdate > std::chrono::minutes(1)) { lastTimeoutUpdate = timeNow; auto authTokensIt = authTokens.begin(); while (authTokensIt != authTokens.end()) { if (timeNow - authTokensIt->second->lastUpdated >= timeoutInMinutes) { authTokensIt = authTokens.erase(authTokensIt); needWrite = true; } else { authTokensIt++; } } } } std::chrono::time_point lastTimeoutUpdate; boost::container::flat_map> authTokens; std::random_device rd; bool needWrite{false}; std::chrono::minutes timeoutInMinutes; }; } // namespace persistent_data } // namespace crow // to_json(...) definition for objects of UserSession type namespace nlohmann { template <> struct adl_serializer> { static void to_json(nlohmann::json& j, const std::shared_ptr& p) { if (p->persistence != crow::persistent_data::PersistenceType::SINGLE_REQUEST) { j = nlohmann::json{{"unique_id", p->uniqueId}, {"session_token", p->sessionToken}, {"username", p->username}, {"csrf_token", p->csrfToken}}; } } }; } // namespace nlohmann