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