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