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 #include <boost/url/format.hpp>
27 
28 namespace redfish
29 {
30 
31 inline void fillSessionObject(crow::Response& res,
32                               const persistent_data::UserSession& session)
33 {
34     res.jsonValue["Id"] = session.uniqueId;
35     res.jsonValue["UserName"] = session.username;
36     res.jsonValue["@odata.id"] = boost::urls::format(
37         "/redfish/v1/SessionService/Sessions/{}", session.uniqueId);
38     res.jsonValue["@odata.type"] = "#Session.v1_5_0.Session";
39     res.jsonValue["Name"] = "User Session";
40     res.jsonValue["Description"] = "Manager User Session";
41     res.jsonValue["ClientOriginIPAddress"] = session.clientIp;
42     if (session.clientId)
43     {
44         res.jsonValue["Context"] = *session.clientId;
45     }
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     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
54     {
55         return;
56     }
57     asyncResp->res.addHeader(
58         boost::beast::http::field::link,
59         "</redfish/v1/JsonSchemas/Session/Session.json>; rel=describedby");
60 }
61 
62 inline void
63     handleSessionGet(crow::App& app, const crow::Request& req,
64                      const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
65                      const std::string& sessionId)
66 {
67     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
68     {
69         return;
70     }
71     asyncResp->res.addHeader(
72         boost::beast::http::field::link,
73         "</redfish/v1/JsonSchemas/Session/Session.json>; rel=describedby");
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.session);
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"] =
138             boost::urls::format("/redfish/v1/SessionService/Sessions/{}", *uid);
139         ret.emplace_back(std::move(session));
140     }
141     return ret;
142 }
143 
144 inline void handleSessionCollectionHead(
145     crow::App& app, const crow::Request& req,
146     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
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     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     asyncResp->res.jsonValue["Members"] = getSessionCollectionMembers();
170     asyncResp->res.jsonValue["Members@odata.count"] =
171         asyncResp->res.jsonValue["Members"].size();
172     asyncResp->res.jsonValue["@odata.type"] =
173         "#SessionCollection.SessionCollection";
174     asyncResp->res.jsonValue["@odata.id"] =
175         "/redfish/v1/SessionService/Sessions/";
176     asyncResp->res.jsonValue["Name"] = "Session Collection";
177     asyncResp->res.jsonValue["Description"] = "Session Collection";
178 }
179 
180 inline void handleSessionCollectionMembersGet(
181     crow::App& app, const crow::Request& req,
182     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
183 {
184     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
185     {
186         return;
187     }
188     asyncResp->res.jsonValue = getSessionCollectionMembers();
189 }
190 
191 inline void handleSessionCollectionPost(
192     crow::App& app, const crow::Request& req,
193     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
194 {
195     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
196     {
197         return;
198     }
199     std::string username;
200     std::string password;
201     std::optional<std::string> clientId;
202     if (!json_util::readJsonPatch(req, asyncResp->res, "UserName", username,
203                                   "Password", password, "Context", clientId))
204     {
205         return;
206     }
207 
208     if (password.empty() || username.empty() ||
209         asyncResp->res.result() != boost::beast::http::status::ok)
210     {
211         if (username.empty())
212         {
213             messages::propertyMissing(asyncResp->res, "UserName");
214         }
215 
216         if (password.empty())
217         {
218             messages::propertyMissing(asyncResp->res, "Password");
219         }
220 
221         return;
222     }
223 
224     int pamrc = pamAuthenticateUser(username, password);
225     bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
226     if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
227     {
228         messages::resourceAtUriUnauthorized(asyncResp->res, req.url(),
229                                             "Invalid username or password");
230         return;
231     }
232 
233     // User is authenticated - create session
234     std::shared_ptr<persistent_data::UserSession> session =
235         persistent_data::SessionStore::getInstance().generateUserSession(
236             username, req.ipAddress, clientId,
237             persistent_data::PersistenceType::TIMEOUT, isConfigureSelfOnly);
238     if (session == nullptr)
239     {
240         messages::internalError(asyncResp->res);
241         return;
242     }
243 
244     asyncResp->res.addHeader("X-Auth-Token", session->sessionToken);
245     asyncResp->res.addHeader(
246         "Location", "/redfish/v1/SessionService/Sessions/" + session->uniqueId);
247     asyncResp->res.result(boost::beast::http::status::created);
248     if (session->isConfigureSelfOnly)
249     {
250         messages::passwordChangeRequired(
251             asyncResp->res,
252             boost::urls::format("/redfish/v1/AccountService/Accounts/{}",
253                                 session->username));
254     }
255 
256     fillSessionObject(asyncResp->res, *session);
257 }
258 inline void handleSessionServiceHead(
259     crow::App& app, const crow::Request& req,
260     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
261 {
262     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
263     {
264         return;
265     }
266     asyncResp->res.addHeader(
267         boost::beast::http::field::link,
268         "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby");
269 }
270 inline void
271     handleSessionServiceGet(crow::App& app, const crow::Request& req,
272                             const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
273 
274 {
275     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
276     {
277         return;
278     }
279     asyncResp->res.addHeader(
280         boost::beast::http::field::link,
281         "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby");
282 
283     asyncResp->res.jsonValue["@odata.type"] =
284         "#SessionService.v1_0_2.SessionService";
285     asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/SessionService/";
286     asyncResp->res.jsonValue["Name"] = "Session Service";
287     asyncResp->res.jsonValue["Id"] = "SessionService";
288     asyncResp->res.jsonValue["Description"] = "Session Service";
289     asyncResp->res.jsonValue["SessionTimeout"] =
290         persistent_data::SessionStore::getInstance().getTimeoutInSeconds();
291     asyncResp->res.jsonValue["ServiceEnabled"] = true;
292 
293     asyncResp->res.jsonValue["Sessions"]["@odata.id"] =
294         "/redfish/v1/SessionService/Sessions";
295 }
296 
297 inline void handleSessionServicePatch(
298     crow::App& app, const crow::Request& req,
299     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
300 {
301     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
302     {
303         return;
304     }
305     std::optional<int64_t> sessionTimeout;
306     if (!json_util::readJsonPatch(req, asyncResp->res, "SessionTimeout",
307                                   sessionTimeout))
308     {
309         return;
310     }
311 
312     if (sessionTimeout)
313     {
314         // The mininum & maximum allowed values for session timeout
315         // are 30 seconds and 86400 seconds respectively as per the
316         // session service schema mentioned at
317         // https://redfish.dmtf.org/schemas/v1/SessionService.v1_1_7.json
318 
319         if (*sessionTimeout <= 86400 && *sessionTimeout >= 30)
320         {
321             std::chrono::seconds sessionTimeoutInseconds(*sessionTimeout);
322             persistent_data::SessionStore::getInstance().updateSessionTimeout(
323                 sessionTimeoutInseconds);
324             messages::propertyValueModified(asyncResp->res, "SessionTimeOut",
325                                             std::to_string(*sessionTimeout));
326         }
327         else
328         {
329             messages::propertyValueNotInList(asyncResp->res, *sessionTimeout,
330                                              "SessionTimeOut");
331         }
332     }
333 }
334 
335 inline void requestRoutesSession(App& app)
336 {
337     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
338         .privileges(redfish::privileges::headSession)
339         .methods(boost::beast::http::verb::head)(
340             std::bind_front(handleSessionHead, std::ref(app)));
341 
342     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
343         .privileges(redfish::privileges::getSession)
344         .methods(boost::beast::http::verb::get)(
345             std::bind_front(handleSessionGet, std::ref(app)));
346 
347     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/")
348         .privileges(redfish::privileges::deleteSession)
349         .methods(boost::beast::http::verb::delete_)(
350             std::bind_front(handleSessionDelete, std::ref(app)));
351 
352     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
353         .privileges(redfish::privileges::headSessionCollection)
354         .methods(boost::beast::http::verb::head)(
355             std::bind_front(handleSessionCollectionHead, std::ref(app)));
356 
357     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
358         .privileges(redfish::privileges::getSessionCollection)
359         .methods(boost::beast::http::verb::get)(
360             std::bind_front(handleSessionCollectionGet, std::ref(app)));
361 
362     // Note, the next two routes technically don't match the privilege
363     // registry given the way login mechanisms work.  The base privilege
364     // registry lists this endpoint as requiring login privilege, but because
365     // this is the endpoint responsible for giving the login privilege, and it
366     // is itself its own route, it needs to not require Login
367     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/")
368         .privileges({})
369         .methods(boost::beast::http::verb::post)(
370             std::bind_front(handleSessionCollectionPost, std::ref(app)));
371 
372     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/Members/")
373         .privileges({})
374         .methods(boost::beast::http::verb::post)(
375             std::bind_front(handleSessionCollectionPost, std::ref(app)));
376 
377     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
378         .privileges(redfish::privileges::headSessionService)
379         .methods(boost::beast::http::verb::head)(
380             std::bind_front(handleSessionServiceHead, std::ref(app)));
381 
382     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
383         .privileges(redfish::privileges::getSessionService)
384         .methods(boost::beast::http::verb::get)(
385             std::bind_front(handleSessionServiceGet, std::ref(app)));
386 
387     BMCWEB_ROUTE(app, "/redfish/v1/SessionService/")
388         .privileges(redfish::privileges::patchSessionService)
389         .methods(boost::beast::http::verb::patch)(
390             std::bind_front(handleSessionServicePatch, std::ref(app)));
391 }
392 
393 } // namespace redfish
394