/* Copyright (c) 2018 Intel Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #pragma once #include "account_service.hpp" #include "app.hpp" #include "cookies.hpp" #include "error_messages.hpp" #include "http/utility.hpp" #include "persistent_data.hpp" #include "query.hpp" #include "registries/privilege_registry.hpp" #include "utils/json_utils.hpp" #include #include #include namespace redfish { inline void fillSessionObject(crow::Response& res, const persistent_data::UserSession& session) { res.jsonValue["Id"] = session.uniqueId; res.jsonValue["UserName"] = session.username; nlohmann::json::array_t roles; roles.emplace_back(redfish::getRoleIdFromPrivilege(session.userRole)); res.jsonValue["Roles"] = std::move(roles); res.jsonValue["@odata.id"] = boost::urls::format( "/redfish/v1/SessionService/Sessions/{}", session.uniqueId); res.jsonValue["@odata.type"] = "#Session.v1_7_0.Session"; res.jsonValue["Name"] = "User Session"; res.jsonValue["Description"] = "Manager User Session"; res.jsonValue["ClientOriginIPAddress"] = session.clientIp; if (session.clientId) { res.jsonValue["Context"] = *session.clientId; } } inline void handleSessionHead(crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& /*sessionId*/) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } asyncResp->res.addHeader( boost::beast::http::field::link, "; rel=describedby"); } inline void handleSessionGet(crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& sessionId) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } asyncResp->res.addHeader( boost::beast::http::field::link, "; rel=describedby"); // Note that control also reaches here via doPost and doDelete. auto session = persistent_data::SessionStore::getInstance().getSessionByUid(sessionId); if (session == nullptr) { messages::resourceNotFound(asyncResp->res, "Session", sessionId); return; } fillSessionObject(asyncResp->res, *session); } inline void handleSessionDelete(crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& sessionId) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } auto session = persistent_data::SessionStore::getInstance().getSessionByUid(sessionId); if (session == nullptr) { messages::resourceNotFound(asyncResp->res, "Session", sessionId); return; } // Perform a proper ConfigureSelf authority check. If a // session is being used to DELETE some other user's session, // then the ConfigureSelf privilege does not apply. In that // case, perform the authority check again without the user's // ConfigureSelf privilege. if (req.session != nullptr && !session->username.empty() && session->username != req.session->username) { Privileges effectiveUserPrivileges = redfish::getUserPrivileges(*req.session); if (!effectiveUserPrivileges.isSupersetOf({"ConfigureUsers"})) { messages::insufficientPrivilege(asyncResp->res); return; } } if (req.session != nullptr && req.session->uniqueId == sessionId && session->cookieAuth) { bmcweb::clearSessionCookies(asyncResp->res); } persistent_data::SessionStore::getInstance().removeSession(session); messages::success(asyncResp->res); } inline nlohmann::json getSessionCollectionMembers() { std::vector sessionIds = persistent_data::SessionStore::getInstance().getAllUniqueIds(); nlohmann::json ret = nlohmann::json::array(); for (const std::string& uid : sessionIds) { nlohmann::json::object_t session; session["@odata.id"] = boost::urls::format("/redfish/v1/SessionService/Sessions/{}", uid); ret.emplace_back(std::move(session)); } return ret; } inline void handleSessionCollectionHead( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } asyncResp->res.addHeader( boost::beast::http::field::link, "; rel=describedby"); } inline void handleSessionCollectionGet( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } asyncResp->res.addHeader( boost::beast::http::field::link, "; rel=describedby"); asyncResp->res.jsonValue["Members"] = getSessionCollectionMembers(); asyncResp->res.jsonValue["Members@odata.count"] = asyncResp->res.jsonValue["Members"].size(); asyncResp->res.jsonValue["@odata.type"] = "#SessionCollection.SessionCollection"; asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/SessionService/Sessions"; asyncResp->res.jsonValue["Name"] = "Session Collection"; asyncResp->res.jsonValue["Description"] = "Session Collection"; } inline void handleSessionCollectionMembersGet( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } asyncResp->res.jsonValue = getSessionCollectionMembers(); } inline void handleSessionCollectionPost( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } std::string username; std::string password; std::optional clientId; std::optional token; if (!json_util::readJsonPatch( // req, asyncResp->res, // "Context", clientId, // "Password", password, // "Token", token, // "UserName", username // )) { return; } if (password.empty() || username.empty() || asyncResp->res.result() != boost::beast::http::status::ok) { if (username.empty()) { messages::propertyMissing(asyncResp->res, "UserName"); } if (password.empty()) { messages::propertyMissing(asyncResp->res, "Password"); } return; } int pamrc = pamAuthenticateUser(username, password, token); bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD; if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly) { messages::resourceAtUriUnauthorized(asyncResp->res, req.url(), "Invalid username or password"); return; } // User is authenticated - create session std::shared_ptr session = persistent_data::SessionStore::getInstance().generateUserSession( username, req.ipAddress, clientId, persistent_data::SessionType::Session, isConfigureSelfOnly); if (session == nullptr) { messages::internalError(asyncResp->res); return; } // When session is created by webui-vue give it session cookies as a // non-standard Redfish extension. This is needed for authentication for // WebSockets-based functionality. if (!req.getHeaderValue("X-Requested-With").empty()) { bmcweb::setSessionCookies(asyncResp->res, *session); } else { asyncResp->res.addHeader("X-Auth-Token", session->sessionToken); } asyncResp->res.addHeader( "Location", "/redfish/v1/SessionService/Sessions/" + session->uniqueId); asyncResp->res.result(boost::beast::http::status::created); if (session->isConfigureSelfOnly) { messages::passwordChangeRequired( asyncResp->res, boost::urls::format("/redfish/v1/AccountService/Accounts/{}", session->username)); } crow::getUserInfo(asyncResp, username, session, [asyncResp, session]() { fillSessionObject(asyncResp->res, *session); }); } inline void handleSessionServiceHead( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } asyncResp->res.addHeader( boost::beast::http::field::link, "; rel=describedby"); } inline void handleSessionServiceGet(crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } asyncResp->res.addHeader( boost::beast::http::field::link, "; rel=describedby"); asyncResp->res.jsonValue["@odata.type"] = "#SessionService.v1_0_2.SessionService"; asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/SessionService"; asyncResp->res.jsonValue["Name"] = "Session Service"; asyncResp->res.jsonValue["Id"] = "SessionService"; asyncResp->res.jsonValue["Description"] = "Session Service"; asyncResp->res.jsonValue["SessionTimeout"] = persistent_data::SessionStore::getInstance().getTimeoutInSeconds(); asyncResp->res.jsonValue["ServiceEnabled"] = true; asyncResp->res.jsonValue["Sessions"]["@odata.id"] = "/redfish/v1/SessionService/Sessions"; } inline void handleSessionServicePatch( crow::App& app, const crow::Request& req, const std::shared_ptr& asyncResp) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } std::optional sessionTimeout; if (!json_util::readJsonPatch( // req, asyncResp->res, // "SessionTimeout", sessionTimeout // )) { return; } if (sessionTimeout) { // The minimum & maximum allowed values for session timeout // are 30 seconds and 86400 seconds respectively as per the // session service schema mentioned at // https://redfish.dmtf.org/schemas/v1/SessionService.v1_1_7.json if (*sessionTimeout <= 86400 && *sessionTimeout >= 30) { std::chrono::seconds sessionTimeoutInseconds(*sessionTimeout); persistent_data::SessionStore::getInstance().updateSessionTimeout( sessionTimeoutInseconds); messages::propertyValueModified(asyncResp->res, "SessionTimeOut", std::to_string(*sessionTimeout)); } else { messages::propertyValueNotInList(asyncResp->res, *sessionTimeout, "SessionTimeOut"); } } } inline void requestRoutesSession(App& app) { BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions//") .privileges(redfish::privileges::headSession) .methods(boost::beast::http::verb::head)( std::bind_front(handleSessionHead, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions//") .privileges(redfish::privileges::getSession) .methods(boost::beast::http::verb::get)( std::bind_front(handleSessionGet, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions//") .privileges(redfish::privileges::deleteSession) .methods(boost::beast::http::verb::delete_)( std::bind_front(handleSessionDelete, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/") .privileges(redfish::privileges::headSessionCollection) .methods(boost::beast::http::verb::head)( std::bind_front(handleSessionCollectionHead, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/") .privileges(redfish::privileges::getSessionCollection) .methods(boost::beast::http::verb::get)( std::bind_front(handleSessionCollectionGet, std::ref(app))); // Note, the next two routes technically don't match the privilege // registry given the way login mechanisms work. The base privilege // registry lists this endpoint as requiring login privilege, but because // this is the endpoint responsible for giving the login privilege, and it // is itself its own route, it needs to not require Login BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/") .privileges({}) .methods(boost::beast::http::verb::post)( std::bind_front(handleSessionCollectionPost, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/SessionService/Sessions/Members/") .privileges({}) .methods(boost::beast::http::verb::post)( std::bind_front(handleSessionCollectionPost, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/SessionService/") .privileges(redfish::privileges::headSessionService) .methods(boost::beast::http::verb::head)( std::bind_front(handleSessionServiceHead, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/SessionService/") .privileges(redfish::privileges::getSessionService) .methods(boost::beast::http::verb::get)( std::bind_front(handleSessionServiceGet, std::ref(app))); BMCWEB_ROUTE(app, "/redfish/v1/SessionService/") .privileges(redfish::privileges::patchSessionService) .methods(boost::beast::http::verb::patch)( std::bind_front(handleSessionServicePatch, std::ref(app))); } } // namespace redfish