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 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 } 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 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 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 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