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