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