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 "error_messages.hpp"
19 #include "persistent_data.hpp"
20 
21 #include <app.hpp>
22 #include <http/utility.hpp>
23 #include <query.hpp>
24 #include <registries/privilege_registry.hpp>
25 #include <utils/json_utils.hpp>
26 
27 namespace redfish
28 {
29 
30 inline void fillSessionObject(crow::Response& res,
31                               const persistent_data::UserSession& session)
32 {
33     res.jsonValue["Id"] = session.uniqueId;
34     res.jsonValue["UserName"] = session.username;
35     res.jsonValue["@odata.id"] =
36         "/redfish/v1/SessionService/Sessions/" + session.uniqueId;
37     res.jsonValue["@odata.type"] = "#Session.v1_5_0.Session";
38     res.jsonValue["Name"] = "User Session";
39     res.jsonValue["Description"] = "Manager User Session";
40     res.jsonValue["ClientOriginIPAddress"] = session.clientIp;
41     if (session.clientId)
42     {
43         res.jsonValue["Context"] = *session.clientId;
44     }
45 // The below implementation is deprecated in leiu of Session.Context
46 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
47     res.jsonValue["Oem"]["OpenBMC"]["@odata.type"] =
48         "#OemSession.v1_0_0.Session";
49     res.jsonValue["Oem"]["OpenBMC"]["ClientID"] = session.clientId.value_or("");
50 #endif
51 }
52 
53 inline void
54     handleSessionHead(crow::App& app, const crow::Request& req,
55                       const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
56                       const std::string& /*sessionId*/)
57 {
58 
59     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
60     {
61         return;
62     }
63     asyncResp->res.addHeader(
64         boost::beast::http::field::link,
65         "</redfish/v1/JsonSchemas/Session/Session.json>; rel=describedby");
66 }
67 
68 inline void
69     handleSessionGet(crow::App& app, const crow::Request& req,
70                      const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
71                      const std::string& sessionId)
72 {
73     handleSessionHead(app, req, asyncResp, sessionId);
74 
75     // Note that control also reaches here via doPost and doDelete.
76     auto session =
77         persistent_data::SessionStore::getInstance().getSessionByUid(sessionId);
78 
79     if (session == nullptr)
80     {
81         messages::resourceNotFound(asyncResp->res, "Session", sessionId);
82         return;
83     }
84 
85     fillSessionObject(asyncResp->res, *session);
86 }
87 
88 inline void
89     handleSessionDelete(crow::App& app, const crow::Request& req,
90                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
91                         const std::string& sessionId)
92 {
93     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
94     {
95         return;
96     }
97     auto session =
98         persistent_data::SessionStore::getInstance().getSessionByUid(sessionId);
99 
100     if (session == nullptr)
101     {
102         messages::resourceNotFound(asyncResp->res, "Session", sessionId);
103         return;
104     }
105 
106     // Perform a proper ConfigureSelf authority check.  If a
107     // session is being used to DELETE some other user's session,
108     // then the ConfigureSelf privilege does not apply.  In that
109     // case, perform the authority check again without the user's
110     // ConfigureSelf privilege.
111     if (req.session != nullptr && !session->username.empty() &&
112         session->username != req.session->username)
113     {
114         Privileges effectiveUserPrivileges =
115             redfish::getUserPrivileges(req.userRole);
116 
117         if (!effectiveUserPrivileges.isSupersetOf({"ConfigureUsers"}))
118         {
119             messages::insufficientPrivilege(asyncResp->res);
120             return;
121         }
122     }
123 
124     persistent_data::SessionStore::getInstance().removeSession(session);
125     messages::success(asyncResp->res);
126 }
127 
128 inline nlohmann::json getSessionCollectionMembers()
129 {
130     std::vector<const std::string*> sessionIds =
131         persistent_data::SessionStore::getInstance().getUniqueIds(
132             false, persistent_data::PersistenceType::TIMEOUT);
133     nlohmann::json ret = nlohmann::json::array();
134     for (const std::string* uid : sessionIds)
135     {
136         nlohmann::json::object_t session;
137         session["@odata.id"] = "/redfish/v1/SessionService/Sessions/" + *uid;
138         ret.push_back(std::move(session));
139     }
140     return ret;
141 }
142 
143 inline void handleSessionCollectionHead(
144     crow::App& app, const crow::Request& req,
145     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
146 {
147 
148     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
149     {
150         return;
151     }
152     asyncResp->res.addHeader(
153         boost::beast::http::field::link,
154         "</redfish/v1/JsonSchemas/SessionCollection.json>; rel=describedby");
155 }
156 
157 inline void handleSessionCollectionGet(
158     crow::App& app, const crow::Request& req,
159     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
160 {
161     handleSessionCollectionHead(app, req, asyncResp);
162     asyncResp->res.jsonValue["Members"] = getSessionCollectionMembers();
163     asyncResp->res.jsonValue["Members@odata.count"] =
164         asyncResp->res.jsonValue["Members"].size();
165     asyncResp->res.jsonValue["@odata.type"] =
166         "#SessionCollection.SessionCollection";
167     asyncResp->res.jsonValue["@odata.id"] =
168         "/redfish/v1/SessionService/Sessions/";
169     asyncResp->res.jsonValue["Name"] = "Session Collection";
170     asyncResp->res.jsonValue["Description"] = "Session Collection";
171 }
172 
173 inline void handleSessionCollectionMembersGet(
174     crow::App& app, const crow::Request& req,
175     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
176 {
177     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
178     {
179         return;
180     }
181     asyncResp->res.jsonValue = getSessionCollectionMembers();
182 }
183 
184 inline void handleSessionCollectionPost(
185     crow::App& app, const crow::Request& req,
186     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
187 {
188     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
189     {
190         return;
191     }
192     std::string username;
193     std::string password;
194     std::optional<nlohmann::json> oemObject;
195     std::optional<std::string> clientId;
196     if (!json_util::readJsonPatch(req, asyncResp->res, "UserName", username,
197                                   "Password", password, "Context", clientId,
198                                   "Oem", oemObject))
199     {
200         return;
201     }
202 
203     if (password.empty() || username.empty() ||
204         asyncResp->res.result() != boost::beast::http::status::ok)
205     {
206         if (username.empty())
207         {
208             messages::propertyMissing(asyncResp->res, "UserName");
209         }
210 
211         if (password.empty())
212         {
213             messages::propertyMissing(asyncResp->res, "Password");
214         }
215 
216         return;
217     }
218 
219     int pamrc = pamAuthenticateUser(username, password);
220     bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
221     if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
222     {
223         messages::resourceAtUriUnauthorized(asyncResp->res, req.urlView,
224                                             "Invalid username or password");
225         return;
226     }
227 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
228     if (oemObject)
229     {
230         std::optional<nlohmann::json> bmcOem;
231         if (!json_util::readJson(*oemObject, asyncResp->res, "OpenBMC", bmcOem))
232         {
233             return;
234         }
235 
236         std::optional<std::string> oemClientId;
237         if (!json_util::readJson(*bmcOem, asyncResp->res, "ClientID",
238                                  oemClientId))
239         {
240             BMCWEB_LOG_ERROR << "Could not read ClientId";
241             return;
242         }
243         if (oemClientId)
244         {
245             if (clientId)
246             {
247                 messages::propertyValueConflict(*oemClientId, *clientId);
248                 return;
249             }
250             clientId = *oemClientId;
251         }
252     }
253 #endif
254 
255     // User is authenticated - create session
256     std::shared_ptr<persistent_data::UserSession> session =
257         persistent_data::SessionStore::getInstance().generateUserSession(
258             username, req.ipAddress, clientId,
259             persistent_data::PersistenceType::TIMEOUT, isConfigureSelfOnly);
260     if (session == nullptr)
261     {
262         messages::internalError(asyncResp->res);
263         return;
264     }
265 
266     asyncResp->res.addHeader("X-Auth-Token", session->sessionToken);
267     asyncResp->res.addHeader(
268         "Location", "/redfish/v1/SessionService/Sessions/" + session->uniqueId);
269     asyncResp->res.result(boost::beast::http::status::created);
270     if (session->isConfigureSelfOnly)
271     {
272         messages::passwordChangeRequired(
273             asyncResp->res,
274             crow::utility::urlFromPieces("redfish", "v1", "AccountService",
275                                          "Accounts", session->username));
276     }
277 
278     fillSessionObject(asyncResp->res, *session);
279 }
280 inline void handleSessionServiceHead(
281     crow::App& app, const crow::Request& req,
282     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
283 {
284 
285     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
286     {
287         return;
288     }
289     asyncResp->res.addHeader(
290         boost::beast::http::field::link,
291         "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby");
292 }
293 inline void
294     handleSessionServiceGet(crow::App& app, const crow::Request& req,
295                             const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
296 
297 {
298     handleSessionServiceHead(app, req, asyncResp);
299     asyncResp->res.jsonValue["@odata.type"] =
300         "#SessionService.v1_0_2.SessionService";
301     asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/SessionService/";
302     asyncResp->res.jsonValue["Name"] = "Session Service";
303     asyncResp->res.jsonValue["Id"] = "SessionService";
304     asyncResp->res.jsonValue["Description"] = "Session Service";
305     asyncResp->res.jsonValue["SessionTimeout"] =
306         persistent_data::SessionStore::getInstance().getTimeoutInSeconds();
307     asyncResp->res.jsonValue["ServiceEnabled"] = true;
308 
309     asyncResp->res.jsonValue["Sessions"]["@odata.id"] =
310         "/redfish/v1/SessionService/Sessions";
311 }
312 
313 inline void handleSessionServicePatch(
314     crow::App& app, const crow::Request& req,
315     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
316 {
317     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
318     {
319         return;
320     }
321     std::optional<int64_t> sessionTimeout;
322     if (!json_util::readJsonPatch(req, asyncResp->res, "SessionTimeout",
323                                   sessionTimeout))
324     {
325         return;
326     }
327 
328     if (sessionTimeout)
329     {
330         // The mininum & maximum allowed values for session timeout
331         // are 30 seconds and 86400 seconds respectively as per the
332         // session service schema mentioned at
333         // https://redfish.dmtf.org/schemas/v1/SessionService.v1_1_7.json
334 
335         if (*sessionTimeout <= 86400 && *sessionTimeout >= 30)
336         {
337             std::chrono::seconds sessionTimeoutInseconds(*sessionTimeout);
338             persistent_data::SessionStore::getInstance().updateSessionTimeout(
339                 sessionTimeoutInseconds);
340             messages::propertyValueModified(asyncResp->res, "SessionTimeOut",
341                                             std::to_string(*sessionTimeout));
342         }
343         else
344         {
345             messages::propertyValueNotInList(asyncResp->res,
346                                              std::to_string(*sessionTimeout),
347                                              "SessionTimeOut");
348         }
349     }
350 }
351 
352 inline void requestRoutesSession(App& app)
353 {
354     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
355         .privileges(redfish::privileges::headSession)
356         .methods(boost::beast::http::verb::head)(
357             std::bind_front(handleSessionHead, std::ref(app)));
358 
359     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
360         .privileges(redfish::privileges::getSession)
361         .methods(boost::beast::http::verb::get)(
362             std::bind_front(handleSessionGet, std::ref(app)));
363 
364     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
365         .privileges(redfish::privileges::deleteSession)
366         .methods(boost::beast::http::verb::delete_)(
367             std::bind_front(handleSessionDelete, std::ref(app)));
368 
369     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
370         .privileges(redfish::privileges::headSessionCollection)
371         .methods(boost::beast::http::verb::head)(
372             std::bind_front(handleSessionCollectionHead, std::ref(app)));
373 
374     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
375         .privileges(redfish::privileges::getSessionCollection)
376         .methods(boost::beast::http::verb::get)(
377             std::bind_front(handleSessionCollectionGet, std::ref(app)));
378 
379     // Note, the next two routes technically don't match the privilege
380     // registry given the way login mechanisms work.  The base privilege
381     // registry lists this endpoint as requiring login privilege, but because
382     // this is the endpoint responsible for giving the login privilege, and it
383     // is itself its own route, it needs to not require Login
384     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
385         .privileges({})
386         .methods(boost::beast::http::verb::post)(
387             std::bind_front(handleSessionCollectionPost, std::ref(app)));
388 
389     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/Members/")
390         .privileges({})
391         .methods(boost::beast::http::verb::post)(
392             std::bind_front(handleSessionCollectionPost, std::ref(app)));
393 
394     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
395         .privileges(redfish::privileges::headSessionService)
396         .methods(boost::beast::http::verb::head)(
397             std::bind_front(handleSessionServiceHead, std::ref(app)));
398 
399     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
400         .privileges(redfish::privileges::getSessionService)
401         .methods(boost::beast::http::verb::get)(
402             std::bind_front(handleSessionServiceGet, std::ref(app)));
403 
404     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
405         .privileges(redfish::privileges::patchSessionService)
406         .methods(boost::beast::http::verb::patch)(
407             std::bind_front(handleSessionServicePatch, std::ref(app)));
408 }
409 
410 } // namespace redfish
411