blob: ebf63607a467b0fe7f4487bd2aa67ee7cd4dd9cd [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;
Joseph Reynolds8fd315a2019-09-12 12:02:33 -0500279 bool isConfigureSelfOnly;
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +0100280
Ed Tanous1abe55e2018-09-05 08:30:59 -0700281 /**
282 * @brief Fills object with data from UserSession's JSON representation
283 *
284 * This replaces nlohmann's from_json to ensure no-throw approach
285 *
286 * @param[in] j JSON object from which data should be loaded
287 *
288 * @return a shared pointer if data has been loaded properly, nullptr
289 * otherwise
290 */
291 static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j)
292 {
293 std::shared_ptr<UserSession> userSession =
294 std::make_shared<UserSession>();
295 for (const auto& element : j.items())
296 {
297 const std::string* thisValue =
298 element.value().get_ptr<const std::string*>();
299 if (thisValue == nullptr)
300 {
301 BMCWEB_LOG_ERROR << "Error reading persistent store. Property "
302 << element.key() << " was not of type string";
303 return nullptr;
304 }
305 if (element.key() == "unique_id")
306 {
307 userSession->uniqueId = *thisValue;
308 }
309 else if (element.key() == "session_token")
310 {
311 userSession->sessionToken = *thisValue;
312 }
313 else if (element.key() == "csrf_token")
314 {
315 userSession->csrfToken = *thisValue;
316 }
317 else if (element.key() == "username")
318 {
319 userSession->username = *thisValue;
320 }
321 else
322 {
323 BMCWEB_LOG_ERROR
324 << "Got unexpected property reading persistent file: "
325 << element.key();
326 return nullptr;
327 }
328 }
329
330 // For now, sessions that were persisted through a reboot get their idle
331 // timer reset. This could probably be overcome with a better
332 // understanding of wall clock time and steady timer time, possibly
333 // persisting values with wall clock time instead of steady timer, but
334 // the tradeoffs of all the corner cases involved are non-trivial, so
335 // this is done temporarily
336 userSession->lastUpdated = std::chrono::steady_clock::now();
337 userSession->persistence = PersistenceType::TIMEOUT;
Joseph Reynolds8fd315a2019-09-12 12:02:33 -0500338 userSession->isConfigureSelfOnly = false;
Ed Tanous1abe55e2018-09-05 08:30:59 -0700339
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(
Joseph Reynolds8fd315a2019-09-12 12:02:33 -0500350 const std::string_view username, bool configureSelfOnly,
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 = {
Joseph Reynolds368b1d42019-08-15 15:29:06 -0500356 '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',
Ed Tanous1abe55e2018-09-05 08:30:59 -0700359 '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
Ed Tanous1abe55e2018-09-05 08:30:59 -0700387 auto session = std::make_shared<UserSession>(UserSession{
Ed Tanousca0c93b2019-09-19 11:53:50 -0700388 uniqueId, sessionToken, std::string(username), csrfToken,
Ed Tanous1abe55e2018-09-05 08:30:59 -0700389 std::chrono::steady_clock::now(), persistence});
Joseph Reynolds8fd315a2019-09-12 12:02:33 -0500390 session->isConfigureSelfOnly = configureSelfOnly;
Ed Tanous1abe55e2018-09-05 08:30:59 -0700391 auto it = authTokens.emplace(std::make_pair(sessionToken, session));
392 // Only need to write to disk if session isn't about to be destroyed.
393 needWrite = persistence == PersistenceType::TIMEOUT;
394 return it.first->second;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100395 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700396
397 std::shared_ptr<UserSession>
Ed Tanous39e77502019-03-04 17:35:53 -0800398 loginSessionByToken(const std::string_view token)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700399 {
400 applySessionTimeouts();
401 auto sessionIt = authTokens.find(std::string(token));
402 if (sessionIt == authTokens.end())
403 {
404 return nullptr;
405 }
406 std::shared_ptr<UserSession> userSession = sessionIt->second;
407 userSession->lastUpdated = std::chrono::steady_clock::now();
408 return userSession;
409 }
410
Ed Tanous39e77502019-03-04 17:35:53 -0800411 std::shared_ptr<UserSession> getSessionByUid(const std::string_view uid)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700412 {
413 applySessionTimeouts();
414 // TODO(Ed) this is inefficient
415 auto sessionIt = authTokens.begin();
416 while (sessionIt != authTokens.end())
417 {
418 if (sessionIt->second->uniqueId == uid)
419 {
420 return sessionIt->second;
421 }
422 sessionIt++;
423 }
424 return nullptr;
425 }
426
427 void removeSession(std::shared_ptr<UserSession> session)
428 {
429 authTokens.erase(session->sessionToken);
430 needWrite = true;
431 }
432
433 std::vector<const std::string*> getUniqueIds(
434 bool getAll = true,
435 const PersistenceType& type = PersistenceType::SINGLE_REQUEST)
436 {
437 applySessionTimeouts();
438
439 std::vector<const std::string*> ret;
440 ret.reserve(authTokens.size());
441 for (auto& session : authTokens)
442 {
443 if (getAll || type == session.second->persistence)
444 {
445 ret.push_back(&session.second->uniqueId);
446 }
447 }
448 return ret;
449 }
450
451 bool needsWrite()
452 {
453 return needWrite;
454 }
Ed Tanousb01bf292019-03-25 19:25:26 +0000455 int getTimeoutInSeconds() const
Ed Tanous1abe55e2018-09-05 08:30:59 -0700456 {
457 return std::chrono::seconds(timeoutInMinutes).count();
458 };
459
460 // Persistent data middleware needs to be able to serialize our authTokens
461 // structure, which is private
462 friend Middleware;
463
464 static SessionStore& getInstance()
465 {
466 static SessionStore sessionStore;
467 return sessionStore;
468 }
469
470 SessionStore(const SessionStore&) = delete;
471 SessionStore& operator=(const SessionStore&) = delete;
472
473 private:
474 SessionStore() : timeoutInMinutes(60)
475 {
476 }
477
478 void applySessionTimeouts()
479 {
480 auto timeNow = std::chrono::steady_clock::now();
481 if (timeNow - lastTimeoutUpdate > std::chrono::minutes(1))
482 {
483 lastTimeoutUpdate = timeNow;
484 auto authTokensIt = authTokens.begin();
485 while (authTokensIt != authTokens.end())
486 {
487 if (timeNow - authTokensIt->second->lastUpdated >=
488 timeoutInMinutes)
489 {
490 authTokensIt = authTokens.erase(authTokensIt);
491 needWrite = true;
492 }
493 else
494 {
495 authTokensIt++;
496 }
497 }
498 }
499 }
Ratan Gupta12c04ef2019-04-03 10:08:11 +0530500
Ed Tanous1abe55e2018-09-05 08:30:59 -0700501 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
502 boost::container::flat_map<std::string, std::shared_ptr<UserSession>>
503 authTokens;
504 std::random_device rd;
505 bool needWrite{false};
506 std::chrono::minutes timeoutInMinutes;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100507};
508
Ed Tanous1abe55e2018-09-05 08:30:59 -0700509} // namespace persistent_data
510} // namespace crow
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200511
512// to_json(...) definition for objects of UserSession type
Ed Tanous1abe55e2018-09-05 08:30:59 -0700513namespace nlohmann
514{
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200515template <>
Ed Tanous1abe55e2018-09-05 08:30:59 -0700516struct adl_serializer<std::shared_ptr<crow::persistent_data::UserSession>>
517{
518 static void
519 to_json(nlohmann::json& j,
520 const std::shared_ptr<crow::persistent_data::UserSession>& p)
521 {
522 if (p->persistence !=
523 crow::persistent_data::PersistenceType::SINGLE_REQUEST)
524 {
525 j = nlohmann::json{{"unique_id", p->uniqueId},
526 {"session_token", p->sessionToken},
527 {"username", p->username},
528 {"csrf_token", p->csrfToken}};
529 }
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200530 }
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200531};
Ed Tanous1abe55e2018-09-05 08:30:59 -0700532} // namespace nlohmann