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