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