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