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