xref: /openbmc/bmcweb/redfish-core/lib/redfish_sessions.hpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
4 #pragma once
5 
6 #include "account_service.hpp"
7 #include "app.hpp"
8 #include "cookies.hpp"
9 #include "error_messages.hpp"
10 #include "http/utility.hpp"
11 #include "persistent_data.hpp"
12 #include "query.hpp"
13 #include "registries/privilege_registry.hpp"
14 #include "utils/json_utils.hpp"
15 
16 #include <boost/url/format.hpp>
17 
18 #include <string>
19 #include <vector>
20 
21 namespace redfish
22 {
23 
24 inline void fillSessionObject(crow::Response& res,
25                               const persistent_data::UserSession& session)
26 {
27     res.jsonValue["Id"] = session.uniqueId;
28     res.jsonValue["UserName"] = session.username;
29     nlohmann::json::array_t roles;
30     roles.emplace_back(redfish::getRoleIdFromPrivilege(session.userRole));
31     res.jsonValue["Roles"] = std::move(roles);
32     res.jsonValue["@odata.id"] = boost::urls::format(
33         "/redfish/v1/SessionService/Sessions/{}", session.uniqueId);
34     res.jsonValue["@odata.type"] = "#Session.v1_7_0.Session";
35     res.jsonValue["Name"] = "User Session";
36     res.jsonValue["Description"] = "Manager User Session";
37     res.jsonValue["ClientOriginIPAddress"] = session.clientIp;
38     if (session.clientId)
39     {
40         res.jsonValue["Context"] = *session.clientId;
41     }
42 }
43 
44 inline void
45     handleSessionHead(crow::App& app, const crow::Request& req,
46                       const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
47                       const std::string& /*sessionId*/)
48 {
49     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
50     {
51         return;
52     }
53     asyncResp->res.addHeader(
54         boost::beast::http::field::link,
55         "</redfish/v1/JsonSchemas/Session/Session.json>; rel=describedby");
56 }
57 
58 inline void
59     handleSessionGet(crow::App& app, const crow::Request& req,
60                      const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
61                      const std::string& sessionId)
62 {
63     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
64     {
65         return;
66     }
67     asyncResp->res.addHeader(
68         boost::beast::http::field::link,
69         "</redfish/v1/JsonSchemas/Session/Session.json>; rel=describedby");
70 
71     // Note that control also reaches here via doPost and doDelete.
72     auto session =
73         persistent_data::SessionStore::getInstance().getSessionByUid(sessionId);
74 
75     if (session == nullptr)
76     {
77         messages::resourceNotFound(asyncResp->res, "Session", sessionId);
78         return;
79     }
80 
81     fillSessionObject(asyncResp->res, *session);
82 }
83 
84 inline void
85     handleSessionDelete(crow::App& app, const crow::Request& req,
86                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
87                         const std::string& sessionId)
88 {
89     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
90     {
91         return;
92     }
93     auto session =
94         persistent_data::SessionStore::getInstance().getSessionByUid(sessionId);
95 
96     if (session == nullptr)
97     {
98         messages::resourceNotFound(asyncResp->res, "Session", sessionId);
99         return;
100     }
101 
102     // Perform a proper ConfigureSelf authority check.  If a
103     // session is being used to DELETE some other user's session,
104     // then the ConfigureSelf privilege does not apply.  In that
105     // case, perform the authority check again without the user's
106     // ConfigureSelf privilege.
107     if (req.session != nullptr && !session->username.empty() &&
108         session->username != req.session->username)
109     {
110         Privileges effectiveUserPrivileges =
111             redfish::getUserPrivileges(*req.session);
112 
113         if (!effectiveUserPrivileges.isSupersetOf({"ConfigureUsers"}))
114         {
115             messages::insufficientPrivilege(asyncResp->res);
116             return;
117         }
118     }
119 
120     if (req.session != nullptr && req.session->uniqueId == sessionId &&
121         session->cookieAuth)
122     {
123         bmcweb::clearSessionCookies(asyncResp->res);
124     }
125 
126     persistent_data::SessionStore::getInstance().removeSession(session);
127     messages::success(asyncResp->res);
128 }
129 
130 inline nlohmann::json getSessionCollectionMembers()
131 {
132     std::vector<std::string> sessionIds =
133         persistent_data::SessionStore::getInstance().getAllUniqueIds();
134     nlohmann::json ret = nlohmann::json::array();
135     for (const std::string& uid : sessionIds)
136     {
137         nlohmann::json::object_t session;
138         session["@odata.id"] =
139             boost::urls::format("/redfish/v1/SessionService/Sessions/{}", uid);
140         ret.emplace_back(std::move(session));
141     }
142     return ret;
143 }
144 
145 inline void handleSessionCollectionHead(
146     crow::App& app, const crow::Request& req,
147     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
148 {
149     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
150     {
151         return;
152     }
153     asyncResp->res.addHeader(
154         boost::beast::http::field::link,
155         "</redfish/v1/JsonSchemas/SessionCollection.json>; rel=describedby");
156 }
157 
158 inline void handleSessionCollectionGet(
159     crow::App& app, const crow::Request& req,
160     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
161 {
162     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
163     {
164         return;
165     }
166     asyncResp->res.addHeader(
167         boost::beast::http::field::link,
168         "</redfish/v1/JsonSchemas/SessionCollection.json>; rel=describedby");
169 
170     asyncResp->res.jsonValue["Members"] = getSessionCollectionMembers();
171     asyncResp->res.jsonValue["Members@odata.count"] =
172         asyncResp->res.jsonValue["Members"].size();
173     asyncResp->res.jsonValue["@odata.type"] =
174         "#SessionCollection.SessionCollection";
175     asyncResp->res.jsonValue["@odata.id"] =
176         "/redfish/v1/SessionService/Sessions";
177     asyncResp->res.jsonValue["Name"] = "Session Collection";
178     asyncResp->res.jsonValue["Description"] = "Session Collection";
179 }
180 
181 inline void handleSessionCollectionMembersGet(
182     crow::App& app, const crow::Request& req,
183     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
184 {
185     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
186     {
187         return;
188     }
189     asyncResp->res.jsonValue = getSessionCollectionMembers();
190 }
191 
192 inline void processAfterSessionCreation(
193     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
194     const crow::Request& req, const std::string& username,
195     std::shared_ptr<persistent_data::UserSession>& session)
196 {
197     // When session is created by webui-vue give it session cookies as a
198     // non-standard Redfish extension. This is needed for authentication for
199     // WebSockets-based functionality.
200     if (!req.getHeaderValue("X-Requested-With").empty())
201     {
202         bmcweb::setSessionCookies(asyncResp->res, *session);
203     }
204     else
205     {
206         asyncResp->res.addHeader("X-Auth-Token", session->sessionToken);
207     }
208 
209     asyncResp->res.addHeader(
210         "Location", "/redfish/v1/SessionService/Sessions/" + session->uniqueId);
211     asyncResp->res.result(boost::beast::http::status::created);
212     if (session->isConfigureSelfOnly)
213     {
214         messages::passwordChangeRequired(
215             asyncResp->res,
216             boost::urls::format("/redfish/v1/AccountService/Accounts/{}",
217                                 session->username));
218     }
219 
220     crow::getUserInfo(asyncResp, username, session, [asyncResp, session]() {
221         fillSessionObject(asyncResp->res, *session);
222     });
223 }
224 
225 inline void handleSessionCollectionPost(
226     crow::App& app, const crow::Request& req,
227     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
228 {
229     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
230     {
231         return;
232     }
233     std::string username;
234     std::string password;
235     std::optional<std::string> clientId;
236     std::optional<std::string> token;
237     if (!json_util::readJsonPatch( //
238             req, asyncResp->res, //
239             "Context", clientId, //
240             "Password", password, //
241             "Token", token, //
242             "UserName", username //
243             ))
244     {
245         return;
246     }
247     if (password.empty() || username.empty() ||
248         asyncResp->res.result() != boost::beast::http::status::ok)
249     {
250         if (username.empty())
251         {
252             messages::propertyMissing(asyncResp->res, "UserName");
253         }
254 
255         if (password.empty())
256         {
257             messages::propertyMissing(asyncResp->res, "Password");
258         }
259 
260         return;
261     }
262 
263     int pamrc = pamAuthenticateUser(username, password, token);
264     bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
265     if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
266     {
267         messages::resourceAtUriUnauthorized(asyncResp->res, req.url(),
268                                             "Invalid username or password");
269         return;
270     }
271 
272     // User is authenticated - create session
273     std::shared_ptr<persistent_data::UserSession> session =
274         persistent_data::SessionStore::getInstance().generateUserSession(
275             username, req.ipAddress, clientId,
276             persistent_data::SessionType::Session, isConfigureSelfOnly);
277     if (session == nullptr)
278     {
279         messages::internalError(asyncResp->res);
280         return;
281     }
282     processAfterSessionCreation(asyncResp, req, username, session);
283 }
284 
285 inline void handleSessionServiceHead(
286     crow::App& app, const crow::Request& req,
287     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
288 {
289     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
290     {
291         return;
292     }
293     asyncResp->res.addHeader(
294         boost::beast::http::field::link,
295         "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby");
296 }
297 inline void
298     handleSessionServiceGet(crow::App& app, const crow::Request& req,
299                             const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
300 
301 {
302     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
303     {
304         return;
305     }
306     asyncResp->res.addHeader(
307         boost::beast::http::field::link,
308         "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby");
309 
310     asyncResp->res.jsonValue["@odata.type"] =
311         "#SessionService.v1_0_2.SessionService";
312     asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/SessionService";
313     asyncResp->res.jsonValue["Name"] = "Session Service";
314     asyncResp->res.jsonValue["Id"] = "SessionService";
315     asyncResp->res.jsonValue["Description"] = "Session Service";
316     asyncResp->res.jsonValue["SessionTimeout"] =
317         persistent_data::SessionStore::getInstance().getTimeoutInSeconds();
318     asyncResp->res.jsonValue["ServiceEnabled"] = true;
319 
320     asyncResp->res.jsonValue["Sessions"]["@odata.id"] =
321         "/redfish/v1/SessionService/Sessions";
322 }
323 
324 inline void handleSessionServicePatch(
325     crow::App& app, const crow::Request& req,
326     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
327 {
328     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
329     {
330         return;
331     }
332     std::optional<int64_t> sessionTimeout;
333     if (!json_util::readJsonPatch( //
334             req, asyncResp->res, //
335             "SessionTimeout", sessionTimeout //
336             ))
337     {
338         return;
339     }
340 
341     if (sessionTimeout)
342     {
343         // The minimum & maximum allowed values for session timeout
344         // are 30 seconds and 86400 seconds respectively as per the
345         // session service schema mentioned at
346         // https://redfish.dmtf.org/schemas/v1/SessionService.v1_1_7.json
347 
348         if (*sessionTimeout <= 86400 && *sessionTimeout >= 30)
349         {
350             std::chrono::seconds sessionTimeoutInseconds(*sessionTimeout);
351             persistent_data::SessionStore::getInstance().updateSessionTimeout(
352                 sessionTimeoutInseconds);
353             messages::propertyValueModified(asyncResp->res, "SessionTimeOut",
354                                             std::to_string(*sessionTimeout));
355         }
356         else
357         {
358             messages::propertyValueNotInList(asyncResp->res, *sessionTimeout,
359                                              "SessionTimeOut");
360         }
361     }
362 }
363 
364 inline void requestRoutesSession(App& app)
365 {
366     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
367         .privileges(redfish::privileges::headSession)
368         .methods(boost::beast::http::verb::head)(
369             std::bind_front(handleSessionHead, std::ref(app)));
370 
371     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
372         .privileges(redfish::privileges::getSession)
373         .methods(boost::beast::http::verb::get)(
374             std::bind_front(handleSessionGet, std::ref(app)));
375 
376     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
377         .privileges(redfish::privileges::deleteSession)
378         .methods(boost::beast::http::verb::delete_)(
379             std::bind_front(handleSessionDelete, std::ref(app)));
380 
381     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
382         .privileges(redfish::privileges::headSessionCollection)
383         .methods(boost::beast::http::verb::head)(
384             std::bind_front(handleSessionCollectionHead, std::ref(app)));
385 
386     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
387         .privileges(redfish::privileges::getSessionCollection)
388         .methods(boost::beast::http::verb::get)(
389             std::bind_front(handleSessionCollectionGet, std::ref(app)));
390 
391     // Note, the next two routes technically don't match the privilege
392     // registry given the way login mechanisms work.  The base privilege
393     // registry lists this endpoint as requiring login privilege, but because
394     // this is the endpoint responsible for giving the login privilege, and it
395     // is itself its own route, it needs to not require Login
396     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
397         .privileges({})
398         .methods(boost::beast::http::verb::post)(
399             std::bind_front(handleSessionCollectionPost, std::ref(app)));
400 
401     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/Members/")
402         .privileges({})
403         .methods(boost::beast::http::verb::post)(
404             std::bind_front(handleSessionCollectionPost, std::ref(app)));
405 
406     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
407         .privileges(redfish::privileges::headSessionService)
408         .methods(boost::beast::http::verb::head)(
409             std::bind_front(handleSessionServiceHead, std::ref(app)));
410 
411     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
412         .privileges(redfish::privileges::getSessionService)
413         .methods(boost::beast::http::verb::get)(
414             std::bind_front(handleSessionServiceGet, std::ref(app)));
415 
416     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
417         .privileges(redfish::privileges::patchSessionService)
418         .methods(boost::beast::http::verb::patch)(
419             std::bind_front(handleSessionServicePatch, std::ref(app)));
420 }
421 
422 } // namespace redfish
423