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 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 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 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 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 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 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 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 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 204 inline void processAfterSessionCreation( 205 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 206 const crow::Request& req, const std::string& username, 207 std::shared_ptr<persistent_data::UserSession>& session) 208 { 209 // When session is created by webui-vue give it session cookies as a 210 // non-standard Redfish extension. This is needed for authentication for 211 // WebSockets-based functionality. 212 if (!req.getHeaderValue("X-Requested-With").empty()) 213 { 214 bmcweb::setSessionCookies(asyncResp->res, *session); 215 } 216 else 217 { 218 asyncResp->res.addHeader("X-Auth-Token", session->sessionToken); 219 } 220 221 asyncResp->res.addHeader( 222 "Location", "/redfish/v1/SessionService/Sessions/" + session->uniqueId); 223 asyncResp->res.result(boost::beast::http::status::created); 224 if (session->isConfigureSelfOnly) 225 { 226 messages::passwordChangeRequired( 227 asyncResp->res, 228 boost::urls::format("/redfish/v1/AccountService/Accounts/{}", 229 session->username)); 230 } 231 232 crow::getUserInfo(asyncResp, username, session, [asyncResp, session]() { 233 fillSessionObject(asyncResp->res, *session); 234 }); 235 } 236 237 inline void handleSessionCollectionPost( 238 crow::App& app, const crow::Request& req, 239 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 240 { 241 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 242 { 243 return; 244 } 245 std::string username; 246 std::string password; 247 std::optional<std::string> clientId; 248 std::optional<std::string> token; 249 if (!json_util::readJsonPatch( // 250 req, asyncResp->res, // 251 "Context", clientId, // 252 "Password", password, // 253 "Token", token, // 254 "UserName", username // 255 )) 256 { 257 return; 258 } 259 if (password.empty() || username.empty() || 260 asyncResp->res.result() != boost::beast::http::status::ok) 261 { 262 if (username.empty()) 263 { 264 messages::propertyMissing(asyncResp->res, "UserName"); 265 } 266 267 if (password.empty()) 268 { 269 messages::propertyMissing(asyncResp->res, "Password"); 270 } 271 272 return; 273 } 274 275 int pamrc = pamAuthenticateUser(username, password, token); 276 bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD; 277 if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly) 278 { 279 messages::resourceAtUriUnauthorized(asyncResp->res, req.url(), 280 "Invalid username or password"); 281 return; 282 } 283 284 // User is authenticated - create session 285 std::shared_ptr<persistent_data::UserSession> session = 286 persistent_data::SessionStore::getInstance().generateUserSession( 287 username, req.ipAddress, clientId, 288 persistent_data::SessionType::Session, isConfigureSelfOnly); 289 if (session == nullptr) 290 { 291 messages::internalError(asyncResp->res); 292 return; 293 } 294 processAfterSessionCreation(asyncResp, req, username, session); 295 } 296 297 inline void handleSessionServiceHead( 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 asyncResp->res.addHeader( 306 boost::beast::http::field::link, 307 "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby"); 308 } 309 inline void 310 handleSessionServiceGet(crow::App& app, const crow::Request& req, 311 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 312 313 { 314 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 315 { 316 return; 317 } 318 asyncResp->res.addHeader( 319 boost::beast::http::field::link, 320 "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby"); 321 322 asyncResp->res.jsonValue["@odata.type"] = 323 "#SessionService.v1_0_2.SessionService"; 324 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/SessionService"; 325 asyncResp->res.jsonValue["Name"] = "Session Service"; 326 asyncResp->res.jsonValue["Id"] = "SessionService"; 327 asyncResp->res.jsonValue["Description"] = "Session Service"; 328 asyncResp->res.jsonValue["SessionTimeout"] = 329 persistent_data::SessionStore::getInstance().getTimeoutInSeconds(); 330 asyncResp->res.jsonValue["ServiceEnabled"] = true; 331 332 asyncResp->res.jsonValue["Sessions"]["@odata.id"] = 333 "/redfish/v1/SessionService/Sessions"; 334 } 335 336 inline void handleSessionServicePatch( 337 crow::App& app, const crow::Request& req, 338 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 339 { 340 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 341 { 342 return; 343 } 344 std::optional<int64_t> sessionTimeout; 345 if (!json_util::readJsonPatch( // 346 req, asyncResp->res, // 347 "SessionTimeout", sessionTimeout // 348 )) 349 { 350 return; 351 } 352 353 if (sessionTimeout) 354 { 355 // The minimum & maximum allowed values for session timeout 356 // are 30 seconds and 86400 seconds respectively as per the 357 // session service schema mentioned at 358 // https://redfish.dmtf.org/schemas/v1/SessionService.v1_1_7.json 359 360 if (*sessionTimeout <= 86400 && *sessionTimeout >= 30) 361 { 362 std::chrono::seconds sessionTimeoutInseconds(*sessionTimeout); 363 persistent_data::SessionStore::getInstance().updateSessionTimeout( 364 sessionTimeoutInseconds); 365 messages::propertyValueModified(asyncResp->res, "SessionTimeOut", 366 std::to_string(*sessionTimeout)); 367 } 368 else 369 { 370 messages::propertyValueNotInList(asyncResp->res, *sessionTimeout, 371 "SessionTimeOut"); 372 } 373 } 374 } 375 376 inline void requestRoutesSession(App& app) 377 { 378 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/") 379 .privileges(redfish::privileges::headSession) 380 .methods(boost::beast::http::verb::head)( 381 std::bind_front(handleSessionHead, std::ref(app))); 382 383 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/") 384 .privileges(redfish::privileges::getSession) 385 .methods(boost::beast::http::verb::get)( 386 std::bind_front(handleSessionGet, std::ref(app))); 387 388 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/") 389 .privileges(redfish::privileges::deleteSession) 390 .methods(boost::beast::http::verb::delete_)( 391 std::bind_front(handleSessionDelete, std::ref(app))); 392 393 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/") 394 .privileges(redfish::privileges::headSessionCollection) 395 .methods(boost::beast::http::verb::head)( 396 std::bind_front(handleSessionCollectionHead, std::ref(app))); 397 398 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/") 399 .privileges(redfish::privileges::getSessionCollection) 400 .methods(boost::beast::http::verb::get)( 401 std::bind_front(handleSessionCollectionGet, std::ref(app))); 402 403 // Note, the next two routes technically don't match the privilege 404 // registry given the way login mechanisms work. The base privilege 405 // registry lists this endpoint as requiring login privilege, but because 406 // this is the endpoint responsible for giving the login privilege, and it 407 // is itself its own route, it needs to not require Login 408 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/") 409 .privileges({}) 410 .methods(boost::beast::http::verb::post)( 411 std::bind_front(handleSessionCollectionPost, std::ref(app))); 412 413 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/Members/") 414 .privileges({}) 415 .methods(boost::beast::http::verb::post)( 416 std::bind_front(handleSessionCollectionPost, std::ref(app))); 417 418 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/") 419 .privileges(redfish::privileges::headSessionService) 420 .methods(boost::beast::http::verb::head)( 421 std::bind_front(handleSessionServiceHead, std::ref(app))); 422 423 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/") 424 .privileges(redfish::privileges::getSessionService) 425 .methods(boost::beast::http::verb::get)( 426 std::bind_front(handleSessionServiceGet, std::ref(app))); 427 428 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/") 429 .privileges(redfish::privileges::patchSessionService) 430 .methods(boost::beast::http::verb::patch)( 431 std::bind_front(handleSessionServicePatch, std::ref(app))); 432 } 433 434 } // namespace redfish 435