xref: /openbmc/bmcweb/include/sessions.hpp (revision 504af5a0568171b72caf13234cc81380b261fa21)
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         // Not yet supported
181         // return MTLSCommonNameParseMode::UserPrincipalName;
182     }
183     if constexpr (BMCWEB_META_TLS_COMMON_NAME_PARSING)
184     {
185         if (name == "Meta")
186         {
187             return MTLSCommonNameParseMode::Meta;
188         }
189     }
190     return MTLSCommonNameParseMode::Invalid;
191 }
192 
193 struct AuthConfigMethods
194 {
195     // Authentication paths
196     bool basic = BMCWEB_BASIC_AUTH;
197     bool sessionToken = BMCWEB_SESSION_AUTH;
198     bool xtoken = BMCWEB_XTOKEN_AUTH;
199     bool cookie = BMCWEB_COOKIE_AUTH;
200     bool tls = BMCWEB_MUTUAL_TLS_AUTH;
201 
202     // Whether or not unauthenticated TLS should be accepted
203     // true = reject connections if mutual tls is not provided
204     // false = allow connection, and allow user to use other auth method
205     // Always default to false, because root certificates will not
206     // be provisioned at startup
207     bool tlsStrict = false;
208 
209     MTLSCommonNameParseMode mTLSCommonNameParsingMode =
210         getMTLSCommonNameParseMode(
211             BMCWEB_MUTUAL_TLS_COMMON_NAME_PARSING_DEFAULT);
212 
fromJsonpersistent_data::AuthConfigMethods213     void fromJson(const nlohmann::json::object_t& j)
214     {
215         for (const auto& element : j)
216         {
217             const bool* value = element.second.get_ptr<const bool*>();
218             if (value != nullptr)
219             {
220                 if (element.first == "XToken")
221                 {
222                     xtoken = *value;
223                 }
224                 else if (element.first == "Cookie")
225                 {
226                     cookie = *value;
227                 }
228                 else if (element.first == "SessionToken")
229                 {
230                     sessionToken = *value;
231                 }
232                 else if (element.first == "BasicAuth")
233                 {
234                     basic = *value;
235                 }
236                 else if (element.first == "TLS")
237                 {
238                     tls = *value;
239                 }
240                 else if (element.first == "TLSStrict")
241                 {
242                     tlsStrict = *value;
243                 }
244             }
245             const uint64_t* intValue =
246                 element.second.get_ptr<const uint64_t*>();
247             if (intValue != nullptr)
248             {
249                 if (element.first == "MTLSCommonNameParseMode")
250                 {
251                     if (*intValue <= 2 || *intValue == 100)
252                     {
253                         mTLSCommonNameParsingMode =
254                             static_cast<MTLSCommonNameParseMode>(*intValue);
255                     }
256                     else
257                     {
258                         BMCWEB_LOG_ERROR(
259                             "Json value of {} was out of range of the enum.  Ignoring",
260                             *intValue);
261                     }
262                 }
263             }
264         }
265     }
266 };
267 
268 class SessionStore
269 {
270   public:
generateUserSession(std::string_view username,const boost::asio::ip::address & clientIp,const std::optional<std::string> & clientId,SessionType sessionType,bool isConfigureSelfOnly=false)271     std::shared_ptr<UserSession> generateUserSession(
272         std::string_view username, const boost::asio::ip::address& clientIp,
273         const std::optional<std::string>& clientId, SessionType sessionType,
274         bool isConfigureSelfOnly = false)
275     {
276         // Only need csrf tokens for cookie based auth, token doesn't matter
277         std::string sessionToken =
278             bmcweb::getRandomIdOfLength(sessionTokenSize);
279         std::string csrfToken = bmcweb::getRandomIdOfLength(sessionTokenSize);
280         std::string uniqueId = bmcweb::getRandomIdOfLength(10);
281 
282         //
283         if (sessionToken.empty() || csrfToken.empty() || uniqueId.empty())
284         {
285             BMCWEB_LOG_ERROR("Failed to generate session tokens");
286             return nullptr;
287         }
288 
289         auto session = std::make_shared<UserSession>(UserSession{
290             uniqueId,
291             sessionToken,
292             std::string(username),
293             csrfToken,
294             clientId,
295             redfish::ip_util::toString(clientIp),
296             std::chrono::steady_clock::now(),
297             sessionType,
298             false,
299             isConfigureSelfOnly,
300             "",
301             {}});
302         auto it = authTokens.emplace(sessionToken, session);
303         // Only need to write to disk if session isn't about to be destroyed.
304         needWrite = sessionType != SessionType::Basic &&
305                     sessionType != SessionType::MutualTLS;
306         return it.first->second;
307     }
308 
loginSessionByToken(std::string_view token)309     std::shared_ptr<UserSession> loginSessionByToken(std::string_view token)
310     {
311         applySessionTimeouts();
312         if (token.size() != sessionTokenSize)
313         {
314             return nullptr;
315         }
316         auto sessionIt = authTokens.find(std::string(token));
317         if (sessionIt == authTokens.end())
318         {
319             return nullptr;
320         }
321         std::shared_ptr<UserSession> userSession = sessionIt->second;
322         userSession->lastUpdated = std::chrono::steady_clock::now();
323         return userSession;
324     }
325 
getSessionByUid(std::string_view uid)326     std::shared_ptr<UserSession> getSessionByUid(std::string_view uid)
327     {
328         applySessionTimeouts();
329         // TODO(Ed) this is inefficient
330         auto sessionIt = authTokens.begin();
331         while (sessionIt != authTokens.end())
332         {
333             if (sessionIt->second->uniqueId == uid)
334             {
335                 return sessionIt->second;
336             }
337             sessionIt++;
338         }
339         return nullptr;
340     }
341 
removeSession(const std::shared_ptr<UserSession> & session)342     void removeSession(const std::shared_ptr<UserSession>& session)
343     {
344         authTokens.erase(session->sessionToken);
345         needWrite = true;
346     }
347 
getAllUniqueIds()348     std::vector<std::string> getAllUniqueIds()
349     {
350         applySessionTimeouts();
351         std::vector<std::string> ret;
352         ret.reserve(authTokens.size());
353         for (auto& session : authTokens)
354         {
355             ret.push_back(session.second->uniqueId);
356         }
357         return ret;
358     }
359 
getUniqueIdsBySessionType(SessionType type)360     std::vector<std::string> getUniqueIdsBySessionType(SessionType type)
361     {
362         applySessionTimeouts();
363 
364         std::vector<std::string> ret;
365         ret.reserve(authTokens.size());
366         for (auto& session : authTokens)
367         {
368             if (type == session.second->sessionType)
369             {
370                 ret.push_back(session.second->uniqueId);
371             }
372         }
373         return ret;
374     }
375 
getSessions()376     std::vector<std::shared_ptr<UserSession>> getSessions()
377     {
378         std::vector<std::shared_ptr<UserSession>> sessions;
379         sessions.reserve(authTokens.size());
380         for (auto& session : authTokens)
381         {
382             sessions.push_back(session.second);
383         }
384         return sessions;
385     }
386 
removeSessionsByUsername(std::string_view username)387     void removeSessionsByUsername(std::string_view username)
388     {
389         std::erase_if(authTokens, [username](const auto& value) {
390             if (value.second == nullptr)
391             {
392                 return false;
393             }
394             return value.second->username == username;
395         });
396     }
397 
removeSessionsByUsernameExceptSession(std::string_view username,const std::shared_ptr<UserSession> & session)398     void removeSessionsByUsernameExceptSession(
399         std::string_view username, const std::shared_ptr<UserSession>& session)
400     {
401         std::erase_if(authTokens, [username, session](const auto& value) {
402             if (value.second == nullptr)
403             {
404                 return false;
405             }
406 
407             return value.second->username == username &&
408                    value.second->uniqueId != session->uniqueId;
409         });
410     }
411 
updateAuthMethodsConfig(const AuthConfigMethods & config)412     void updateAuthMethodsConfig(const AuthConfigMethods& config)
413     {
414         bool isTLSchanged = (authMethodsConfig.tls != config.tls);
415         authMethodsConfig = config;
416         needWrite = true;
417         if (isTLSchanged)
418         {
419             // recreate socket connections with new settings
420             // NOLINTNEXTLINE(misc-include-cleaner)
421             std::raise(SIGHUP);
422         }
423     }
424 
getAuthMethodsConfig()425     AuthConfigMethods& getAuthMethodsConfig()
426     {
427         return authMethodsConfig;
428     }
429 
needsWrite() const430     bool needsWrite() const
431     {
432         return needWrite;
433     }
getTimeoutInSeconds() const434     int64_t getTimeoutInSeconds() const
435     {
436         return std::chrono::seconds(timeoutInSeconds).count();
437     }
438 
updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds)439     void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds)
440     {
441         timeoutInSeconds = newTimeoutInSeconds;
442         needWrite = true;
443     }
444 
getInstance()445     static SessionStore& getInstance()
446     {
447         static SessionStore sessionStore;
448         return sessionStore;
449     }
450 
applySessionTimeouts()451     void applySessionTimeouts()
452     {
453         auto timeNow = std::chrono::steady_clock::now();
454         if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1))
455         {
456             lastTimeoutUpdate = timeNow;
457             auto authTokensIt = authTokens.begin();
458             while (authTokensIt != authTokens.end())
459             {
460                 if (timeNow - authTokensIt->second->lastUpdated >=
461                     timeoutInSeconds)
462                 {
463                     authTokensIt = authTokens.erase(authTokensIt);
464 
465                     needWrite = true;
466                 }
467                 else
468                 {
469                     authTokensIt++;
470                 }
471             }
472         }
473     }
474 
475     SessionStore(const SessionStore&) = delete;
476     SessionStore& operator=(const SessionStore&) = delete;
477     SessionStore(SessionStore&&) = delete;
478     SessionStore& operator=(const SessionStore&&) = delete;
479     ~SessionStore() = default;
480 
481     std::unordered_map<std::string, std::shared_ptr<UserSession>,
482                        std::hash<std::string>, bmcweb::ConstantTimeCompare>
483         authTokens;
484 
485     std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
486     bool needWrite{false};
487     std::chrono::seconds timeoutInSeconds;
488     AuthConfigMethods authMethodsConfig;
489 
490   private:
SessionStore()491     SessionStore() : timeoutInSeconds(1800) {}
492 };
493 
494 } // namespace persistent_data
495