blob: f46152155801024cba237f961732ba8dc2b33939 [file] [log] [blame]
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +01001#pragma once
2
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +01003#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>
Zbigniew Kurzynski009c2a42019-11-14 13:37:15 +01007#include <csignal>
Ratan Gupta12c04ef2019-04-03 10:08:11 +05308#include <dbus_singleton.hpp>
Ed Tanous1abe55e2018-09-05 08:30:59 -07009#include <nlohmann/json.hpp>
10#include <pam_authenticate.hpp>
11#include <random>
RAJESWARAN THILLAIGOVINDAN70525172019-07-09 13:15:05 -050012#include <sdbusplus/bus/match.hpp>
Ratan Gupta12c04ef2019-04-03 10:08:11 +053013
Ed Tanousc94ad492019-10-10 15:39:33 -070014#include "logging.h"
Ed Tanous51dae672018-09-05 16:07:32 -070015#include "utility.h"
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010016
Ed Tanous1abe55e2018-09-05 08:30:59 -070017namespace crow
18{
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010019
Ed Tanous1abe55e2018-09-05 08:30:59 -070020namespace persistent_data
21{
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010022
Ed Tanous51dae672018-09-05 16:07:32 -070023// entropy: 20 characters, 62 possibilities. log2(62^20) = 119 bits of
24// entropy. OWASP recommends at least 64
25// https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
26constexpr std::size_t sessionTokenSize = 20;
27
Ed Tanous1abe55e2018-09-05 08:30:59 -070028enum class PersistenceType
29{
30 TIMEOUT, // User session times out after a predetermined amount of time
31 SINGLE_REQUEST // User times out once this request is completed.
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010032};
33
James Feist7166bf02019-12-10 16:52:14 +000034constexpr char const* userService = "xyz.openbmc_project.User.Manager";
35constexpr char const* userObjPath = "/xyz/openbmc_project/user";
36constexpr char const* userAttrIface = "xyz.openbmc_project.User.Attributes";
37constexpr char const* dbusPropertiesIface = "org.freedesktop.DBus.Properties";
38
39struct UserRoleMap
40{
41 using GetManagedPropertyType =
42 boost::container::flat_map<std::string,
43 std::variant<std::string, bool>>;
44
45 using InterfacesPropertiesType =
46 boost::container::flat_map<std::string, GetManagedPropertyType>;
47
48 using GetManagedObjectsType = std::vector<
49 std::pair<sdbusplus::message::object_path, InterfacesPropertiesType>>;
50
51 static UserRoleMap& getInstance()
52 {
53 static UserRoleMap userRoleMap;
54 return userRoleMap;
55 }
56
57 UserRoleMap(const UserRoleMap&) = delete;
58 UserRoleMap& operator=(const UserRoleMap&) = delete;
59
60 std::string getUserRole(std::string_view name)
61 {
62 auto it = roleMap.find(std::string(name));
63 if (it == roleMap.end())
64 {
65 BMCWEB_LOG_ERROR << "User name " << name
66 << " is not found in the UserRoleMap.";
67 return "";
68 }
69 return it->second;
70 }
71
72 std::string
73 extractUserRole(const InterfacesPropertiesType& interfacesProperties)
74 {
75 auto iface = interfacesProperties.find(userAttrIface);
76 if (iface == interfacesProperties.end())
77 {
78 return {};
79 }
80
81 auto& properties = iface->second;
82 auto property = properties.find("UserPrivilege");
83 if (property == properties.end())
84 {
85 return {};
86 }
87
88 const std::string* role = std::get_if<std::string>(&property->second);
89 if (role == nullptr)
90 {
91 BMCWEB_LOG_ERROR << "UserPrivilege property value is null";
92 return {};
93 }
94
95 return *role;
96 }
97
98 private:
99 void userAdded(sdbusplus::message::message& m)
100 {
101 sdbusplus::message::object_path objPath;
102 InterfacesPropertiesType interfacesProperties;
103
104 try
105 {
106 m.read(objPath, interfacesProperties);
107 }
108 catch (const sdbusplus::exception::SdBusError& e)
109 {
110 BMCWEB_LOG_ERROR << "Failed to parse user add signal."
111 << "ERROR=" << e.what()
112 << "REPLY_SIG=" << m.get_signature();
113 return;
114 }
115 BMCWEB_LOG_DEBUG << "obj path = " << objPath.str;
116
117 std::size_t lastPos = objPath.str.rfind("/");
118 if (lastPos == std::string::npos)
119 {
120 return;
121 };
122
123 std::string name = objPath.str.substr(lastPos + 1);
124 std::string role = this->extractUserRole(interfacesProperties);
125
126 // Insert the newly added user name and the role
127 auto res = roleMap.emplace(name, role);
128 if (res.second == false)
129 {
130 BMCWEB_LOG_ERROR << "Insertion of the user=\"" << name
131 << "\" in the roleMap failed.";
132 return;
133 }
134 }
135
136 void userRemoved(sdbusplus::message::message& m)
137 {
138 sdbusplus::message::object_path objPath;
139
140 try
141 {
142 m.read(objPath);
143 }
144 catch (const sdbusplus::exception::SdBusError& e)
145 {
146 BMCWEB_LOG_ERROR << "Failed to parse user delete signal.";
147 BMCWEB_LOG_ERROR << "ERROR=" << e.what()
148 << "REPLY_SIG=" << m.get_signature();
149 return;
150 }
151
152 BMCWEB_LOG_DEBUG << "obj path = " << objPath.str;
153
154 std::size_t lastPos = objPath.str.rfind("/");
155 if (lastPos == std::string::npos)
156 {
157 return;
158 };
159
160 // User name must be atleast 1 char in length.
161 if ((lastPos + 1) >= objPath.str.length())
162 {
163 return;
164 }
165
166 std::string name = objPath.str.substr(lastPos + 1);
167
168 roleMap.erase(name);
169 }
170
171 void userPropertiesChanged(sdbusplus::message::message& m)
172 {
173 std::string interface;
174 GetManagedPropertyType changedProperties;
175 m.read(interface, changedProperties);
176 const std::string path = m.get_path();
177
178 BMCWEB_LOG_DEBUG << "Object Path = \"" << path << "\"";
179
180 std::size_t lastPos = path.rfind("/");
181 if (lastPos == std::string::npos)
182 {
183 return;
184 };
185
186 // User name must be at least 1 char in length.
187 if ((lastPos + 1) == path.length())
188 {
189 return;
190 }
191
192 std::string user = path.substr(lastPos + 1);
193
194 BMCWEB_LOG_DEBUG << "User Name = \"" << user << "\"";
195
196 auto index = changedProperties.find("UserPrivilege");
197 if (index == changedProperties.end())
198 {
199 return;
200 }
201
202 const std::string* role = std::get_if<std::string>(&index->second);
203 if (role == nullptr)
204 {
205 return;
206 }
207 BMCWEB_LOG_DEBUG << "Role = \"" << *role << "\"";
208
209 auto it = roleMap.find(user);
210 if (it == roleMap.end())
211 {
212 BMCWEB_LOG_ERROR << "User Name = \"" << user
213 << "\" is not found. But, received "
214 "propertiesChanged signal";
215 return;
216 }
217 it->second = *role;
218 }
219
220 UserRoleMap() :
221 userAddedSignal(
222 *crow::connections::systemBus,
223 sdbusplus::bus::match::rules::interfacesAdded(userObjPath),
224 [this](sdbusplus::message::message& m) {
225 BMCWEB_LOG_DEBUG << "User Added";
226 this->userAdded(m);
227 }),
228 userRemovedSignal(
229 *crow::connections::systemBus,
230 sdbusplus::bus::match::rules::interfacesRemoved(userObjPath),
231 [this](sdbusplus::message::message& m) {
232 BMCWEB_LOG_DEBUG << "User Removed";
233 this->userRemoved(m);
234 }),
235 userPropertiesChangedSignal(
236 *crow::connections::systemBus,
237 sdbusplus::bus::match::rules::path_namespace(userObjPath) +
238 sdbusplus::bus::match::rules::type::signal() +
239 sdbusplus::bus::match::rules::member("PropertiesChanged") +
240 sdbusplus::bus::match::rules::interface(dbusPropertiesIface) +
241 sdbusplus::bus::match::rules::argN(0, userAttrIface),
242 [this](sdbusplus::message::message& m) {
243 BMCWEB_LOG_DEBUG << "Properties Changed";
244 this->userPropertiesChanged(m);
245 })
246 {
247 crow::connections::systemBus->async_method_call(
248 [this](boost::system::error_code ec,
249 GetManagedObjectsType& managedObjects) {
250 if (ec)
251 {
252 BMCWEB_LOG_DEBUG << "User manager call failed, ignoring";
253 return;
254 }
255
256 for (auto& managedObj : managedObjects)
257 {
258 std::size_t lastPos = managedObj.first.str.rfind("/");
259 if (lastPos == std::string::npos)
260 {
261 continue;
262 };
263 std::string name = managedObj.first.str.substr(lastPos + 1);
264 std::string role = extractUserRole(managedObj.second);
265 roleMap.emplace(name, role);
266 }
267 },
268 userService, userObjPath, "org.freedesktop.DBus.ObjectManager",
269 "GetManagedObjects");
270 }
271
272 boost::container::flat_map<std::string, std::string> roleMap;
273 sdbusplus::bus::match_t userAddedSignal;
274 sdbusplus::bus::match_t userRemovedSignal;
275 sdbusplus::bus::match_t userPropertiesChangedSignal;
276};
277
Ed Tanous1abe55e2018-09-05 08:30:59 -0700278struct UserSession
279{
280 std::string uniqueId;
281 std::string sessionToken;
282 std::string username;
283 std::string csrfToken;
284 std::chrono::time_point<std::chrono::steady_clock> lastUpdated;
285 PersistenceType persistence;
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +0100286
Ed Tanous1abe55e2018-09-05 08:30:59 -0700287 /**
288 * @brief Fills object with data from UserSession's JSON representation
289 *
290 * This replaces nlohmann's from_json to ensure no-throw approach
291 *
292 * @param[in] j JSON object from which data should be loaded
293 *
294 * @return a shared pointer if data has been loaded properly, nullptr
295 * otherwise
296 */
297 static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j)
298 {
299 std::shared_ptr<UserSession> userSession =
300 std::make_shared<UserSession>();
301 for (const auto& element : j.items())
302 {
303 const std::string* thisValue =
304 element.value().get_ptr<const std::string*>();
305 if (thisValue == nullptr)
306 {
307 BMCWEB_LOG_ERROR << "Error reading persistent store. Property "
308 << element.key() << " was not of type string";
309 return nullptr;
310 }
311 if (element.key() == "unique_id")
312 {
313 userSession->uniqueId = *thisValue;
314 }
315 else if (element.key() == "session_token")
316 {
317 userSession->sessionToken = *thisValue;
318 }
319 else if (element.key() == "csrf_token")
320 {
321 userSession->csrfToken = *thisValue;
322 }
323 else if (element.key() == "username")
324 {
325 userSession->username = *thisValue;
326 }
327 else
328 {
329 BMCWEB_LOG_ERROR
330 << "Got unexpected property reading persistent file: "
331 << element.key();
332 return nullptr;
333 }
334 }
335
336 // For now, sessions that were persisted through a reboot get their idle
337 // timer reset. This could probably be overcome with a better
338 // understanding of wall clock time and steady timer time, possibly
339 // persisting values with wall clock time instead of steady timer, but
340 // the tradeoffs of all the corner cases involved are non-trivial, so
341 // this is done temporarily
342 userSession->lastUpdated = std::chrono::steady_clock::now();
343 userSession->persistence = PersistenceType::TIMEOUT;
344
345 return userSession;
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +0100346 }
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100347};
348
Zbigniew Kurzynski78158632019-11-05 12:57:37 +0100349struct AuthConfigMethods
350{
351 bool xtoken = true;
352 bool cookie = true;
353 bool sessionToken = true;
354 bool basic = true;
Zbigniew Kurzynski501f1e52019-10-02 11:22:11 +0200355 bool tls = true;
Zbigniew Kurzynski78158632019-11-05 12:57:37 +0100356
357 void fromJson(const nlohmann::json& j)
358 {
359 for (const auto& element : j.items())
360 {
361 const bool* value = element.value().get_ptr<const bool*>();
362 if (value == nullptr)
363 {
364 continue;
365 }
366
367 if (element.key() == "XToken")
368 {
369 xtoken = *value;
370 }
371 else if (element.key() == "Cookie")
372 {
373 cookie = *value;
374 }
375 else if (element.key() == "SessionToken")
376 {
377 sessionToken = *value;
378 }
379 else if (element.key() == "BasicAuth")
380 {
381 basic = *value;
382 }
Zbigniew Kurzynski501f1e52019-10-02 11:22:11 +0200383 else if (element.key() == "TLS")
384 {
385 tls = *value;
386 }
Zbigniew Kurzynski78158632019-11-05 12:57:37 +0100387 }
388 }
389};
390
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100391class Middleware;
392
Ed Tanous1abe55e2018-09-05 08:30:59 -0700393class SessionStore
394{
395 public:
396 std::shared_ptr<UserSession> generateUserSession(
Ed Tanous39e77502019-03-04 17:35:53 -0800397 const std::string_view username,
Ed Tanous1abe55e2018-09-05 08:30:59 -0700398 PersistenceType persistence = PersistenceType::TIMEOUT)
399 {
400 // TODO(ed) find a secure way to not generate session identifiers if
401 // persistence is set to SINGLE_REQUEST
402 static constexpr std::array<char, 62> alphanum = {
Joseph Reynolds368b1d42019-08-15 15:29:06 -0500403 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
404 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
405 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c',
Ed Tanous1abe55e2018-09-05 08:30:59 -0700406 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
407 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100408
Ed Tanous1abe55e2018-09-05 08:30:59 -0700409 std::string sessionToken;
Ed Tanous51dae672018-09-05 16:07:32 -0700410 sessionToken.resize(sessionTokenSize, '0');
Ed Tanous271584a2019-07-09 16:24:22 -0700411 std::uniform_int_distribution<size_t> dist(0, alphanum.size() - 1);
412 for (size_t i = 0; i < sessionToken.size(); ++i)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700413 {
414 sessionToken[i] = alphanum[dist(rd)];
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100415 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700416 // Only need csrf tokens for cookie based auth, token doesn't matter
417 std::string csrfToken;
Ed Tanous51dae672018-09-05 16:07:32 -0700418 csrfToken.resize(sessionTokenSize, '0');
Ed Tanous271584a2019-07-09 16:24:22 -0700419 for (size_t i = 0; i < csrfToken.size(); ++i)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700420 {
421 csrfToken[i] = alphanum[dist(rd)];
422 }
423
424 std::string uniqueId;
425 uniqueId.resize(10, '0');
Ed Tanous271584a2019-07-09 16:24:22 -0700426 for (size_t i = 0; i < uniqueId.size(); ++i)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700427 {
428 uniqueId[i] = alphanum[dist(rd)];
429 }
Ratan Gupta12c04ef2019-04-03 10:08:11 +0530430
Ed Tanous1abe55e2018-09-05 08:30:59 -0700431 auto session = std::make_shared<UserSession>(UserSession{
Ed Tanousca0c93b2019-09-19 11:53:50 -0700432 uniqueId, sessionToken, std::string(username), csrfToken,
Ed Tanous1abe55e2018-09-05 08:30:59 -0700433 std::chrono::steady_clock::now(), persistence});
434 auto it = authTokens.emplace(std::make_pair(sessionToken, session));
435 // Only need to write to disk if session isn't about to be destroyed.
436 needWrite = persistence == PersistenceType::TIMEOUT;
437 return it.first->second;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100438 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700439
440 std::shared_ptr<UserSession>
Ed Tanous39e77502019-03-04 17:35:53 -0800441 loginSessionByToken(const std::string_view token)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700442 {
443 applySessionTimeouts();
Ed Tanous51dae672018-09-05 16:07:32 -0700444 if (token.size() != sessionTokenSize)
445 {
446 return nullptr;
447 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700448 auto sessionIt = authTokens.find(std::string(token));
449 if (sessionIt == authTokens.end())
450 {
451 return nullptr;
452 }
453 std::shared_ptr<UserSession> userSession = sessionIt->second;
454 userSession->lastUpdated = std::chrono::steady_clock::now();
455 return userSession;
456 }
457
Ed Tanous39e77502019-03-04 17:35:53 -0800458 std::shared_ptr<UserSession> getSessionByUid(const std::string_view uid)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700459 {
460 applySessionTimeouts();
461 // TODO(Ed) this is inefficient
462 auto sessionIt = authTokens.begin();
463 while (sessionIt != authTokens.end())
464 {
465 if (sessionIt->second->uniqueId == uid)
466 {
467 return sessionIt->second;
468 }
469 sessionIt++;
470 }
471 return nullptr;
472 }
473
474 void removeSession(std::shared_ptr<UserSession> session)
475 {
476 authTokens.erase(session->sessionToken);
477 needWrite = true;
478 }
479
480 std::vector<const std::string*> getUniqueIds(
481 bool getAll = true,
482 const PersistenceType& type = PersistenceType::SINGLE_REQUEST)
483 {
484 applySessionTimeouts();
485
486 std::vector<const std::string*> ret;
487 ret.reserve(authTokens.size());
488 for (auto& session : authTokens)
489 {
490 if (getAll || type == session.second->persistence)
491 {
492 ret.push_back(&session.second->uniqueId);
493 }
494 }
495 return ret;
496 }
497
Zbigniew Kurzynski78158632019-11-05 12:57:37 +0100498 void updateAuthMethodsConfig(const AuthConfigMethods& config)
499 {
Zbigniew Kurzynski009c2a42019-11-14 13:37:15 +0100500 bool isTLSchanged = (authMethodsConfig.tls != config.tls);
Zbigniew Kurzynski78158632019-11-05 12:57:37 +0100501 authMethodsConfig = config;
502 needWrite = true;
Zbigniew Kurzynski009c2a42019-11-14 13:37:15 +0100503 if (isTLSchanged)
504 {
505 // recreate socket connections with new settings
506 std::raise(SIGHUP);
507 }
Zbigniew Kurzynski78158632019-11-05 12:57:37 +0100508 }
509
510 AuthConfigMethods& getAuthMethodsConfig()
511 {
512 return authMethodsConfig;
513 }
514
Ed Tanous1abe55e2018-09-05 08:30:59 -0700515 bool needsWrite()
516 {
517 return needWrite;
518 }
Ed Tanous271584a2019-07-09 16:24:22 -0700519 int64_t getTimeoutInSeconds() const
Ed Tanous1abe55e2018-09-05 08:30:59 -0700520 {
521 return std::chrono::seconds(timeoutInMinutes).count();
522 };
523
524 // Persistent data middleware needs to be able to serialize our authTokens
525 // structure, which is private
526 friend Middleware;
527
528 static SessionStore& getInstance()
529 {
530 static SessionStore sessionStore;
531 return sessionStore;
532 }
533
534 SessionStore(const SessionStore&) = delete;
535 SessionStore& operator=(const SessionStore&) = delete;
536
537 private:
538 SessionStore() : timeoutInMinutes(60)
539 {
540 }
541
542 void applySessionTimeouts()
543 {
544 auto timeNow = std::chrono::steady_clock::now();
545 if (timeNow - lastTimeoutUpdate > std::chrono::minutes(1))
546 {
547 lastTimeoutUpdate = timeNow;
548 auto authTokensIt = authTokens.begin();
549 while (authTokensIt != authTokens.end())
550 {
551 if (timeNow - authTokensIt->second->lastUpdated >=
552 timeoutInMinutes)
553 {
554 authTokensIt = authTokens.erase(authTokensIt);
555 needWrite = true;
556 }
557 else
558 {
559 authTokensIt++;
560 }
561 }
562 }
563 }
Ratan Gupta12c04ef2019-04-03 10:08:11 +0530564
Ed Tanous1abe55e2018-09-05 08:30:59 -0700565 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
Ed Tanous51dae672018-09-05 16:07:32 -0700566 std::unordered_map<std::string, std::shared_ptr<UserSession>,
567 std::hash<std::string>,
568 crow::utility::ConstantTimeCompare>
Ed Tanous1abe55e2018-09-05 08:30:59 -0700569 authTokens;
570 std::random_device rd;
571 bool needWrite{false};
572 std::chrono::minutes timeoutInMinutes;
Zbigniew Kurzynski78158632019-11-05 12:57:37 +0100573 AuthConfigMethods authMethodsConfig;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100574};
575
Ed Tanous1abe55e2018-09-05 08:30:59 -0700576} // namespace persistent_data
577} // namespace crow
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200578
579// to_json(...) definition for objects of UserSession type
Ed Tanous1abe55e2018-09-05 08:30:59 -0700580namespace nlohmann
581{
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200582template <>
Ed Tanous1abe55e2018-09-05 08:30:59 -0700583struct adl_serializer<std::shared_ptr<crow::persistent_data::UserSession>>
584{
585 static void
586 to_json(nlohmann::json& j,
587 const std::shared_ptr<crow::persistent_data::UserSession>& p)
588 {
589 if (p->persistence !=
590 crow::persistent_data::PersistenceType::SINGLE_REQUEST)
591 {
592 j = nlohmann::json{{"unique_id", p->uniqueId},
593 {"session_token", p->sessionToken},
594 {"username", p->username},
595 {"csrf_token", p->csrfToken}};
596 }
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200597 }
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200598};
Zbigniew Kurzynski78158632019-11-05 12:57:37 +0100599
600template <> struct adl_serializer<crow::persistent_data::AuthConfigMethods>
601{
602 static void to_json(nlohmann::json& j,
603 const crow::persistent_data::AuthConfigMethods& c)
604 {
605 j = nlohmann::json{{"XToken", c.xtoken},
606 {"Cookie", c.cookie},
607 {"SessionToken", c.sessionToken},
Zbigniew Kurzynski501f1e52019-10-02 11:22:11 +0200608 {"BasicAuth", c.basic},
609 {"TLS", c.tls}};
Zbigniew Kurzynski78158632019-11-05 12:57:37 +0100610 }
611};
Ed Tanous1abe55e2018-09-05 08:30:59 -0700612} // namespace nlohmann