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