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 (session->cookieAuth)
133     {
134         bmcweb::clearSessionCookies(asyncResp->res);
135     }
136 
137     persistent_data::SessionStore::getInstance().removeSession(session);
138     messages::success(asyncResp->res);
139 }
140 
getSessionCollectionMembers()141 inline nlohmann::json getSessionCollectionMembers()
142 {
143     std::vector<std::string> sessionIds =
144         persistent_data::SessionStore::getInstance().getAllUniqueIds();
145     nlohmann::json ret = nlohmann::json::array();
146     for (const std::string& uid : sessionIds)
147     {
148         nlohmann::json::object_t session;
149         session["@odata.id"] =
150             boost::urls::format("/redfish/v1/SessionService/Sessions/{}", uid);
151         ret.emplace_back(std::move(session));
152     }
153     return ret;
154 }
155 
handleSessionCollectionHead(crow::App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)156 inline void handleSessionCollectionHead(
157     crow::App& app, const crow::Request& req,
158     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
159 {
160     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
161     {
162         return;
163     }
164     asyncResp->res.addHeader(
165         boost::beast::http::field::link,
166         "</redfish/v1/JsonSchemas/SessionCollection.json>; rel=describedby");
167 }
168 
handleSessionCollectionGet(crow::App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)169 inline void handleSessionCollectionGet(
170     crow::App& app, const crow::Request& req,
171     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
172 {
173     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
174     {
175         return;
176     }
177     asyncResp->res.addHeader(
178         boost::beast::http::field::link,
179         "</redfish/v1/JsonSchemas/SessionCollection.json>; rel=describedby");
180 
181     asyncResp->res.jsonValue["Members"] = getSessionCollectionMembers();
182     asyncResp->res.jsonValue["Members@odata.count"] =
183         asyncResp->res.jsonValue["Members"].size();
184     asyncResp->res.jsonValue["@odata.type"] =
185         "#SessionCollection.SessionCollection";
186     asyncResp->res.jsonValue["@odata.id"] =
187         "/redfish/v1/SessionService/Sessions";
188     asyncResp->res.jsonValue["Name"] = "Session Collection";
189     asyncResp->res.jsonValue["Description"] = "Session Collection";
190 }
191 
handleSessionCollectionMembersGet(crow::App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)192 inline void handleSessionCollectionMembersGet(
193     crow::App& app, const crow::Request& req,
194     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
195 {
196     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
197     {
198         return;
199     }
200     asyncResp->res.jsonValue = getSessionCollectionMembers();
201 }
202 
handleSessionCollectionPost(crow::App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)203 inline void handleSessionCollectionPost(
204     crow::App& app, const crow::Request& req,
205     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
206 {
207     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
208     {
209         return;
210     }
211     std::string username;
212     std::string password;
213     std::optional<std::string> clientId;
214     if (!json_util::readJsonPatch(req, asyncResp->res, "UserName", username,
215                                   "Password", password, "Context", clientId))
216     {
217         return;
218     }
219 
220     if (password.empty() || username.empty() ||
221         asyncResp->res.result() != boost::beast::http::status::ok)
222     {
223         if (username.empty())
224         {
225             messages::propertyMissing(asyncResp->res, "UserName");
226         }
227 
228         if (password.empty())
229         {
230             messages::propertyMissing(asyncResp->res, "Password");
231         }
232 
233         return;
234     }
235 
236     int pamrc = pamAuthenticateUser(username, password);
237     bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
238     if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
239     {
240         messages::resourceAtUriUnauthorized(asyncResp->res, req.url(),
241                                             "Invalid username or password");
242         return;
243     }
244 
245     // User is authenticated - create session
246     std::shared_ptr<persistent_data::UserSession> session =
247         persistent_data::SessionStore::getInstance().generateUserSession(
248             username, req.ipAddress, clientId,
249             persistent_data::SessionType::Session, isConfigureSelfOnly);
250     if (session == nullptr)
251     {
252         messages::internalError(asyncResp->res);
253         return;
254     }
255 
256     // When session is created by webui-vue give it session cookies as a
257     // non-standard Redfish extension. This is needed for authentication for
258     // WebSockets-based functionality.
259     if (!req.getHeaderValue("X-Requested-With").empty())
260     {
261         bmcweb::setSessionCookies(asyncResp->res, *session);
262     }
263     else
264     {
265         asyncResp->res.addHeader("X-Auth-Token", session->sessionToken);
266     }
267 
268     asyncResp->res.addHeader(
269         "Location", "/redfish/v1/SessionService/Sessions/" + session->uniqueId);
270     asyncResp->res.result(boost::beast::http::status::created);
271     if (session->isConfigureSelfOnly)
272     {
273         messages::passwordChangeRequired(
274             asyncResp->res,
275             boost::urls::format("/redfish/v1/AccountService/Accounts/{}",
276                                 session->username));
277     }
278 
279     crow::getUserInfo(asyncResp, username, session, [asyncResp, session]() {
280         fillSessionObject(asyncResp->res, *session);
281     });
282 }
handleSessionServiceHead(crow::App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)283 inline void handleSessionServiceHead(
284     crow::App& app, const crow::Request& req,
285     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
286 {
287     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
288     {
289         return;
290     }
291     asyncResp->res.addHeader(
292         boost::beast::http::field::link,
293         "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby");
294 }
295 inline void
handleSessionServiceGet(crow::App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)296     handleSessionServiceGet(crow::App& app, const crow::Request& req,
297                             const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
298 
299 {
300     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
301     {
302         return;
303     }
304     asyncResp->res.addHeader(
305         boost::beast::http::field::link,
306         "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby");
307 
308     asyncResp->res.jsonValue["@odata.type"] =
309         "#SessionService.v1_0_2.SessionService";
310     asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/SessionService";
311     asyncResp->res.jsonValue["Name"] = "Session Service";
312     asyncResp->res.jsonValue["Id"] = "SessionService";
313     asyncResp->res.jsonValue["Description"] = "Session Service";
314     asyncResp->res.jsonValue["SessionTimeout"] =
315         persistent_data::SessionStore::getInstance().getTimeoutInSeconds();
316     asyncResp->res.jsonValue["ServiceEnabled"] = true;
317 
318     asyncResp->res.jsonValue["Sessions"]["@odata.id"] =
319         "/redfish/v1/SessionService/Sessions";
320 }
321 
handleSessionServicePatch(crow::App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)322 inline void handleSessionServicePatch(
323     crow::App& app, const crow::Request& req,
324     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
325 {
326     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
327     {
328         return;
329     }
330     std::optional<int64_t> sessionTimeout;
331     if (!json_util::readJsonPatch(req, asyncResp->res, "SessionTimeout",
332                                   sessionTimeout))
333     {
334         return;
335     }
336 
337     if (sessionTimeout)
338     {
339         // The minimum & maximum allowed values for session timeout
340         // are 30 seconds and 86400 seconds respectively as per the
341         // session service schema mentioned at
342         // https://redfish.dmtf.org/schemas/v1/SessionService.v1_1_7.json
343 
344         if (*sessionTimeout <= 86400 && *sessionTimeout >= 30)
345         {
346             std::chrono::seconds sessionTimeoutInseconds(*sessionTimeout);
347             persistent_data::SessionStore::getInstance().updateSessionTimeout(
348                 sessionTimeoutInseconds);
349             messages::propertyValueModified(asyncResp->res, "SessionTimeOut",
350                                             std::to_string(*sessionTimeout));
351         }
352         else
353         {
354             messages::propertyValueNotInList(asyncResp->res, *sessionTimeout,
355                                              "SessionTimeOut");
356         }
357     }
358 }
359 
requestRoutesSession(App & app)360 inline void requestRoutesSession(App& app)
361 {
362     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
363         .privileges(redfish::privileges::headSession)
364         .methods(boost::beast::http::verb::head)(
365             std::bind_front(handleSessionHead, std::ref(app)));
366 
367     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
368         .privileges(redfish::privileges::getSession)
369         .methods(boost::beast::http::verb::get)(
370             std::bind_front(handleSessionGet, std::ref(app)));
371 
372     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
373         .privileges(redfish::privileges::deleteSession)
374         .methods(boost::beast::http::verb::delete_)(
375             std::bind_front(handleSessionDelete, std::ref(app)));
376 
377     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
378         .privileges(redfish::privileges::headSessionCollection)
379         .methods(boost::beast::http::verb::head)(
380             std::bind_front(handleSessionCollectionHead, std::ref(app)));
381 
382     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
383         .privileges(redfish::privileges::getSessionCollection)
384         .methods(boost::beast::http::verb::get)(
385             std::bind_front(handleSessionCollectionGet, std::ref(app)));
386 
387     // Note, the next two routes technically don't match the privilege
388     // registry given the way login mechanisms work.  The base privilege
389     // registry lists this endpoint as requiring login privilege, but because
390     // this is the endpoint responsible for giving the login privilege, and it
391     // is itself its own route, it needs to not require Login
392     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
393         .privileges({})
394         .methods(boost::beast::http::verb::post)(
395             std::bind_front(handleSessionCollectionPost, std::ref(app)));
396 
397     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/Members/")
398         .privileges({})
399         .methods(boost::beast::http::verb::post)(
400             std::bind_front(handleSessionCollectionPost, std::ref(app)));
401 
402     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
403         .privileges(redfish::privileges::headSessionService)
404         .methods(boost::beast::http::verb::head)(
405             std::bind_front(handleSessionServiceHead, std::ref(app)));
406 
407     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
408         .privileges(redfish::privileges::getSessionService)
409         .methods(boost::beast::http::verb::get)(
410             std::bind_front(handleSessionServiceGet, std::ref(app)));
411 
412     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
413         .privileges(redfish::privileges::patchSessionService)
414         .methods(boost::beast::http::verb::patch)(
415             std::bind_front(handleSessionServicePatch, std::ref(app)));
416 }
417 
418 } // namespace redfish
419