1 #pragma once 2 3 #include <boost/container/flat_map.hpp> 4 #include <boost/uuid/uuid.hpp> 5 #include <boost/uuid/uuid_generators.hpp> 6 #include <boost/uuid/uuid_io.hpp> 7 #include <dbus_singleton.hpp> 8 #include <nlohmann/json.hpp> 9 #include <pam_authenticate.hpp> 10 #include <random> 11 #include <sdbusplus/bus/match.hpp> 12 13 #include "crow/logging.h" 14 15 namespace crow 16 { 17 18 namespace persistent_data 19 { 20 21 enum class PersistenceType 22 { 23 TIMEOUT, // User session times out after a predetermined amount of time 24 SINGLE_REQUEST // User times out once this request is completed. 25 }; 26 27 constexpr char const* userService = "xyz.openbmc_project.User.Manager"; 28 constexpr char const* userObjPath = "/xyz/openbmc_project/user"; 29 constexpr char const* userAttrIface = "xyz.openbmc_project.User.Attributes"; 30 constexpr char const* dbusPropertiesIface = "org.freedesktop.DBus.Properties"; 31 32 struct UserRoleMap 33 { 34 using GetManagedPropertyType = 35 boost::container::flat_map<std::string, 36 std::variant<std::string, bool>>; 37 38 using InterfacesPropertiesType = 39 boost::container::flat_map<std::string, GetManagedPropertyType>; 40 41 using GetManagedObjectsType = std::vector< 42 std::pair<sdbusplus::message::object_path, InterfacesPropertiesType>>; 43 44 static UserRoleMap& getInstance() 45 { 46 static UserRoleMap userRoleMap; 47 return userRoleMap; 48 } 49 50 UserRoleMap(const UserRoleMap&) = delete; 51 UserRoleMap& operator=(const UserRoleMap&) = delete; 52 53 std::string getUserRole(std::string_view name) 54 { 55 auto it = roleMap.find(std::string(name)); 56 if (it == roleMap.end()) 57 { 58 BMCWEB_LOG_ERROR << "User name " << name 59 << " is not found in the UserRoleMap."; 60 return ""; 61 } 62 return it->second; 63 } 64 65 std::string 66 extractUserRole(const InterfacesPropertiesType& interfacesProperties) 67 { 68 auto iface = interfacesProperties.find(userAttrIface); 69 if (iface == interfacesProperties.end()) 70 { 71 return {}; 72 } 73 74 auto& properties = iface->second; 75 auto property = properties.find("UserPrivilege"); 76 if (property == properties.end()) 77 { 78 return {}; 79 } 80 81 const std::string* role = std::get_if<std::string>(&property->second); 82 if (role == nullptr) 83 { 84 BMCWEB_LOG_ERROR << "UserPrivilege property value is null"; 85 return {}; 86 } 87 88 return *role; 89 } 90 91 private: 92 void userAdded(sdbusplus::message::message& m) 93 { 94 sdbusplus::message::object_path objPath; 95 InterfacesPropertiesType interfacesProperties; 96 97 try 98 { 99 m.read(objPath, interfacesProperties); 100 } 101 catch (const sdbusplus::exception::SdBusError& e) 102 { 103 BMCWEB_LOG_ERROR << "Failed to parse user add signal." 104 << "ERROR=" << e.what() 105 << "REPLY_SIG=" << m.get_signature(); 106 return; 107 } 108 BMCWEB_LOG_DEBUG << "obj path = " << objPath.str; 109 110 std::size_t lastPos = objPath.str.rfind("/"); 111 if (lastPos == std::string::npos) 112 { 113 return; 114 }; 115 116 std::string name = objPath.str.substr(lastPos + 1); 117 std::string role = this->extractUserRole(interfacesProperties); 118 119 // Insert the newly added user name and the role 120 auto res = roleMap.emplace(name, role); 121 if (res.second == false) 122 { 123 BMCWEB_LOG_ERROR << "Insertion of the user=\"" << name 124 << "\" in the roleMap failed."; 125 return; 126 } 127 } 128 129 void userRemoved(sdbusplus::message::message& m) 130 { 131 sdbusplus::message::object_path objPath; 132 133 try 134 { 135 m.read(objPath); 136 } 137 catch (const sdbusplus::exception::SdBusError& e) 138 { 139 BMCWEB_LOG_ERROR << "Failed to parse user delete signal."; 140 BMCWEB_LOG_ERROR << "ERROR=" << e.what() 141 << "REPLY_SIG=" << m.get_signature(); 142 return; 143 } 144 145 BMCWEB_LOG_DEBUG << "obj path = " << objPath.str; 146 147 std::size_t lastPos = objPath.str.rfind("/"); 148 if (lastPos == std::string::npos) 149 { 150 return; 151 }; 152 153 // User name must be atleast 1 char in length. 154 if ((lastPos + 1) >= objPath.str.length()) 155 { 156 return; 157 } 158 159 std::string name = objPath.str.substr(lastPos + 1); 160 161 roleMap.erase(name); 162 } 163 164 void userPropertiesChanged(sdbusplus::message::message& m) 165 { 166 std::string interface; 167 GetManagedPropertyType changedProperties; 168 m.read(interface, changedProperties); 169 const std::string path = m.get_path(); 170 171 BMCWEB_LOG_DEBUG << "Object Path = \"" << path << "\""; 172 173 std::size_t lastPos = path.rfind("/"); 174 if (lastPos == std::string::npos) 175 { 176 return; 177 }; 178 179 // User name must be at least 1 char in length. 180 if ((lastPos + 1) == path.length()) 181 { 182 return; 183 } 184 185 std::string user = path.substr(lastPos + 1); 186 187 BMCWEB_LOG_DEBUG << "User Name = \"" << user << "\""; 188 189 auto index = changedProperties.find("UserPrivilege"); 190 if (index == changedProperties.end()) 191 { 192 return; 193 } 194 195 const std::string* role = std::get_if<std::string>(&index->second); 196 if (role == nullptr) 197 { 198 return; 199 } 200 BMCWEB_LOG_DEBUG << "Role = \"" << *role << "\""; 201 202 auto it = roleMap.find(user); 203 if (it == roleMap.end()) 204 { 205 BMCWEB_LOG_ERROR << "User Name = \"" << user 206 << "\" is not found. But, received " 207 "propertiesChanged signal"; 208 return; 209 } 210 it->second = *role; 211 } 212 213 UserRoleMap() : 214 userAddedSignal( 215 *crow::connections::systemBus, 216 sdbusplus::bus::match::rules::interfacesAdded(userObjPath), 217 [this](sdbusplus::message::message& m) { 218 BMCWEB_LOG_DEBUG << "User Added"; 219 this->userAdded(m); 220 }), 221 userRemovedSignal( 222 *crow::connections::systemBus, 223 sdbusplus::bus::match::rules::interfacesRemoved(userObjPath), 224 [this](sdbusplus::message::message& m) { 225 BMCWEB_LOG_DEBUG << "User Removed"; 226 this->userRemoved(m); 227 }), 228 userPropertiesChangedSignal( 229 *crow::connections::systemBus, 230 sdbusplus::bus::match::rules::path_namespace(userObjPath) + 231 sdbusplus::bus::match::rules::type::signal() + 232 sdbusplus::bus::match::rules::member("PropertiesChanged") + 233 sdbusplus::bus::match::rules::interface(dbusPropertiesIface) + 234 sdbusplus::bus::match::rules::argN(0, userAttrIface), 235 [this](sdbusplus::message::message& m) { 236 BMCWEB_LOG_DEBUG << "Properties Changed"; 237 this->userPropertiesChanged(m); 238 }) 239 { 240 crow::connections::systemBus->async_method_call( 241 [this](boost::system::error_code ec, 242 GetManagedObjectsType& managedObjects) { 243 if (ec) 244 { 245 BMCWEB_LOG_DEBUG << "User manager call failed, ignoring"; 246 return; 247 } 248 249 for (auto& managedObj : managedObjects) 250 { 251 std::size_t lastPos = managedObj.first.str.rfind("/"); 252 if (lastPos == std::string::npos) 253 { 254 continue; 255 }; 256 std::string name = managedObj.first.str.substr(lastPos + 1); 257 std::string role = extractUserRole(managedObj.second); 258 roleMap.emplace(name, role); 259 } 260 }, 261 userService, userObjPath, "org.freedesktop.DBus.ObjectManager", 262 "GetManagedObjects"); 263 } 264 265 boost::container::flat_map<std::string, std::string> roleMap; 266 sdbusplus::bus::match_t userAddedSignal; 267 sdbusplus::bus::match_t userRemovedSignal; 268 sdbusplus::bus::match_t userPropertiesChangedSignal; 269 }; 270 271 struct UserSession 272 { 273 std::string uniqueId; 274 std::string sessionToken; 275 std::string username; 276 std::string csrfToken; 277 std::chrono::time_point<std::chrono::steady_clock> lastUpdated; 278 PersistenceType persistence; 279 280 /** 281 * @brief Fills object with data from UserSession's JSON representation 282 * 283 * This replaces nlohmann's from_json to ensure no-throw approach 284 * 285 * @param[in] j JSON object from which data should be loaded 286 * 287 * @return a shared pointer if data has been loaded properly, nullptr 288 * otherwise 289 */ 290 static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j) 291 { 292 std::shared_ptr<UserSession> userSession = 293 std::make_shared<UserSession>(); 294 for (const auto& element : j.items()) 295 { 296 const std::string* thisValue = 297 element.value().get_ptr<const std::string*>(); 298 if (thisValue == nullptr) 299 { 300 BMCWEB_LOG_ERROR << "Error reading persistent store. Property " 301 << element.key() << " was not of type string"; 302 return nullptr; 303 } 304 if (element.key() == "unique_id") 305 { 306 userSession->uniqueId = *thisValue; 307 } 308 else if (element.key() == "session_token") 309 { 310 userSession->sessionToken = *thisValue; 311 } 312 else if (element.key() == "csrf_token") 313 { 314 userSession->csrfToken = *thisValue; 315 } 316 else if (element.key() == "username") 317 { 318 userSession->username = *thisValue; 319 } 320 else 321 { 322 BMCWEB_LOG_ERROR 323 << "Got unexpected property reading persistent file: " 324 << element.key(); 325 return nullptr; 326 } 327 } 328 329 // For now, sessions that were persisted through a reboot get their idle 330 // timer reset. This could probably be overcome with a better 331 // understanding of wall clock time and steady timer time, possibly 332 // persisting values with wall clock time instead of steady timer, but 333 // the tradeoffs of all the corner cases involved are non-trivial, so 334 // this is done temporarily 335 userSession->lastUpdated = std::chrono::steady_clock::now(); 336 userSession->persistence = PersistenceType::TIMEOUT; 337 338 return userSession; 339 } 340 }; 341 342 class Middleware; 343 344 class SessionStore 345 { 346 public: 347 std::shared_ptr<UserSession> generateUserSession( 348 const std::string_view username, 349 PersistenceType persistence = PersistenceType::TIMEOUT) 350 { 351 // TODO(ed) find a secure way to not generate session identifiers if 352 // persistence is set to SINGLE_REQUEST 353 static constexpr std::array<char, 62> alphanum = { 354 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 355 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 356 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 357 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 358 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; 359 360 // entropy: 30 characters, 62 possibilities. log2(62^30) = 178 bits of 361 // entropy. OWASP recommends at least 60 362 // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy 363 std::string sessionToken; 364 sessionToken.resize(20, '0'); 365 std::uniform_int_distribution<size_t> dist(0, alphanum.size() - 1); 366 for (size_t i = 0; i < sessionToken.size(); ++i) 367 { 368 sessionToken[i] = alphanum[dist(rd)]; 369 } 370 // Only need csrf tokens for cookie based auth, token doesn't matter 371 std::string csrfToken; 372 csrfToken.resize(20, '0'); 373 for (size_t i = 0; i < csrfToken.size(); ++i) 374 { 375 csrfToken[i] = alphanum[dist(rd)]; 376 } 377 378 std::string uniqueId; 379 uniqueId.resize(10, '0'); 380 for (size_t i = 0; i < uniqueId.size(); ++i) 381 { 382 uniqueId[i] = alphanum[dist(rd)]; 383 } 384 385 auto session = std::make_shared<UserSession>(UserSession{ 386 uniqueId, sessionToken, std::string(username), csrfToken, 387 std::chrono::steady_clock::now(), persistence}); 388 auto it = authTokens.emplace(std::make_pair(sessionToken, session)); 389 // Only need to write to disk if session isn't about to be destroyed. 390 needWrite = persistence == PersistenceType::TIMEOUT; 391 return it.first->second; 392 } 393 394 std::shared_ptr<UserSession> 395 loginSessionByToken(const std::string_view token) 396 { 397 applySessionTimeouts(); 398 auto sessionIt = authTokens.find(std::string(token)); 399 if (sessionIt == authTokens.end()) 400 { 401 return nullptr; 402 } 403 std::shared_ptr<UserSession> userSession = sessionIt->second; 404 userSession->lastUpdated = std::chrono::steady_clock::now(); 405 return userSession; 406 } 407 408 std::shared_ptr<UserSession> getSessionByUid(const std::string_view uid) 409 { 410 applySessionTimeouts(); 411 // TODO(Ed) this is inefficient 412 auto sessionIt = authTokens.begin(); 413 while (sessionIt != authTokens.end()) 414 { 415 if (sessionIt->second->uniqueId == uid) 416 { 417 return sessionIt->second; 418 } 419 sessionIt++; 420 } 421 return nullptr; 422 } 423 424 void removeSession(std::shared_ptr<UserSession> session) 425 { 426 authTokens.erase(session->sessionToken); 427 needWrite = true; 428 } 429 430 std::vector<const std::string*> getUniqueIds( 431 bool getAll = true, 432 const PersistenceType& type = PersistenceType::SINGLE_REQUEST) 433 { 434 applySessionTimeouts(); 435 436 std::vector<const std::string*> ret; 437 ret.reserve(authTokens.size()); 438 for (auto& session : authTokens) 439 { 440 if (getAll || type == session.second->persistence) 441 { 442 ret.push_back(&session.second->uniqueId); 443 } 444 } 445 return ret; 446 } 447 448 bool needsWrite() 449 { 450 return needWrite; 451 } 452 int64_t getTimeoutInSeconds() const 453 { 454 return std::chrono::seconds(timeoutInMinutes).count(); 455 }; 456 457 // Persistent data middleware needs to be able to serialize our authTokens 458 // structure, which is private 459 friend Middleware; 460 461 static SessionStore& getInstance() 462 { 463 static SessionStore sessionStore; 464 return sessionStore; 465 } 466 467 SessionStore(const SessionStore&) = delete; 468 SessionStore& operator=(const SessionStore&) = delete; 469 470 private: 471 SessionStore() : timeoutInMinutes(60) 472 { 473 } 474 475 void applySessionTimeouts() 476 { 477 auto timeNow = std::chrono::steady_clock::now(); 478 if (timeNow - lastTimeoutUpdate > std::chrono::minutes(1)) 479 { 480 lastTimeoutUpdate = timeNow; 481 auto authTokensIt = authTokens.begin(); 482 while (authTokensIt != authTokens.end()) 483 { 484 if (timeNow - authTokensIt->second->lastUpdated >= 485 timeoutInMinutes) 486 { 487 authTokensIt = authTokens.erase(authTokensIt); 488 needWrite = true; 489 } 490 else 491 { 492 authTokensIt++; 493 } 494 } 495 } 496 } 497 498 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate; 499 boost::container::flat_map<std::string, std::shared_ptr<UserSession>> 500 authTokens; 501 std::random_device rd; 502 bool needWrite{false}; 503 std::chrono::minutes timeoutInMinutes; 504 }; 505 506 } // namespace persistent_data 507 } // namespace crow 508 509 // to_json(...) definition for objects of UserSession type 510 namespace nlohmann 511 { 512 template <> 513 struct adl_serializer<std::shared_ptr<crow::persistent_data::UserSession>> 514 { 515 static void 516 to_json(nlohmann::json& j, 517 const std::shared_ptr<crow::persistent_data::UserSession>& p) 518 { 519 if (p->persistence != 520 crow::persistent_data::PersistenceType::SINGLE_REQUEST) 521 { 522 j = nlohmann::json{{"unique_id", p->uniqueId}, 523 {"session_token", p->sessionToken}, 524 {"username", p->username}, 525 {"csrf_token", p->csrfToken}}; 526 } 527 } 528 }; 529 } // namespace nlohmann 530