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