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