xref: /openbmc/bmcweb/include/persistent_data.hpp (revision 13880454f39e243c9efeb5d2b742f161ba7edee4)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 
5 #include "event_service_store.hpp"
6 #include "logging.hpp"
7 #include "ossl_random.hpp"
8 #include "sessions.hpp"
9 // NOLINTNEXTLINE(misc-include-cleaner)
10 #include "utility.hpp"
11 
12 #include <boost/beast/core/file_base.hpp>
13 #include <boost/beast/core/file_posix.hpp>
14 #include <boost/beast/http/fields.hpp>
15 #include <nlohmann/json.hpp>
16 
17 #include <chrono>
18 #include <cstdint>
19 #include <filesystem>
20 #include <fstream>
21 #include <memory>
22 #include <optional>
23 #include <string>
24 #include <system_error>
25 #include <utility>
26 
27 namespace persistent_data
28 {
29 
30 class ConfigFile
31 {
32     uint64_t jsonRevision = 1;
33 
34   public:
getStateFile()35     static std::string getStateFile()
36     {
37         // NOLINTNEXTLINE(concurrency-mt-unsafe)
38         const char* stateDir = std::getenv("STATE_DIRECTORY");
39         if (stateDir == nullptr)
40         {
41             stateDir = ".";
42         }
43         return std::string(stateDir) + "/bmcweb_persistent_data.json";
44     }
45 
filename()46     static const std::string& filename()
47     {
48         const static std::string fname = getStateFile();
49         return fname;
50     }
51 
ConfigFile()52     ConfigFile()
53     {
54         readData();
55     }
56 
~ConfigFile()57     ~ConfigFile()
58     {
59         // Make sure we aren't writing stale sessions
60         persistent_data::SessionStore::getInstance().applySessionTimeouts();
61         if (persistent_data::SessionStore::getInstance().needsWrite())
62         {
63             writeData();
64         }
65     }
66 
67     ConfigFile(const ConfigFile&) = delete;
68     ConfigFile(ConfigFile&&) = delete;
69     ConfigFile& operator=(const ConfigFile&) = delete;
70     ConfigFile& operator=(ConfigFile&&) = delete;
71 
72     // TODO(ed) this should really use protobuf, or some other serialization
73     // library, but adding another dependency is somewhat outside the scope of
74     // this application for the moment
readData()75     void readData()
76     {
77         std::ifstream persistentFile(filename());
78         uint64_t fileRevision = 0;
79         if (persistentFile.is_open())
80         {
81             // call with exceptions disabled
82             auto data = nlohmann::json::parse(persistentFile, nullptr, false);
83             if (data.is_discarded())
84             {
85                 BMCWEB_LOG_ERROR("Error parsing persistent data in json file.");
86             }
87             else
88             {
89                 const nlohmann::json::object_t* obj =
90                     data.get_ptr<nlohmann::json::object_t*>();
91                 if (obj == nullptr)
92                 {
93                     return;
94                 }
95                 for (const auto& item : *obj)
96                 {
97                     if (item.first == "revision")
98                     {
99                         fileRevision = 0;
100 
101                         const uint64_t* uintPtr =
102                             item.second.get_ptr<const uint64_t*>();
103                         if (uintPtr == nullptr)
104                         {
105                             BMCWEB_LOG_ERROR("Failed to read revision flag");
106                         }
107                         else
108                         {
109                             fileRevision = *uintPtr;
110                         }
111                     }
112                     else if (item.first == "system_uuid")
113                     {
114                         const std::string* jSystemUuid =
115                             item.second.get_ptr<const std::string*>();
116                         if (jSystemUuid != nullptr)
117                         {
118                             systemUuid = *jSystemUuid;
119                         }
120                     }
121                     else if (item.first == "service_identification")
122                     {
123                         const std::string* jServiceIdentification =
124                             item.second.get_ptr<const std::string*>();
125                         if (jServiceIdentification != nullptr)
126                         {
127                             serviceIdentification = *jServiceIdentification;
128                         }
129                     }
130                     else if (item.first == "auth_config")
131                     {
132                         const nlohmann::json::object_t* jObj =
133                             item.second
134                                 .get_ptr<const nlohmann::json::object_t*>();
135                         if (jObj == nullptr)
136                         {
137                             continue;
138                         }
139                         SessionStore::getInstance()
140                             .getAuthMethodsConfig()
141                             .fromJson(*jObj);
142                     }
143                     else if (item.first == "sessions")
144                     {
145                         for (const auto& elem : item.second)
146                         {
147                             const nlohmann::json::object_t* jObj =
148                                 elem.get_ptr<const nlohmann::json::object_t*>();
149                             if (jObj == nullptr)
150                             {
151                                 continue;
152                             }
153                             std::shared_ptr<UserSession> newSession =
154                                 UserSession::fromJson(*jObj);
155 
156                             if (newSession == nullptr)
157                             {
158                                 BMCWEB_LOG_ERROR("Problem reading session "
159                                                  "from persistent store");
160                                 continue;
161                             }
162 
163                             BMCWEB_LOG_DEBUG("Restored session: {} {} {}",
164                                              newSession->csrfToken,
165                                              newSession->uniqueId,
166                                              newSession->sessionToken);
167                             SessionStore::getInstance().authTokens.emplace(
168                                 newSession->sessionToken, newSession);
169                         }
170                     }
171                     else if (item.first == "timeout")
172                     {
173                         const int64_t* jTimeout =
174                             item.second.get_ptr<const int64_t*>();
175                         if (jTimeout == nullptr)
176                         {
177                             BMCWEB_LOG_DEBUG(
178                                 "Problem reading session timeout value");
179                             continue;
180                         }
181                         std::chrono::seconds sessionTimeoutInseconds(*jTimeout);
182                         BMCWEB_LOG_DEBUG("Restored Session Timeout: {}",
183                                          sessionTimeoutInseconds.count());
184                         SessionStore::getInstance().updateSessionTimeout(
185                             sessionTimeoutInseconds);
186                     }
187                     else if (item.first == "eventservice_config")
188                     {
189                         const nlohmann::json::object_t* esobj =
190                             item.second
191                                 .get_ptr<const nlohmann::json::object_t*>();
192                         if (esobj == nullptr)
193                         {
194                             BMCWEB_LOG_DEBUG(
195                                 "Problem reading EventService value");
196                             continue;
197                         }
198 
199                         EventServiceStore::getInstance()
200                             .getEventServiceConfig()
201                             .fromJson(*esobj);
202                     }
203                     else if (item.first == "subscriptions")
204                     {
205                         for (const auto& elem : item.second)
206                         {
207                             const nlohmann::json::object_t* subobj =
208                                 elem.get_ptr<const nlohmann::json::object_t*>();
209                             if (subobj == nullptr)
210                             {
211                                 continue;
212                             }
213 
214                             std::optional<UserSubscription> newSub =
215                                 UserSubscription::fromJson(*subobj);
216 
217                             if (!newSub)
218                             {
219                                 BMCWEB_LOG_ERROR(
220                                     "Problem reading subscription from persistent store");
221                                 continue;
222                             }
223 
224                             std::string id = newSub->id;
225                             BMCWEB_LOG_DEBUG("Restored subscription: {} {}", id,
226                                              newSub->customText);
227 
228                             EventServiceStore::getInstance()
229                                 .subscriptionsConfigMap.emplace(
230                                     id, std::make_shared<UserSubscription>(
231                                             std::move(*newSub)));
232                         }
233                     }
234                     else
235                     {
236                         // Do nothing in the case of extra fields.  We may have
237                         // cases where fields are added in the future, and we
238                         // want to at least attempt to gracefully support
239                         // downgrades in that case, even if we don't officially
240                         // support it
241                     }
242                 }
243             }
244         }
245         bool needWrite = false;
246 
247         if (systemUuid.empty())
248         {
249             systemUuid = bmcweb::getRandomUUID();
250             needWrite = true;
251         }
252         if (fileRevision < jsonRevision)
253         {
254             needWrite = true;
255         }
256         // write revision changes or system uuid changes immediately
257         if (needWrite)
258         {
259             writeData();
260         }
261     }
262 
writeData()263     void writeData()
264     {
265         const std::string& fname = filename();
266         std::filesystem::path path(fname);
267         path = path.parent_path();
268         if (!path.empty())
269         {
270             std::error_code ecDir;
271             std::filesystem::create_directories(path, ecDir);
272             if (ecDir)
273             {
274                 BMCWEB_LOG_CRITICAL("Can't create persistent folders {}",
275                                     ecDir.message());
276                 return;
277             }
278         }
279         boost::beast::file_posix persistentFile;
280         boost::system::error_code ec;
281         persistentFile.open(fname.c_str(), boost::beast::file_mode::write, ec);
282         if (ec)
283         {
284             BMCWEB_LOG_CRITICAL("Unable to store persistent data to file {}",
285                                 ec.message());
286             return;
287         }
288 
289         // set the permission of the file to 640
290         std::filesystem::perms permission =
291             std::filesystem::perms::owner_read |
292             std::filesystem::perms::owner_write |
293             std::filesystem::perms::group_read;
294         std::filesystem::permissions(fname, permission, ec);
295         if (ec)
296         {
297             BMCWEB_LOG_CRITICAL("Failed to set filesystem permissions {}",
298                                 ec.message());
299             return;
300         }
301         const AuthConfigMethods& c =
302             SessionStore::getInstance().getAuthMethodsConfig();
303         const auto& eventServiceConfig =
304             EventServiceStore::getInstance().getEventServiceConfig();
305         nlohmann::json::object_t data;
306         nlohmann::json& authConfig = data["auth_config"];
307 
308         authConfig["XToken"] = c.xtoken;
309         authConfig["Cookie"] = c.cookie;
310         authConfig["SessionToken"] = c.sessionToken;
311         authConfig["BasicAuth"] = c.basic;
312         authConfig["TLS"] = c.tls;
313         authConfig["TLSStrict"] = c.tlsStrict;
314         authConfig["MTLSCommonNameParseMode"] =
315             static_cast<int>(c.mTLSCommonNameParsingMode);
316 
317         nlohmann::json& eventserviceConfig = data["eventservice_config"];
318         eventserviceConfig["ServiceEnabled"] = eventServiceConfig.enabled;
319         eventserviceConfig["DeliveryRetryAttempts"] =
320             eventServiceConfig.retryAttempts;
321         eventserviceConfig["DeliveryRetryIntervalSeconds"] =
322             eventServiceConfig.retryTimeoutInterval;
323 
324         data["system_uuid"] = systemUuid;
325         data["service_identification"] = serviceIdentification;
326         data["revision"] = jsonRevision;
327         data["timeout"] = SessionStore::getInstance().getTimeoutInSeconds();
328 
329         nlohmann::json& sessions = data["sessions"];
330         sessions = nlohmann::json::array();
331         for (const auto& p : SessionStore::getInstance().authTokens)
332         {
333             if (p.second->sessionType != persistent_data::SessionType::Basic &&
334                 p.second->sessionType !=
335                     persistent_data::SessionType::MutualTLS)
336             {
337                 nlohmann::json::object_t session;
338                 session["unique_id"] = p.second->uniqueId;
339                 session["session_token"] = p.second->sessionToken;
340                 session["username"] = p.second->username;
341                 session["csrf_token"] = p.second->csrfToken;
342                 session["client_ip"] = p.second->clientIp;
343                 const std::optional<std::string>& clientId = p.second->clientId;
344                 if (clientId)
345                 {
346                     session["client_id"] = *clientId;
347                 }
348                 sessions.emplace_back(std::move(session));
349             }
350         }
351         nlohmann::json& subscriptions = data["subscriptions"];
352         subscriptions = nlohmann::json::array();
353         for (const auto& it :
354              EventServiceStore::getInstance().subscriptionsConfigMap)
355         {
356             if (it.second == nullptr)
357             {
358                 continue;
359             }
360             const UserSubscription& subValue = *it.second;
361             if (subValue.subscriptionType == "SSE")
362             {
363                 BMCWEB_LOG_DEBUG("The subscription type is SSE, so skipping.");
364                 continue;
365             }
366             nlohmann::json::object_t headers;
367             for (const boost::beast::http::fields::value_type& header :
368                  subValue.httpHeaders)
369             {
370                 // Note, these are technically copies because nlohmann doesn't
371                 // support key lookup by std::string_view.  At least the
372                 // following code can use move
373                 // https://github.com/nlohmann/json/issues/1529
374                 std::string name(header.name_string());
375                 headers[std::move(name)] = header.value();
376             }
377 
378             nlohmann::json::object_t subscription;
379 
380             subscription["Id"] = subValue.id;
381             subscription["Context"] = subValue.customText;
382             subscription["DeliveryRetryPolicy"] = subValue.retryPolicy;
383             subscription["SendHeartbeat"] = subValue.sendHeartbeat;
384             subscription["HeartbeatIntervalMinutes"] =
385                 subValue.hbIntervalMinutes;
386             subscription["Destination"] = subValue.destinationUrl;
387             subscription["EventFormatType"] = subValue.eventFormatType;
388             subscription["HttpHeaders"] = std::move(headers);
389             subscription["MessageIds"] = subValue.registryMsgIds;
390             subscription["Protocol"] = subValue.protocol;
391             subscription["RegistryPrefixes"] = subValue.registryPrefixes;
392             subscription["OriginResources"] = subValue.originResources;
393             subscription["ResourceTypes"] = subValue.resourceTypes;
394             subscription["SubscriptionType"] = subValue.subscriptionType;
395             subscription["MetricReportDefinitions"] =
396                 subValue.metricReportDefinitions;
397             subscription["VerifyCertificate"] = subValue.verifyCertificate;
398 
399             subscriptions.emplace_back(std::move(subscription));
400         }
401         std::string out = nlohmann::json(data).dump(
402             -1, ' ', true, nlohmann::json::error_handler_t::replace);
403         persistentFile.write(out.data(), out.size(), ec);
404         if (ec)
405         {
406             BMCWEB_LOG_ERROR("Failed to write file {}", ec.message());
407         }
408     }
409 
410     std::string systemUuid;
411     std::string serviceIdentification;
412 };
413 
getConfig()414 inline ConfigFile& getConfig()
415 {
416     static ConfigFile f;
417     return f;
418 }
419 
420 } // namespace persistent_data
421