xref: /openbmc/bmcweb/include/sessions.hpp (revision cb103130)
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