1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include "logging.hpp" 6 #include "ossl_random.hpp" 7 #include "utility.hpp" 8 #include "utils/ip_utils.hpp" 9 10 #include <nlohmann/json.hpp> 11 12 #include <algorithm> 13 #include <csignal> 14 #include <memory> 15 #include <optional> 16 #include <random> 17 #include <string> 18 #include <vector> 19 20 namespace persistent_data 21 { 22 23 // entropy: 20 characters, 62 possibilities. log2(62^20) = 119 bits of 24 // entropy. OWASP recommends at least 64 25 // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy 26 constexpr std::size_t sessionTokenSize = 20; 27 28 enum class SessionType 29 { 30 None, 31 Basic, 32 Session, 33 Cookie, 34 MutualTLS 35 }; 36 37 struct UserSession 38 { 39 std::string uniqueId; 40 std::string sessionToken; 41 std::string username; 42 std::string csrfToken; 43 std::optional<std::string> clientId; 44 std::string clientIp; 45 std::chrono::time_point<std::chrono::steady_clock> lastUpdated; 46 SessionType sessionType{SessionType::None}; 47 bool cookieAuth = false; 48 bool isConfigureSelfOnly = false; 49 std::string userRole; 50 std::vector<std::string> userGroups; 51 52 // There are two sources of truth for isConfigureSelfOnly: 53 // 1. When pamAuthenticateUser() returns PAM_NEW_AUTHTOK_REQD. 54 // 2. D-Bus User.Manager.GetUserInfo property UserPasswordExpired. 55 // These should be in sync, but the underlying condition can change at any 56 // time. For example, a password can expire or be changed outside of 57 // bmcweb. The value stored here is updated at the start of each 58 // operation and used as the truth within bmcweb. 59 60 /** 61 * @brief Fills object with data from UserSession's JSON representation 62 * 63 * This replaces nlohmann's from_json to ensure no-throw approach 64 * 65 * @param[in] j JSON object from which data should be loaded 66 * 67 * @return a shared pointer if data has been loaded properly, nullptr 68 * otherwise 69 */ 70 static std::shared_ptr<UserSession> 71 fromJson(const nlohmann::json::object_t& j) 72 { 73 std::shared_ptr<UserSession> userSession = 74 std::make_shared<UserSession>(); 75 for (const auto& element : j) 76 { 77 const std::string* thisValue = 78 element.second.get_ptr<const std::string*>(); 79 if (thisValue == nullptr) 80 { 81 BMCWEB_LOG_ERROR( 82 "Error reading persistent store. Property {} was not of type string", 83 element.first); 84 continue; 85 } 86 if (element.first == "unique_id") 87 { 88 userSession->uniqueId = *thisValue; 89 } 90 else if (element.first == "session_token") 91 { 92 userSession->sessionToken = *thisValue; 93 } 94 else if (element.first == "csrf_token") 95 { 96 userSession->csrfToken = *thisValue; 97 } 98 else if (element.first == "username") 99 { 100 userSession->username = *thisValue; 101 } 102 else if (element.first == "client_id") 103 { 104 userSession->clientId = *thisValue; 105 } 106 else if (element.first == "client_ip") 107 { 108 userSession->clientIp = *thisValue; 109 } 110 111 else 112 { 113 BMCWEB_LOG_ERROR( 114 "Got unexpected property reading persistent file: {}", 115 element.first); 116 continue; 117 } 118 } 119 // If any of these fields are missing, we can't restore the session, as 120 // we don't have enough information. These 4 fields have been present 121 // in every version of this file in bmcwebs history, so any file, even 122 // on upgrade, should have these present 123 if (userSession->uniqueId.empty() || userSession->username.empty() || 124 userSession->sessionToken.empty() || userSession->csrfToken.empty()) 125 { 126 BMCWEB_LOG_DEBUG("Session missing required security " 127 "information, refusing to restore"); 128 return nullptr; 129 } 130 131 // For now, sessions that were persisted through a reboot get their idle 132 // timer reset. This could probably be overcome with a better 133 // understanding of wall clock time and steady timer time, possibly 134 // persisting values with wall clock time instead of steady timer, but 135 // the tradeoffs of all the corner cases involved are non-trivial, so 136 // this is done temporarily 137 userSession->lastUpdated = std::chrono::steady_clock::now(); 138 userSession->sessionType = SessionType::Session; 139 140 return userSession; 141 } 142 }; 143 144 enum class MTLSCommonNameParseMode 145 { 146 Invalid = 0, 147 // This section approximately matches Redfish AccountService 148 // CertificateMappingAttribute, plus bmcweb defined OEM ones. 149 // Note, IDs in this enum must be maintained between versions, as they are 150 // persisted to disk 151 Whole = 1, 152 CommonName = 2, 153 UserPrincipalName = 3, 154 155 // Intentional gap for future DMTF-defined enums 156 157 // OEM parsing modes for various OEMs 158 Meta = 100, 159 }; 160 161 inline MTLSCommonNameParseMode getMTLSCommonNameParseMode(std::string_view name) 162 { 163 if (name == "CommonName") 164 { 165 return MTLSCommonNameParseMode::CommonName; 166 } 167 if (name == "Whole") 168 { 169 // Not yet supported 170 // return MTLSCommonNameParseMode::Whole; 171 } 172 if (name == "UserPrincipalName") 173 { 174 // Not yet supported 175 // return MTLSCommonNameParseMode::UserPrincipalName; 176 } 177 if constexpr (BMCWEB_META_TLS_COMMON_NAME_PARSING) 178 { 179 if (name == "Meta") 180 { 181 return MTLSCommonNameParseMode::Meta; 182 } 183 } 184 return MTLSCommonNameParseMode::Invalid; 185 } 186 187 struct AuthConfigMethods 188 { 189 // Authentication paths 190 bool basic = BMCWEB_BASIC_AUTH; 191 bool sessionToken = BMCWEB_SESSION_AUTH; 192 bool xtoken = BMCWEB_XTOKEN_AUTH; 193 bool cookie = BMCWEB_COOKIE_AUTH; 194 bool tls = BMCWEB_MUTUAL_TLS_AUTH; 195 196 // Whether or not unauthenticated TLS should be accepted 197 // true = reject connections if mutual tls is not provided 198 // false = allow connection, and allow user to use other auth method 199 // Always default to false, because root certificates will not 200 // be provisioned at startup 201 bool tlsStrict = false; 202 203 MTLSCommonNameParseMode mTLSCommonNameParsingMode = 204 getMTLSCommonNameParseMode( 205 BMCWEB_MUTUAL_TLS_COMMON_NAME_PARSING_DEFAULT); 206 207 void fromJson(const nlohmann::json::object_t& j) 208 { 209 for (const auto& element : j) 210 { 211 const bool* value = element.second.get_ptr<const bool*>(); 212 if (value != nullptr) 213 { 214 if (element.first == "XToken") 215 { 216 xtoken = *value; 217 } 218 else if (element.first == "Cookie") 219 { 220 cookie = *value; 221 } 222 else if (element.first == "SessionToken") 223 { 224 sessionToken = *value; 225 } 226 else if (element.first == "BasicAuth") 227 { 228 basic = *value; 229 } 230 else if (element.first == "TLS") 231 { 232 tls = *value; 233 } 234 else if (element.first == "TLSStrict") 235 { 236 tlsStrict = *value; 237 } 238 } 239 const uint64_t* intValue = 240 element.second.get_ptr<const uint64_t*>(); 241 if (intValue != nullptr) 242 { 243 if (element.first == "MTLSCommonNameParseMode") 244 { 245 if (*intValue <= 2 || *intValue == 100) 246 { 247 mTLSCommonNameParsingMode = 248 static_cast<MTLSCommonNameParseMode>(*intValue); 249 } 250 else 251 { 252 BMCWEB_LOG_ERROR( 253 "Json value of {} was out of range of the enum. Ignoring", 254 *intValue); 255 } 256 } 257 } 258 } 259 } 260 }; 261 262 class SessionStore 263 { 264 public: 265 std::shared_ptr<UserSession> generateUserSession( 266 std::string_view username, const boost::asio::ip::address& clientIp, 267 const std::optional<std::string>& clientId, SessionType sessionType, 268 bool isConfigureSelfOnly = false) 269 { 270 // Only need csrf tokens for cookie based auth, token doesn't matter 271 std::string sessionToken = 272 bmcweb::getRandomIdOfLength(sessionTokenSize); 273 std::string csrfToken = bmcweb::getRandomIdOfLength(sessionTokenSize); 274 std::string uniqueId = bmcweb::getRandomIdOfLength(10); 275 276 // 277 if (sessionToken.empty() || csrfToken.empty() || uniqueId.empty()) 278 { 279 BMCWEB_LOG_ERROR("Failed to generate session tokens"); 280 return nullptr; 281 } 282 283 auto session = std::make_shared<UserSession>(UserSession{ 284 uniqueId, 285 sessionToken, 286 std::string(username), 287 csrfToken, 288 clientId, 289 redfish::ip_util::toString(clientIp), 290 std::chrono::steady_clock::now(), 291 sessionType, 292 false, 293 isConfigureSelfOnly, 294 "", 295 {}}); 296 auto it = authTokens.emplace(sessionToken, session); 297 // Only need to write to disk if session isn't about to be destroyed. 298 needWrite = sessionType != SessionType::Basic && 299 sessionType != SessionType::MutualTLS; 300 return it.first->second; 301 } 302 303 std::shared_ptr<UserSession> loginSessionByToken(std::string_view token) 304 { 305 applySessionTimeouts(); 306 if (token.size() != sessionTokenSize) 307 { 308 return nullptr; 309 } 310 auto sessionIt = authTokens.find(std::string(token)); 311 if (sessionIt == authTokens.end()) 312 { 313 return nullptr; 314 } 315 std::shared_ptr<UserSession> userSession = sessionIt->second; 316 userSession->lastUpdated = std::chrono::steady_clock::now(); 317 return userSession; 318 } 319 320 std::shared_ptr<UserSession> getSessionByUid(std::string_view uid) 321 { 322 applySessionTimeouts(); 323 // TODO(Ed) this is inefficient 324 auto sessionIt = authTokens.begin(); 325 while (sessionIt != authTokens.end()) 326 { 327 if (sessionIt->second->uniqueId == uid) 328 { 329 return sessionIt->second; 330 } 331 sessionIt++; 332 } 333 return nullptr; 334 } 335 336 void removeSession(const std::shared_ptr<UserSession>& session) 337 { 338 authTokens.erase(session->sessionToken); 339 needWrite = true; 340 } 341 342 std::vector<std::string> getAllUniqueIds() 343 { 344 applySessionTimeouts(); 345 std::vector<std::string> ret; 346 ret.reserve(authTokens.size()); 347 for (auto& session : authTokens) 348 { 349 ret.push_back(session.second->uniqueId); 350 } 351 return ret; 352 } 353 354 std::vector<std::string> getUniqueIdsBySessionType(SessionType type) 355 { 356 applySessionTimeouts(); 357 358 std::vector<std::string> ret; 359 ret.reserve(authTokens.size()); 360 for (auto& session : authTokens) 361 { 362 if (type == session.second->sessionType) 363 { 364 ret.push_back(session.second->uniqueId); 365 } 366 } 367 return ret; 368 } 369 370 std::vector<std::shared_ptr<UserSession>> getSessions() 371 { 372 std::vector<std::shared_ptr<UserSession>> sessions; 373 sessions.reserve(authTokens.size()); 374 for (auto& session : authTokens) 375 { 376 sessions.push_back(session.second); 377 } 378 return sessions; 379 } 380 381 void removeSessionsByUsername(std::string_view username) 382 { 383 std::erase_if(authTokens, [username](const auto& value) { 384 if (value.second == nullptr) 385 { 386 return false; 387 } 388 return value.second->username == username; 389 }); 390 } 391 392 void removeSessionsByUsernameExceptSession( 393 std::string_view username, const std::shared_ptr<UserSession>& session) 394 { 395 std::erase_if(authTokens, [username, session](const auto& value) { 396 if (value.second == nullptr) 397 { 398 return false; 399 } 400 401 return value.second->username == username && 402 value.second->uniqueId != session->uniqueId; 403 }); 404 } 405 406 void updateAuthMethodsConfig(const AuthConfigMethods& config) 407 { 408 bool isTLSchanged = (authMethodsConfig.tls != config.tls); 409 authMethodsConfig = config; 410 needWrite = true; 411 if (isTLSchanged) 412 { 413 // recreate socket connections with new settings 414 std::raise(SIGHUP); 415 } 416 } 417 418 AuthConfigMethods& getAuthMethodsConfig() 419 { 420 return authMethodsConfig; 421 } 422 423 bool needsWrite() const 424 { 425 return needWrite; 426 } 427 int64_t getTimeoutInSeconds() const 428 { 429 return std::chrono::seconds(timeoutInSeconds).count(); 430 } 431 432 void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds) 433 { 434 timeoutInSeconds = newTimeoutInSeconds; 435 needWrite = true; 436 } 437 438 static SessionStore& getInstance() 439 { 440 static SessionStore sessionStore; 441 return sessionStore; 442 } 443 444 void applySessionTimeouts() 445 { 446 auto timeNow = std::chrono::steady_clock::now(); 447 if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1)) 448 { 449 lastTimeoutUpdate = timeNow; 450 auto authTokensIt = authTokens.begin(); 451 while (authTokensIt != authTokens.end()) 452 { 453 if (timeNow - authTokensIt->second->lastUpdated >= 454 timeoutInSeconds) 455 { 456 authTokensIt = authTokens.erase(authTokensIt); 457 458 needWrite = true; 459 } 460 else 461 { 462 authTokensIt++; 463 } 464 } 465 } 466 } 467 468 SessionStore(const SessionStore&) = delete; 469 SessionStore& operator=(const SessionStore&) = delete; 470 SessionStore(SessionStore&&) = delete; 471 SessionStore& operator=(const SessionStore&&) = delete; 472 ~SessionStore() = default; 473 474 std::unordered_map<std::string, std::shared_ptr<UserSession>, 475 std::hash<std::string>, bmcweb::ConstantTimeCompare> 476 authTokens; 477 478 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate; 479 bool needWrite{false}; 480 std::chrono::seconds timeoutInSeconds; 481 AuthConfigMethods authMethodsConfig; 482 483 private: 484 SessionStore() : timeoutInSeconds(1800) {} 485 }; 486 487 } // namespace persistent_data 488