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