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 namespace redfish 31 { 32 33 inline void fillSessionObject(crow::Response& res, 34 const persistent_data::UserSession& session) 35 { 36 res.jsonValue["Id"] = session.uniqueId; 37 res.jsonValue["UserName"] = session.username; 38 nlohmann::json::array_t roles; 39 roles.emplace_back(redfish::getRoleIdFromPrivilege(session.userRole)); 40 res.jsonValue["Roles"] = std::move(roles); 41 res.jsonValue["@odata.id"] = boost::urls::format( 42 "/redfish/v1/SessionService/Sessions/{}", session.uniqueId); 43 res.jsonValue["@odata.type"] = "#Session.v1_7_0.Session"; 44 res.jsonValue["Name"] = "User Session"; 45 res.jsonValue["Description"] = "Manager User Session"; 46 res.jsonValue["ClientOriginIPAddress"] = session.clientIp; 47 if (session.clientId) 48 { 49 res.jsonValue["Context"] = *session.clientId; 50 } 51 } 52 53 inline void 54 handleSessionHead(crow::App& app, const crow::Request& req, 55 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 56 const std::string& /*sessionId*/) 57 { 58 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 59 { 60 return; 61 } 62 asyncResp->res.addHeader( 63 boost::beast::http::field::link, 64 "</redfish/v1/JsonSchemas/Session/Session.json>; rel=describedby"); 65 } 66 67 inline void 68 handleSessionGet(crow::App& app, const crow::Request& req, 69 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 70 const std::string& sessionId) 71 { 72 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 73 { 74 return; 75 } 76 asyncResp->res.addHeader( 77 boost::beast::http::field::link, 78 "</redfish/v1/JsonSchemas/Session/Session.json>; rel=describedby"); 79 80 // Note that control also reaches here via doPost and doDelete. 81 auto session = 82 persistent_data::SessionStore::getInstance().getSessionByUid(sessionId); 83 84 if (session == nullptr) 85 { 86 messages::resourceNotFound(asyncResp->res, "Session", sessionId); 87 return; 88 } 89 90 fillSessionObject(asyncResp->res, *session); 91 } 92 93 inline void 94 handleSessionDelete(crow::App& app, const crow::Request& req, 95 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 96 const std::string& sessionId) 97 { 98 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 99 { 100 return; 101 } 102 auto session = 103 persistent_data::SessionStore::getInstance().getSessionByUid(sessionId); 104 105 if (session == nullptr) 106 { 107 messages::resourceNotFound(asyncResp->res, "Session", sessionId); 108 return; 109 } 110 111 // Perform a proper ConfigureSelf authority check. If a 112 // session is being used to DELETE some other user's session, 113 // then the ConfigureSelf privilege does not apply. In that 114 // case, perform the authority check again without the user's 115 // ConfigureSelf privilege. 116 if (req.session != nullptr && !session->username.empty() && 117 session->username != req.session->username) 118 { 119 Privileges effectiveUserPrivileges = 120 redfish::getUserPrivileges(*req.session); 121 122 if (!effectiveUserPrivileges.isSupersetOf({"ConfigureUsers"})) 123 { 124 messages::insufficientPrivilege(asyncResp->res); 125 return; 126 } 127 } 128 129 if (session->cookieAuth) 130 { 131 bmcweb::clearSessionCookies(asyncResp->res); 132 } 133 134 persistent_data::SessionStore::getInstance().removeSession(session); 135 messages::success(asyncResp->res); 136 } 137 138 inline nlohmann::json getSessionCollectionMembers() 139 { 140 std::vector<const std::string*> sessionIds = 141 persistent_data::SessionStore::getInstance().getUniqueIds( 142 false, persistent_data::PersistenceType::TIMEOUT); 143 nlohmann::json ret = nlohmann::json::array(); 144 for (const std::string* uid : sessionIds) 145 { 146 nlohmann::json::object_t session; 147 session["@odata.id"] = 148 boost::urls::format("/redfish/v1/SessionService/Sessions/{}", *uid); 149 ret.emplace_back(std::move(session)); 150 } 151 return ret; 152 } 153 154 inline void handleSessionCollectionHead( 155 crow::App& app, const crow::Request& req, 156 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 157 { 158 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 159 { 160 return; 161 } 162 asyncResp->res.addHeader( 163 boost::beast::http::field::link, 164 "</redfish/v1/JsonSchemas/SessionCollection.json>; rel=describedby"); 165 } 166 167 inline void handleSessionCollectionGet( 168 crow::App& app, const crow::Request& req, 169 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 170 { 171 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 172 { 173 return; 174 } 175 asyncResp->res.addHeader( 176 boost::beast::http::field::link, 177 "</redfish/v1/JsonSchemas/SessionCollection.json>; rel=describedby"); 178 179 asyncResp->res.jsonValue["Members"] = getSessionCollectionMembers(); 180 asyncResp->res.jsonValue["Members@odata.count"] = 181 asyncResp->res.jsonValue["Members"].size(); 182 asyncResp->res.jsonValue["@odata.type"] = 183 "#SessionCollection.SessionCollection"; 184 asyncResp->res.jsonValue["@odata.id"] = 185 "/redfish/v1/SessionService/Sessions"; 186 asyncResp->res.jsonValue["Name"] = "Session Collection"; 187 asyncResp->res.jsonValue["Description"] = "Session Collection"; 188 } 189 190 inline void handleSessionCollectionMembersGet( 191 crow::App& app, const crow::Request& req, 192 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 193 { 194 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 195 { 196 return; 197 } 198 asyncResp->res.jsonValue = getSessionCollectionMembers(); 199 } 200 201 inline void handleSessionCollectionPost( 202 crow::App& app, const crow::Request& req, 203 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 204 { 205 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 206 { 207 return; 208 } 209 std::string username; 210 std::string password; 211 std::optional<std::string> clientId; 212 if (!json_util::readJsonPatch(req, asyncResp->res, "UserName", username, 213 "Password", password, "Context", clientId)) 214 { 215 return; 216 } 217 218 if (password.empty() || username.empty() || 219 asyncResp->res.result() != boost::beast::http::status::ok) 220 { 221 if (username.empty()) 222 { 223 messages::propertyMissing(asyncResp->res, "UserName"); 224 } 225 226 if (password.empty()) 227 { 228 messages::propertyMissing(asyncResp->res, "Password"); 229 } 230 231 return; 232 } 233 234 int pamrc = pamAuthenticateUser(username, password); 235 bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD; 236 if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly) 237 { 238 messages::resourceAtUriUnauthorized(asyncResp->res, req.url(), 239 "Invalid username or password"); 240 return; 241 } 242 243 // User is authenticated - create session 244 std::shared_ptr<persistent_data::UserSession> session = 245 persistent_data::SessionStore::getInstance().generateUserSession( 246 username, req.ipAddress, clientId, 247 persistent_data::PersistenceType::TIMEOUT, isConfigureSelfOnly); 248 if (session == nullptr) 249 { 250 messages::internalError(asyncResp->res); 251 return; 252 } 253 254 // When session is created by webui-vue give it session cookies as a 255 // non-standard Redfish extension. This is needed for authentication for 256 // WebSockets-based functionality. 257 if (!req.getHeaderValue("X-Requested-With").empty()) 258 { 259 bmcweb::setSessionCookies(asyncResp->res, *session); 260 } 261 else 262 { 263 asyncResp->res.addHeader("X-Auth-Token", session->sessionToken); 264 } 265 266 asyncResp->res.addHeader( 267 "Location", "/redfish/v1/SessionService/Sessions/" + session->uniqueId); 268 asyncResp->res.result(boost::beast::http::status::created); 269 if (session->isConfigureSelfOnly) 270 { 271 messages::passwordChangeRequired( 272 asyncResp->res, 273 boost::urls::format("/redfish/v1/AccountService/Accounts/{}", 274 session->username)); 275 } 276 277 crow::getUserInfo(asyncResp, username, session, [asyncResp, session]() { 278 fillSessionObject(asyncResp->res, *session); 279 }); 280 } 281 inline void handleSessionServiceHead( 282 crow::App& app, const crow::Request& req, 283 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 284 { 285 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 286 { 287 return; 288 } 289 asyncResp->res.addHeader( 290 boost::beast::http::field::link, 291 "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby"); 292 } 293 inline void 294 handleSessionServiceGet(crow::App& app, const crow::Request& req, 295 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 296 297 { 298 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 299 { 300 return; 301 } 302 asyncResp->res.addHeader( 303 boost::beast::http::field::link, 304 "</redfish/v1/JsonSchemas/SessionService/SessionService.json>; rel=describedby"); 305 306 asyncResp->res.jsonValue["@odata.type"] = 307 "#SessionService.v1_0_2.SessionService"; 308 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/SessionService"; 309 asyncResp->res.jsonValue["Name"] = "Session Service"; 310 asyncResp->res.jsonValue["Id"] = "SessionService"; 311 asyncResp->res.jsonValue["Description"] = "Session Service"; 312 asyncResp->res.jsonValue["SessionTimeout"] = 313 persistent_data::SessionStore::getInstance().getTimeoutInSeconds(); 314 asyncResp->res.jsonValue["ServiceEnabled"] = true; 315 316 asyncResp->res.jsonValue["Sessions"]["@odata.id"] = 317 "/redfish/v1/SessionService/Sessions"; 318 } 319 320 inline void handleSessionServicePatch( 321 crow::App& app, const crow::Request& req, 322 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 323 { 324 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 325 { 326 return; 327 } 328 std::optional<int64_t> sessionTimeout; 329 if (!json_util::readJsonPatch(req, asyncResp->res, "SessionTimeout", 330 sessionTimeout)) 331 { 332 return; 333 } 334 335 if (sessionTimeout) 336 { 337 // The minimum & maximum allowed values for session timeout 338 // are 30 seconds and 86400 seconds respectively as per the 339 // session service schema mentioned at 340 // https://redfish.dmtf.org/schemas/v1/SessionService.v1_1_7.json 341 342 if (*sessionTimeout <= 86400 && *sessionTimeout >= 30) 343 { 344 std::chrono::seconds sessionTimeoutInseconds(*sessionTimeout); 345 persistent_data::SessionStore::getInstance().updateSessionTimeout( 346 sessionTimeoutInseconds); 347 messages::propertyValueModified(asyncResp->res, "SessionTimeOut", 348 std::to_string(*sessionTimeout)); 349 } 350 else 351 { 352 messages::propertyValueNotInList(asyncResp->res, *sessionTimeout, 353 "SessionTimeOut"); 354 } 355 } 356 } 357 358 inline void requestRoutesSession(App& app) 359 { 360 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/") 361 .privileges(redfish::privileges::headSession) 362 .methods(boost::beast::http::verb::head)( 363 std::bind_front(handleSessionHead, std::ref(app))); 364 365 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/") 366 .privileges(redfish::privileges::getSession) 367 .methods(boost::beast::http::verb::get)( 368 std::bind_front(handleSessionGet, std::ref(app))); 369 370 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/<str>/") 371 .privileges(redfish::privileges::deleteSession) 372 .methods(boost::beast::http::verb::delete_)( 373 std::bind_front(handleSessionDelete, std::ref(app))); 374 375 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/") 376 .privileges(redfish::privileges::headSessionCollection) 377 .methods(boost::beast::http::verb::head)( 378 std::bind_front(handleSessionCollectionHead, std::ref(app))); 379 380 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/") 381 .privileges(redfish::privileges::getSessionCollection) 382 .methods(boost::beast::http::verb::get)( 383 std::bind_front(handleSessionCollectionGet, std::ref(app))); 384 385 // Note, the next two routes technically don't match the privilege 386 // registry given the way login mechanisms work. The base privilege 387 // registry lists this endpoint as requiring login privilege, but because 388 // this is the endpoint responsible for giving the login privilege, and it 389 // is itself its own route, it needs to not require Login 390 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/") 391 .privileges({}) 392 .methods(boost::beast::http::verb::post)( 393 std::bind_front(handleSessionCollectionPost, std::ref(app))); 394 395 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/Members/") 396 .privileges({}) 397 .methods(boost::beast::http::verb::post)( 398 std::bind_front(handleSessionCollectionPost, std::ref(app))); 399 400 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/") 401 .privileges(redfish::privileges::headSessionService) 402 .methods(boost::beast::http::verb::head)( 403 std::bind_front(handleSessionServiceHead, std::ref(app))); 404 405 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/") 406 .privileges(redfish::privileges::getSessionService) 407 .methods(boost::beast::http::verb::get)( 408 std::bind_front(handleSessionServiceGet, std::ref(app))); 409 410 BMCWEB_ROUTE(app, "/redfish/v1/SessionService/") 411 .privileges(redfish::privileges::patchSessionService) 412 .methods(boost::beast::http::verb::patch)( 413 std::bind_front(handleSessionServicePatch, std::ref(app))); 414 } 415 416 } // namespace redfish 417