1 #pragma once 2 3 #include "logging.hpp" 4 #include "random.hpp" 5 #include "utility.hpp" 6 #include "utils/ip_utils.hpp" 7 8 #include <nlohmann/json.hpp> 9 10 #include <algorithm> 11 #include <csignal> 12 #include <optional> 13 #include <random> 14 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE 15 #include "ibm/locks.hpp" 16 #endif 17 18 namespace persistent_data 19 { 20 21 // entropy: 20 characters, 62 possibilities. log2(62^20) = 119 bits of 22 // entropy. OWASP recommends at least 64 23 // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy 24 constexpr std::size_t sessionTokenSize = 20; 25 26 enum class PersistenceType 27 { 28 TIMEOUT, // User session times out after a predetermined amount of time 29 SINGLE_REQUEST // User times out once this request is completed. 30 }; 31 32 struct UserSession 33 { 34 std::string uniqueId; 35 std::string sessionToken; 36 std::string username; 37 std::string csrfToken; 38 std::optional<std::string> clientId; 39 std::string clientIp; 40 std::chrono::time_point<std::chrono::steady_clock> lastUpdated; 41 PersistenceType persistence{PersistenceType::TIMEOUT}; 42 bool cookieAuth = false; 43 bool isConfigureSelfOnly = false; 44 std::string userRole{}; 45 std::vector<std::string> userGroups{}; 46 47 // There are two sources of truth for isConfigureSelfOnly: 48 // 1. When pamAuthenticateUser() returns PAM_NEW_AUTHTOK_REQD. 49 // 2. D-Bus User.Manager.GetUserInfo property UserPasswordExpired. 50 // These should be in sync, but the underlying condition can change at any 51 // time. For example, a password can expire or be changed outside of 52 // bmcweb. The value stored here is updated at the start of each 53 // operation and used as the truth within bmcweb. 54 55 /** 56 * @brief Fills object with data from UserSession's JSON representation 57 * 58 * This replaces nlohmann's from_json to ensure no-throw approach 59 * 60 * @param[in] j JSON object from which data should be loaded 61 * 62 * @return a shared pointer if data has been loaded properly, nullptr 63 * otherwise 64 */ 65 static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j) 66 { 67 std::shared_ptr<UserSession> userSession = 68 std::make_shared<UserSession>(); 69 for (const auto& element : j.items()) 70 { 71 const std::string* thisValue = 72 element.value().get_ptr<const std::string*>(); 73 if (thisValue == nullptr) 74 { 75 BMCWEB_LOG_ERROR << "Error reading persistent store. Property " 76 << element.key() << " was not of type string"; 77 continue; 78 } 79 if (element.key() == "unique_id") 80 { 81 userSession->uniqueId = *thisValue; 82 } 83 else if (element.key() == "session_token") 84 { 85 userSession->sessionToken = *thisValue; 86 } 87 else if (element.key() == "csrf_token") 88 { 89 userSession->csrfToken = *thisValue; 90 } 91 else if (element.key() == "username") 92 { 93 userSession->username = *thisValue; 94 } 95 else if (element.key() == "client_id") 96 { 97 userSession->clientId = *thisValue; 98 } 99 else if (element.key() == "client_ip") 100 { 101 userSession->clientIp = *thisValue; 102 } 103 104 else 105 { 106 BMCWEB_LOG_ERROR 107 << "Got unexpected property reading persistent file: " 108 << element.key(); 109 continue; 110 } 111 } 112 // If any of these fields are missing, we can't restore the session, as 113 // we don't have enough information. These 4 fields have been present 114 // in every version of this file in bmcwebs history, so any file, even 115 // on upgrade, should have these present 116 if (userSession->uniqueId.empty() || userSession->username.empty() || 117 userSession->sessionToken.empty() || userSession->csrfToken.empty()) 118 { 119 BMCWEB_LOG_DEBUG << "Session missing required security " 120 "information, refusing to restore"; 121 return nullptr; 122 } 123 124 // For now, sessions that were persisted through a reboot get their idle 125 // timer reset. This could probably be overcome with a better 126 // understanding of wall clock time and steady timer time, possibly 127 // persisting values with wall clock time instead of steady timer, but 128 // the tradeoffs of all the corner cases involved are non-trivial, so 129 // this is done temporarily 130 userSession->lastUpdated = std::chrono::steady_clock::now(); 131 userSession->persistence = PersistenceType::TIMEOUT; 132 133 return userSession; 134 } 135 }; 136 137 struct AuthConfigMethods 138 { 139 #ifdef BMCWEB_ENABLE_BASIC_AUTHENTICATION 140 bool basic = true; 141 #else 142 bool basic = false; 143 #endif 144 145 #ifdef BMCWEB_ENABLE_SESSION_AUTHENTICATION 146 bool sessionToken = true; 147 #else 148 bool sessionToken = false; 149 #endif 150 151 #ifdef BMCWEB_ENABLE_XTOKEN_AUTHENTICATION 152 bool xtoken = true; 153 #else 154 bool xtoken = false; 155 #endif 156 157 #ifdef BMCWEB_ENABLE_COOKIE_AUTHENTICATION 158 bool cookie = true; 159 #else 160 bool cookie = false; 161 #endif 162 163 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION 164 bool tls = true; 165 #else 166 bool tls = false; 167 #endif 168 169 void fromJson(const nlohmann::json& j) 170 { 171 for (const auto& element : j.items()) 172 { 173 const bool* value = element.value().get_ptr<const bool*>(); 174 if (value == nullptr) 175 { 176 continue; 177 } 178 179 if (element.key() == "XToken") 180 { 181 xtoken = *value; 182 } 183 else if (element.key() == "Cookie") 184 { 185 cookie = *value; 186 } 187 else if (element.key() == "SessionToken") 188 { 189 sessionToken = *value; 190 } 191 else if (element.key() == "BasicAuth") 192 { 193 basic = *value; 194 } 195 else if (element.key() == "TLS") 196 { 197 tls = *value; 198 } 199 } 200 } 201 }; 202 203 class SessionStore 204 { 205 public: 206 std::shared_ptr<UserSession> generateUserSession( 207 std::string_view username, const boost::asio::ip::address& clientIp, 208 const std::optional<std::string>& clientId, 209 PersistenceType persistence = PersistenceType::TIMEOUT, 210 bool isConfigureSelfOnly = false) 211 { 212 // TODO(ed) find a secure way to not generate session identifiers if 213 // persistence is set to SINGLE_REQUEST 214 static constexpr std::array<char, 62> alphanum = { 215 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 216 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 217 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 218 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 219 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; 220 221 std::string sessionToken; 222 sessionToken.resize(sessionTokenSize, '0'); 223 std::uniform_int_distribution<size_t> dist(0, alphanum.size() - 1); 224 225 bmcweb::OpenSSLGenerator gen; 226 227 for (char& sessionChar : sessionToken) 228 { 229 sessionChar = alphanum[dist(gen)]; 230 if (gen.error()) 231 { 232 return nullptr; 233 } 234 } 235 // Only need csrf tokens for cookie based auth, token doesn't matter 236 std::string csrfToken; 237 csrfToken.resize(sessionTokenSize, '0'); 238 for (char& csrfChar : csrfToken) 239 { 240 csrfChar = alphanum[dist(gen)]; 241 if (gen.error()) 242 { 243 return nullptr; 244 } 245 } 246 247 std::string uniqueId; 248 uniqueId.resize(10, '0'); 249 for (char& uidChar : uniqueId) 250 { 251 uidChar = alphanum[dist(gen)]; 252 if (gen.error()) 253 { 254 return nullptr; 255 } 256 } 257 258 auto session = std::make_shared<UserSession>(UserSession{ 259 uniqueId, sessionToken, std::string(username), csrfToken, clientId, 260 redfish::ip_util::toString(clientIp), 261 std::chrono::steady_clock::now(), persistence, false, 262 isConfigureSelfOnly}); 263 auto it = authTokens.emplace(sessionToken, session); 264 // Only need to write to disk if session isn't about to be destroyed. 265 needWrite = persistence == PersistenceType::TIMEOUT; 266 return it.first->second; 267 } 268 269 std::shared_ptr<UserSession> loginSessionByToken(std::string_view token) 270 { 271 applySessionTimeouts(); 272 if (token.size() != sessionTokenSize) 273 { 274 return nullptr; 275 } 276 auto sessionIt = authTokens.find(std::string(token)); 277 if (sessionIt == authTokens.end()) 278 { 279 return nullptr; 280 } 281 std::shared_ptr<UserSession> userSession = sessionIt->second; 282 userSession->lastUpdated = std::chrono::steady_clock::now(); 283 return userSession; 284 } 285 286 std::shared_ptr<UserSession> getSessionByUid(std::string_view uid) 287 { 288 applySessionTimeouts(); 289 // TODO(Ed) this is inefficient 290 auto sessionIt = authTokens.begin(); 291 while (sessionIt != authTokens.end()) 292 { 293 if (sessionIt->second->uniqueId == uid) 294 { 295 return sessionIt->second; 296 } 297 sessionIt++; 298 } 299 return nullptr; 300 } 301 302 void removeSession(const std::shared_ptr<UserSession>& session) 303 { 304 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE 305 crow::ibm_mc_lock::Lock::getInstance().releaseLock(session->uniqueId); 306 #endif 307 authTokens.erase(session->sessionToken); 308 needWrite = true; 309 } 310 311 std::vector<const std::string*> getUniqueIds( 312 bool getAll = true, 313 const PersistenceType& type = PersistenceType::SINGLE_REQUEST) 314 { 315 applySessionTimeouts(); 316 317 std::vector<const std::string*> ret; 318 ret.reserve(authTokens.size()); 319 for (auto& session : authTokens) 320 { 321 if (getAll || type == session.second->persistence) 322 { 323 ret.push_back(&session.second->uniqueId); 324 } 325 } 326 return ret; 327 } 328 329 void removeSessionsByUsername(std::string_view username) 330 { 331 std::erase_if(authTokens, [username](const auto& value) { 332 if (value.second == nullptr) 333 { 334 return false; 335 } 336 return value.second->username == username; 337 }); 338 } 339 340 void updateAuthMethodsConfig(const AuthConfigMethods& config) 341 { 342 bool isTLSchanged = (authMethodsConfig.tls != config.tls); 343 authMethodsConfig = config; 344 needWrite = true; 345 if (isTLSchanged) 346 { 347 // recreate socket connections with new settings 348 std::raise(SIGHUP); 349 } 350 } 351 352 AuthConfigMethods& getAuthMethodsConfig() 353 { 354 return authMethodsConfig; 355 } 356 357 bool needsWrite() const 358 { 359 return needWrite; 360 } 361 int64_t getTimeoutInSeconds() const 362 { 363 return std::chrono::seconds(timeoutInSeconds).count(); 364 } 365 366 void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds) 367 { 368 timeoutInSeconds = newTimeoutInSeconds; 369 needWrite = true; 370 } 371 372 static SessionStore& getInstance() 373 { 374 static SessionStore sessionStore; 375 return sessionStore; 376 } 377 378 void applySessionTimeouts() 379 { 380 auto timeNow = std::chrono::steady_clock::now(); 381 if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1)) 382 { 383 lastTimeoutUpdate = timeNow; 384 auto authTokensIt = authTokens.begin(); 385 while (authTokensIt != authTokens.end()) 386 { 387 if (timeNow - authTokensIt->second->lastUpdated >= 388 timeoutInSeconds) 389 { 390 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE 391 crow::ibm_mc_lock::Lock::getInstance().releaseLock( 392 authTokensIt->second->uniqueId); 393 #endif 394 authTokensIt = authTokens.erase(authTokensIt); 395 396 needWrite = true; 397 } 398 else 399 { 400 authTokensIt++; 401 } 402 } 403 } 404 } 405 406 SessionStore(const SessionStore&) = delete; 407 SessionStore& operator=(const SessionStore&) = delete; 408 SessionStore(SessionStore&&) = delete; 409 SessionStore& operator=(const SessionStore&&) = delete; 410 ~SessionStore() = default; 411 412 std::unordered_map<std::string, std::shared_ptr<UserSession>, 413 std::hash<std::string>, 414 crow::utility::ConstantTimeCompare> 415 authTokens; 416 417 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate; 418 bool needWrite{false}; 419 std::chrono::seconds timeoutInSeconds; 420 AuthConfigMethods authMethodsConfig; 421 422 private: 423 SessionStore() : timeoutInSeconds(1800) {} 424 }; 425 426 } // namespace persistent_data 427