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