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