blob: f8f3e8efdd0f19230b42ed0c3f9398573534fcb0 [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>
Ratan Gupta12c04ef2019-04-03 10:08:11 +05307#include <dbus_singleton.hpp>
Ed Tanous1abe55e2018-09-05 08:30:59 -07008#include <nlohmann/json.hpp>
9#include <pam_authenticate.hpp>
10#include <random>
Ratan Gupta12c04ef2019-04-03 10:08:11 +053011
12#include "crow/logging.h"
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010013
Ed Tanous1abe55e2018-09-05 08:30:59 -070014namespace crow
15{
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010016
Ed Tanous1abe55e2018-09-05 08:30:59 -070017namespace persistent_data
18{
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010019
Ed Tanous1abe55e2018-09-05 08:30:59 -070020enum class PersistenceType
21{
22 TIMEOUT, // User session times out after a predetermined amount of time
23 SINGLE_REQUEST // User times out once this request is completed.
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010024};
25
Ratan Gupta12c04ef2019-04-03 10:08:11 +053026constexpr char const* userService = "xyz.openbmc_project.User.Manager";
27constexpr char const* userObjPath = "/xyz/openbmc_project/user";
28constexpr char const* userAttrIface = "xyz.openbmc_project.User.Attributes";
29constexpr char const* dbusPropertiesIface = "org.freedesktop.DBus.Properties";
30
31class SessionStore;
32
33struct UserRoleMap
34{
35 using GetManagedPropertyType =
36 boost::container::flat_map<std::string,
37 std::variant<std::string, bool>>;
38
39 using InterfacesPropertiesType =
40 boost::container::flat_map<std::string, GetManagedPropertyType>;
41
42 using GetManagedObjectsType = std::vector<
43 std::pair<sdbusplus::message::object_path, InterfacesPropertiesType>>;
44
45 static UserRoleMap& getInstance()
46 {
47 static UserRoleMap userRoleMap;
48 return userRoleMap;
49 }
50
51 UserRoleMap(const UserRoleMap&) = delete;
52 UserRoleMap& operator=(const UserRoleMap&) = delete;
53
54 std::string getUserRole(std::string_view name)
55 {
56 auto it = roleMap.find(std::string(name));
57 if (it == roleMap.end())
58 {
59 BMCWEB_LOG_ERROR << "User name " << name
60 << " is not found in the UserRoleMap.";
61 return "";
62 }
63 return it->second;
64 }
65
66 std::string
67 extractUserRole(const InterfacesPropertiesType& interfacesProperties)
68 {
69 auto iface = interfacesProperties.find(userAttrIface);
70 if (iface == interfacesProperties.end())
71 {
72 return {};
73 }
74
75 auto& properties = iface->second;
76 auto property = properties.find("UserPrivilege");
77 if (property == properties.end())
78 {
79 return {};
80 }
81
82 const std::string* role = std::get_if<std::string>(&property->second);
83 if (role == nullptr)
84 {
85 BMCWEB_LOG_ERROR << "UserPrivilege property value is null";
86 return {};
87 }
88
89 return *role;
90 }
91
92 private:
93 void userAdded(sdbusplus::message::message& m)
94 {
95 sdbusplus::message::object_path objPath;
96 InterfacesPropertiesType interfacesProperties;
97
98 try
99 {
100 m.read(objPath, interfacesProperties);
101 }
102 catch (const sdbusplus::exception::SdBusError& e)
103 {
104 BMCWEB_LOG_ERROR << "Failed to parse user add signal."
105 << "ERROR=" << e.what()
106 << "REPLY_SIG=" << m.get_signature();
107 return;
108 }
109 BMCWEB_LOG_DEBUG << "obj path = " << objPath.str;
110
111 std::size_t lastPos = objPath.str.rfind("/");
112 if (lastPos == std::string::npos)
113 {
114 return;
115 };
116
117 std::string name = objPath.str.substr(lastPos + 1);
118 std::string role = this->extractUserRole(interfacesProperties);
119
120 // Insert the newly added user name and the role
121 auto res = roleMap.emplace(name, role);
122 if (res.second == false)
123 {
124 BMCWEB_LOG_ERROR << "Insertion of the user=\"" << name
125 << "\" in the roleMap failed.";
126 return;
127 }
128 }
129
130 void userRemoved(sdbusplus::message::message& m)
131 {
132 sdbusplus::message::object_path objPath;
133
134 try
135 {
136 m.read(objPath);
137 }
138 catch (const sdbusplus::exception::SdBusError& e)
139 {
140 BMCWEB_LOG_ERROR << "Failed to parse user delete signal.";
141 BMCWEB_LOG_ERROR << "ERROR=" << e.what()
142 << "REPLY_SIG=" << m.get_signature();
143 return;
144 }
145
146 BMCWEB_LOG_DEBUG << "obj path = " << objPath.str;
147
148 std::size_t lastPos = objPath.str.rfind("/");
149 if (lastPos == std::string::npos)
150 {
151 return;
152 };
153
154 // User name must be atleast 1 char in length.
155 if ((lastPos + 1) >= objPath.str.length())
156 {
157 return;
158 }
159
160 std::string name = objPath.str.substr(lastPos + 1);
161
162 roleMap.erase(name);
163 }
164
165 void userPropertiesChanged(sdbusplus::message::message& m)
166 {
167 std::string interface;
168 GetManagedPropertyType changedProperties;
169 m.read(interface, changedProperties);
170 const std::string path = m.get_path();
171
172 BMCWEB_LOG_DEBUG << "Object Path = \"" << path << "\"";
173
174 std::size_t lastPos = path.rfind("/");
175 if (lastPos == std::string::npos)
176 {
177 return;
178 };
179
180 // User name must be at least 1 char in length.
181 if ((lastPos + 1) == path.length())
182 {
183 return;
184 }
185
186 std::string user = path.substr(lastPos + 1);
187
188 BMCWEB_LOG_DEBUG << "User Name = \"" << user << "\"";
189
190 auto index = changedProperties.find("UserPrivilege");
191 if (index == changedProperties.end())
192 {
193 return;
194 }
195
196 const std::string* role = std::get_if<std::string>(&index->second);
197 if (role == nullptr)
198 {
199 return;
200 }
201 BMCWEB_LOG_DEBUG << "Role = \"" << *role << "\"";
202
203 auto it = roleMap.find(user);
204 if (it == roleMap.end())
205 {
206 BMCWEB_LOG_ERROR << "User Name = \"" << user
207 << "\" is not found. But, received "
208 "propertiesChanged signal";
209 return;
210 }
211 it->second = *role;
212 }
213
214 UserRoleMap() :
215 userAddedSignal(
216 *crow::connections::systemBus,
217 sdbusplus::bus::match::rules::interfacesAdded(userObjPath),
218 [this](sdbusplus::message::message& m) {
219 BMCWEB_LOG_DEBUG << "User Added";
220 this->userAdded(m);
221 }),
222 userRemovedSignal(
223 *crow::connections::systemBus,
224 sdbusplus::bus::match::rules::interfacesRemoved(userObjPath),
225 [this](sdbusplus::message::message& m) {
226 BMCWEB_LOG_DEBUG << "User Removed";
227 this->userRemoved(m);
228 }),
229 userPropertiesChangedSignal(
230 *crow::connections::systemBus,
231 sdbusplus::bus::match::rules::path_namespace(userObjPath) +
232 sdbusplus::bus::match::rules::type::signal() +
233 sdbusplus::bus::match::rules::member("PropertiesChanged") +
234 sdbusplus::bus::match::rules::interface(dbusPropertiesIface) +
235 sdbusplus::bus::match::rules::argN(0, userAttrIface),
236 [this](sdbusplus::message::message& m) {
237 BMCWEB_LOG_DEBUG << "Properties Changed";
238 this->userPropertiesChanged(m);
239 })
240 {
241 crow::connections::systemBus->async_method_call(
242 [this](boost::system::error_code ec,
243 GetManagedObjectsType& managedObjects) {
244 if (ec)
245 {
246 BMCWEB_LOG_DEBUG << "User manager call failed, ignoring";
247 return;
248 }
249
250 for (auto& managedObj : managedObjects)
251 {
252 std::size_t lastPos = managedObj.first.str.rfind("/");
253 if (lastPos == std::string::npos)
254 {
255 continue;
256 };
257 std::string name = managedObj.first.str.substr(lastPos + 1);
258 std::string role = extractUserRole(managedObj.second);
259 roleMap.emplace(name, role);
260 }
261 },
262 userService, userObjPath, "org.freedesktop.DBus.ObjectManager",
263 "GetManagedObjects");
264 }
265
266 boost::container::flat_map<std::string, std::string> roleMap;
267 sdbusplus::bus::match_t userAddedSignal;
268 sdbusplus::bus::match_t userRemovedSignal;
269 sdbusplus::bus::match_t userPropertiesChangedSignal;
270};
271
Ed Tanous1abe55e2018-09-05 08:30:59 -0700272struct UserSession
273{
274 std::string uniqueId;
275 std::string sessionToken;
276 std::string username;
Ratan Gupta12c04ef2019-04-03 10:08:11 +0530277 std::string userRole;
Ed Tanous1abe55e2018-09-05 08:30:59 -0700278 std::string csrfToken;
279 std::chrono::time_point<std::chrono::steady_clock> lastUpdated;
280 PersistenceType persistence;
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +0100281
Ed Tanous1abe55e2018-09-05 08:30:59 -0700282 /**
283 * @brief Fills object with data from UserSession's JSON representation
284 *
285 * This replaces nlohmann's from_json to ensure no-throw approach
286 *
287 * @param[in] j JSON object from which data should be loaded
288 *
289 * @return a shared pointer if data has been loaded properly, nullptr
290 * otherwise
291 */
292 static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j)
293 {
294 std::shared_ptr<UserSession> userSession =
295 std::make_shared<UserSession>();
296 for (const auto& element : j.items())
297 {
298 const std::string* thisValue =
299 element.value().get_ptr<const std::string*>();
300 if (thisValue == nullptr)
301 {
302 BMCWEB_LOG_ERROR << "Error reading persistent store. Property "
303 << element.key() << " was not of type string";
304 return nullptr;
305 }
306 if (element.key() == "unique_id")
307 {
308 userSession->uniqueId = *thisValue;
309 }
310 else if (element.key() == "session_token")
311 {
312 userSession->sessionToken = *thisValue;
313 }
314 else if (element.key() == "csrf_token")
315 {
316 userSession->csrfToken = *thisValue;
317 }
318 else if (element.key() == "username")
319 {
320 userSession->username = *thisValue;
321 }
322 else
323 {
324 BMCWEB_LOG_ERROR
325 << "Got unexpected property reading persistent file: "
326 << element.key();
327 return nullptr;
328 }
329 }
330
331 // For now, sessions that were persisted through a reboot get their idle
332 // timer reset. This could probably be overcome with a better
333 // understanding of wall clock time and steady timer time, possibly
334 // persisting values with wall clock time instead of steady timer, but
335 // the tradeoffs of all the corner cases involved are non-trivial, so
336 // this is done temporarily
337 userSession->lastUpdated = std::chrono::steady_clock::now();
338 userSession->persistence = PersistenceType::TIMEOUT;
339
340 return userSession;
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +0100341 }
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100342};
343
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100344class Middleware;
345
Ed Tanous1abe55e2018-09-05 08:30:59 -0700346class SessionStore
347{
348 public:
349 std::shared_ptr<UserSession> generateUserSession(
Ed Tanous39e77502019-03-04 17:35:53 -0800350 const std::string_view username,
Ed Tanous1abe55e2018-09-05 08:30:59 -0700351 PersistenceType persistence = PersistenceType::TIMEOUT)
352 {
353 // TODO(ed) find a secure way to not generate session identifiers if
354 // persistence is set to SINGLE_REQUEST
355 static constexpr std::array<char, 62> alphanum = {
356 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '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', 'a', 'b', 'c',
359 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
360 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100361
Ed Tanous1abe55e2018-09-05 08:30:59 -0700362 // entropy: 30 characters, 62 possibilities. log2(62^30) = 178 bits of
363 // entropy. OWASP recommends at least 60
364 // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy
365 std::string sessionToken;
366 sessionToken.resize(20, '0');
Ed Tanousb01bf292019-03-25 19:25:26 +0000367 std::uniform_int_distribution<int> dist(0, alphanum.size() - 1);
368 for (int i = 0; i < sessionToken.size(); ++i)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700369 {
370 sessionToken[i] = alphanum[dist(rd)];
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100371 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700372 // Only need csrf tokens for cookie based auth, token doesn't matter
373 std::string csrfToken;
374 csrfToken.resize(20, '0');
Ed Tanousb01bf292019-03-25 19:25:26 +0000375 for (int i = 0; i < csrfToken.size(); ++i)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700376 {
377 csrfToken[i] = alphanum[dist(rd)];
378 }
379
380 std::string uniqueId;
381 uniqueId.resize(10, '0');
Ed Tanousb01bf292019-03-25 19:25:26 +0000382 for (int i = 0; i < uniqueId.size(); ++i)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700383 {
384 uniqueId[i] = alphanum[dist(rd)];
385 }
Ratan Gupta12c04ef2019-04-03 10:08:11 +0530386
387 // Get the User Privilege
388 const std::string& role =
389 UserRoleMap::getInstance().getUserRole(username);
390
391 BMCWEB_LOG_DEBUG << "user name=\"" << username << "\" role = " << role;
Ed Tanous1abe55e2018-09-05 08:30:59 -0700392 auto session = std::make_shared<UserSession>(UserSession{
Ratan Gupta12c04ef2019-04-03 10:08:11 +0530393 uniqueId, sessionToken, std::string(username), role, csrfToken,
Ed Tanous1abe55e2018-09-05 08:30:59 -0700394 std::chrono::steady_clock::now(), persistence});
395 auto it = authTokens.emplace(std::make_pair(sessionToken, session));
396 // Only need to write to disk if session isn't about to be destroyed.
397 needWrite = persistence == PersistenceType::TIMEOUT;
398 return it.first->second;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100399 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700400
401 std::shared_ptr<UserSession>
Ed Tanous39e77502019-03-04 17:35:53 -0800402 loginSessionByToken(const std::string_view token)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700403 {
404 applySessionTimeouts();
405 auto sessionIt = authTokens.find(std::string(token));
406 if (sessionIt == authTokens.end())
407 {
408 return nullptr;
409 }
410 std::shared_ptr<UserSession> userSession = sessionIt->second;
411 userSession->lastUpdated = std::chrono::steady_clock::now();
412 return userSession;
413 }
414
Ed Tanous39e77502019-03-04 17:35:53 -0800415 std::shared_ptr<UserSession> getSessionByUid(const std::string_view uid)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700416 {
417 applySessionTimeouts();
418 // TODO(Ed) this is inefficient
419 auto sessionIt = authTokens.begin();
420 while (sessionIt != authTokens.end())
421 {
422 if (sessionIt->second->uniqueId == uid)
423 {
424 return sessionIt->second;
425 }
426 sessionIt++;
427 }
428 return nullptr;
429 }
430
431 void removeSession(std::shared_ptr<UserSession> session)
432 {
433 authTokens.erase(session->sessionToken);
434 needWrite = true;
435 }
436
437 std::vector<const std::string*> getUniqueIds(
438 bool getAll = true,
439 const PersistenceType& type = PersistenceType::SINGLE_REQUEST)
440 {
441 applySessionTimeouts();
442
443 std::vector<const std::string*> ret;
444 ret.reserve(authTokens.size());
445 for (auto& session : authTokens)
446 {
447 if (getAll || type == session.second->persistence)
448 {
449 ret.push_back(&session.second->uniqueId);
450 }
451 }
452 return ret;
453 }
454
455 bool needsWrite()
456 {
457 return needWrite;
458 }
Ed Tanousb01bf292019-03-25 19:25:26 +0000459 int getTimeoutInSeconds() const
Ed Tanous1abe55e2018-09-05 08:30:59 -0700460 {
461 return std::chrono::seconds(timeoutInMinutes).count();
462 };
463
464 // Persistent data middleware needs to be able to serialize our authTokens
465 // structure, which is private
466 friend Middleware;
467
468 static SessionStore& getInstance()
469 {
470 static SessionStore sessionStore;
471 return sessionStore;
472 }
473
474 SessionStore(const SessionStore&) = delete;
475 SessionStore& operator=(const SessionStore&) = delete;
476
477 private:
478 SessionStore() : timeoutInMinutes(60)
479 {
480 }
481
482 void applySessionTimeouts()
483 {
484 auto timeNow = std::chrono::steady_clock::now();
485 if (timeNow - lastTimeoutUpdate > std::chrono::minutes(1))
486 {
487 lastTimeoutUpdate = timeNow;
488 auto authTokensIt = authTokens.begin();
489 while (authTokensIt != authTokens.end())
490 {
491 if (timeNow - authTokensIt->second->lastUpdated >=
492 timeoutInMinutes)
493 {
494 authTokensIt = authTokens.erase(authTokensIt);
495 needWrite = true;
496 }
497 else
498 {
499 authTokensIt++;
500 }
501 }
502 }
503 }
Ratan Gupta12c04ef2019-04-03 10:08:11 +0530504
Ed Tanous1abe55e2018-09-05 08:30:59 -0700505 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
506 boost::container::flat_map<std::string, std::shared_ptr<UserSession>>
507 authTokens;
508 std::random_device rd;
509 bool needWrite{false};
510 std::chrono::minutes timeoutInMinutes;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100511};
512
Ed Tanous1abe55e2018-09-05 08:30:59 -0700513} // namespace persistent_data
514} // namespace crow
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200515
516// to_json(...) definition for objects of UserSession type
Ed Tanous1abe55e2018-09-05 08:30:59 -0700517namespace nlohmann
518{
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200519template <>
Ed Tanous1abe55e2018-09-05 08:30:59 -0700520struct adl_serializer<std::shared_ptr<crow::persistent_data::UserSession>>
521{
522 static void
523 to_json(nlohmann::json& j,
524 const std::shared_ptr<crow::persistent_data::UserSession>& p)
525 {
526 if (p->persistence !=
527 crow::persistent_data::PersistenceType::SINGLE_REQUEST)
528 {
529 j = nlohmann::json{{"unique_id", p->uniqueId},
530 {"session_token", p->sessionToken},
531 {"username", p->username},
532 {"csrf_token", p->csrfToken}};
533 }
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200534 }
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200535};
Ed Tanous1abe55e2018-09-05 08:30:59 -0700536} // namespace nlohmann