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