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