| #include "query.hpp" |
| |
| #include "absl/synchronization/mutex.h" |
| #include "bmcweb_config.h" |
| |
| #include "app.hpp" |
| #include "async_resp.hpp" |
| #include "error_messages.hpp" |
| #include "logging.hpp" |
| #include "managed_store.hpp" |
| #include "redfish_aggregator.hpp" |
| #include "str_utility.hpp" |
| |
| #include <boost/beast/http/verb.hpp> |
| #include <boost/system/error_code.hpp> |
| #include <boost/url/params_view.hpp> |
| #include <boost/url/url_view.hpp> |
| |
| #include <cstdint> |
| #include <functional> |
| #include <memory> |
| #include <new> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <type_traits> |
| #include <utility> |
| |
| #ifdef UNIT_TEST_BUILD |
| #include "test/g3/mock_managed_store.hpp" // NOLINT |
| #endif |
| |
| namespace redfish |
| { |
| |
| namespace query_param |
| { |
| |
| // Validates the property in the $select parameter. Every character is among |
| // [a-zA-Z0-9#@_.] (taken from Redfish spec, section 9.6 Properties) |
| bool isSelectedPropertyAllowed(std::string_view property) |
| { |
| // These a magic number, but with it it's less likely that this code |
| // introduces CVE; e.g., too large properties crash the service. |
| constexpr int maxPropertyLength = 60; |
| if (property.empty() || property.size() > maxPropertyLength) |
| { |
| return false; |
| } |
| for (char ch : property) |
| { |
| if (std::isalnum(static_cast<unsigned char>(ch)) == 0 && ch != '#' && |
| ch != '@' && ch != '.') |
| { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool SelectTrie::insertNode(std::string_view nestedProperty) |
| { |
| if (nestedProperty.empty()) |
| { |
| return false; |
| } |
| SelectTrieNode* currNode = &root; |
| size_t index = nestedProperty.find_first_of('/'); |
| while (!nestedProperty.empty()) |
| { |
| std::string_view property = nestedProperty.substr(0, index); |
| if (!isSelectedPropertyAllowed(property)) |
| { |
| return false; |
| } |
| currNode = currNode->emplace(property); |
| if (index == std::string::npos) |
| { |
| break; |
| } |
| nestedProperty.remove_prefix(index + 1); |
| index = nestedProperty.find_first_of('/'); |
| } |
| currNode->setToSelected(); |
| return true; |
| } |
| |
| // Delegates query parameters according to the given |queryCapabilities| |
| // This function doesn't check query parameter conflicts since the parse |
| // function will take care of it. |
| // Returns a delegated query object which can be used by individual resource |
| // handlers so that handlers don't need to query again. |
| Query delegate(const QueryCapabilities& queryCapabilities, Query& query, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) |
| { |
| Query delegated; |
| // delegate only |
| if (query.isOnly && queryCapabilities.canDelegateOnly) |
| { |
| delegated.isOnly = true; |
| query.isOnly = false; |
| } |
| // delegate expand as much as we can |
| if (query.expandType != ExpandType::None) |
| { |
| delegated.expandType = query.expandType; |
| if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel) |
| { |
| query.expandType = ExpandType::None; |
| delegated.expandLevel = query.expandLevel; |
| query.expandLevel = 0; |
| } |
| else |
| { |
| // We actually dont need to subtract query.expandType here because |
| // When we expand navigation references, we actually start from |
| // the root of the entire response instead of the delegated level |
| delegated.expandLevel = queryCapabilities.canDelegateExpandLevel; |
| } |
| asyncResp->delegatedExpandLevel = delegated.expandLevel; |
| } |
| |
| // delegate top |
| if (query.top && queryCapabilities.canDelegateTop) |
| { |
| delegated.top = query.top; |
| query.top = std::nullopt; |
| } |
| |
| // delegate skip |
| if (query.skip && queryCapabilities.canDelegateSkip) |
| { |
| delegated.skip = query.skip; |
| query.skip = 0; |
| } |
| |
| // delegate select |
| if (!query.selectTrie.root.empty() && queryCapabilities.canDelegateSelect) |
| { |
| delegated.selectTrie = std::move(query.selectTrie); |
| query.selectTrie.root.clear(); |
| } |
| |
| // delegate filter |
| if (!query.filter.empty() && queryCapabilities.canDelegateFilter) |
| { |
| delegated.filter = query.filter; |
| query.filter = ""; |
| } |
| return delegated; |
| } |
| |
| bool getExpandType(std::string_view value, Query& query) |
| { |
| if (value.empty()) |
| { |
| return false; |
| } |
| switch (value[0]) |
| { |
| case '*': |
| query.expandType = ExpandType::Both; |
| break; |
| case '.': |
| query.expandType = ExpandType::NotLinks; |
| break; |
| case '~': |
| query.expandType = ExpandType::Links; |
| break; |
| default: |
| return false; |
| |
| break; |
| } |
| value.remove_prefix(1); |
| if (value.empty()) |
| { |
| query.expandLevel = 1; |
| return true; |
| } |
| constexpr std::string_view levels = "($levels="; |
| if (!value.starts_with(levels)) |
| { |
| return false; |
| } |
| value.remove_prefix(levels.size()); |
| |
| auto it = std::from_chars(value.data(), value.data() + value.size(), |
| query.expandLevel); |
| if (it.ec != std::errc()) |
| { |
| return false; |
| } |
| value.remove_prefix(static_cast<size_t>(it.ptr - value.data())); |
| return value == ")"; |
| } |
| |
| enum class QueryError : std::uint8_t |
| { |
| Ok, |
| OutOfRange, |
| ValueFormat, |
| }; |
| |
| QueryError getNumericParam(std::string_view value, size_t& param) |
| { |
| std::from_chars_result r = |
| std::from_chars(value.data(), value.data() + value.size(), param); |
| |
| // If the number wasn't representable in the type, it's out of range |
| if (r.ec == std::errc::result_out_of_range) |
| { |
| return QueryError::OutOfRange; |
| } |
| // All other errors are value format |
| if (r.ec != std::errc()) |
| { |
| return QueryError::ValueFormat; |
| } |
| return QueryError::Ok; |
| } |
| |
| QueryError getSkipParam(std::string_view value, Query& query) |
| { |
| return getNumericParam(value, query.skip.emplace()); |
| } |
| |
| QueryError getTopParam(std::string_view value, Query& query) |
| { |
| QueryError ret = getNumericParam(value, query.top.emplace()); |
| if (ret != QueryError::Ok) |
| { |
| return ret; |
| } |
| |
| // Range check for sanity. |
| if (query.top > Query::maxTop) |
| { |
| return QueryError::OutOfRange; |
| } |
| |
| return QueryError::Ok; |
| } |
| |
| // Parses and validates the $select parameter. |
| // As per OData URL Conventions and Redfish Spec, the $select values shall be |
| // comma separated Resource Path |
| // Ref: |
| // 1. https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 |
| // 2. |
| // https://docs.oasis-open.org/odata/odata/v4.01/os/abnf/odata-abnf-construction-rules.txt |
| bool getSelectParam(std::string_view value, Query& query) |
| { |
| std::vector<std::string> properties; |
| bmcweb::split(properties, value, ','); |
| if (properties.empty()) |
| { |
| return false; |
| } |
| // These a magic number, but with it it's less likely that this code |
| // introduces CVE; e.g., too large properties crash the service. |
| constexpr int maxNumProperties = 10; |
| if (properties.size() > maxNumProperties) |
| { |
| return false; |
| } |
| for (const auto& property : properties) |
| { |
| if (!query.selectTrie.insertNode(property)) |
| { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool getFilterParam(std::string_view value, Query& query) |
| { |
| if (value.empty()) |
| { |
| return false; |
| } |
| |
| query.filter = std::string(value); |
| return true; |
| } |
| |
| std::optional<Query> parseParameters(boost::urls::params_view urlParams, |
| crow::Response& res) |
| { |
| Query ret; |
| for (const boost::urls::params_view::value_type& it : urlParams) |
| { |
| if (it.key == "only") |
| { |
| if (!it.value.empty()) |
| { |
| messages::queryParameterValueFormatError(res, it.value, it.key); |
| return std::nullopt; |
| } |
| ret.isOnly = true; |
| } |
| else if (it.key == "$expand" && bmcwebInsecureEnableQueryParams) |
| { |
| if (!getExpandType(it.value, ret)) |
| { |
| messages::queryParameterValueFormatError(res, it.value, it.key); |
| return std::nullopt; |
| } |
| } |
| else if (it.key == "$top") |
| { |
| QueryError topRet = getTopParam(it.value, ret); |
| if (topRet == QueryError::ValueFormat) |
| { |
| messages::queryParameterValueFormatError(res, it.value, it.key); |
| return std::nullopt; |
| } |
| if (topRet == QueryError::OutOfRange) |
| { |
| messages::queryParameterOutOfRange( |
| res, it.value, "$top", |
| "0-" + std::to_string(Query::maxTop)); |
| return std::nullopt; |
| } |
| } |
| else if (it.key == "$skip") |
| { |
| QueryError topRet = getSkipParam(it.value, ret); |
| if (topRet == QueryError::ValueFormat) |
| { |
| messages::queryParameterValueFormatError(res, it.value, it.key); |
| return std::nullopt; |
| } |
| if (topRet == QueryError::OutOfRange) |
| { |
| messages::queryParameterOutOfRange( |
| res, it.value, it.key, |
| "0-" + std::to_string(std::numeric_limits<size_t>::max())); |
| return std::nullopt; |
| } |
| } |
| else if (it.key == "$select") |
| { |
| if (!getSelectParam(it.value, ret)) |
| { |
| messages::queryParameterValueFormatError(res, it.value, it.key); |
| return std::nullopt; |
| } |
| } |
| else if (it.key == "$filter") |
| { |
| if (!getFilterParam(it.value, ret)) |
| { |
| messages::queryParameterValueFormatError(res, it.value, it.key); |
| return std::nullopt; |
| } |
| } |
| else |
| { |
| // Intentionally ignore other errors Redfish spec, 7.3.1 |
| if (it.key.starts_with("$")) |
| { |
| // Services shall return... The HTTP 501 Not Implemented |
| // status code for any unsupported query parameters that |
| // start with $ . |
| messages::queryParameterValueFormatError(res, it.value, it.key); |
| res.result(boost::beast::http::status::not_implemented); |
| return std::nullopt; |
| } |
| // "Shall ignore unknown or unsupported query parameters that do |
| // not begin with $ ." |
| } |
| } |
| |
| if (ret.expandType != ExpandType::None && !ret.selectTrie.root.empty()) |
| { |
| messages::queryCombinationInvalid(res); |
| return std::nullopt; |
| } |
| |
| return ret; |
| } |
| |
| bool processOnly(crow::App& app, crow::Response& res, |
| std::function<void(crow::Response&)>& completionHandler, |
| const std::shared_ptr<boost::asio::io_context::strand>& strand) |
| { |
| BMCWEB_LOG_DEBUG << "Processing only query param"; |
| auto itMembers = res.jsonValue.find("Members"); |
| if (itMembers == res.jsonValue.end()) |
| { |
| messages::queryNotSupportedOnResource(res); |
| completionHandler(res); |
| return false; |
| } |
| auto itMemBegin = itMembers->begin(); |
| if (itMemBegin == itMembers->end() || itMembers->size() != 1) |
| { |
| BMCWEB_LOG_DEBUG << "Members contains " << itMembers->size() |
| << " element, returning full collection."; |
| completionHandler(res); |
| return false; |
| } |
| |
| auto itUrl = itMemBegin->find("@odata.id"); |
| if (itUrl == itMemBegin->end()) |
| { |
| BMCWEB_LOG_DEBUG << "No found odata.id"; |
| messages::internalError(res); |
| completionHandler(res); |
| return false; |
| } |
| const std::string* url = itUrl->get_ptr<const std::string*>(); |
| if (url == nullptr) |
| { |
| BMCWEB_LOG_DEBUG << "@odata.id wasn't a string????"; |
| messages::internalError(res); |
| completionHandler(res); |
| return false; |
| } |
| // TODO(Ed) copy request headers? |
| // newReq.session = req.session; |
| std::error_code ec; |
| crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec); |
| if (ec) |
| { |
| messages::internalError(res); |
| completionHandler(res); |
| return false; |
| } |
| |
| auto asyncResp = std::make_shared<bmcweb::AsyncResp>(strand); |
| BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res; |
| asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); |
| asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper()); |
| app.handle(newReq, asyncResp); |
| return true; |
| } |
| |
| // Walks a json object looking for Redfish NavigationReference entries that |
| // might need resolved. It recursively walks the jsonResponse object, looking |
| // for links at every level, and returns a list (out) of locations within the |
| // tree that need to be expanded. The current json pointer location p is passed |
| // in to reference the current node that's being expanded, so it can be combined |
| // with the keys from the jsonResponse object |
| void findNavigationReferencesRecursive(ExpandType eType, |
| nlohmann::json& jsonResponse, |
| const nlohmann::json::json_pointer& p, |
| int depth, bool inLinks, |
| std::vector<ExpandNode>& out) |
| { |
| // If no expand is needed, return early |
| if (eType == ExpandType::None) |
| { |
| return; |
| } |
| |
| nlohmann::json::array_t* array = |
| jsonResponse.get_ptr<nlohmann::json::array_t*>(); |
| if (array != nullptr) |
| { |
| size_t index = 0; |
| // For arrays, walk every element in the array |
| for (auto& element : *array) |
| { |
| nlohmann::json::json_pointer newPtr = p / index; |
| BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr.to_string(); |
| findNavigationReferencesRecursive(eType, element, newPtr, depth, |
| inLinks, out); |
| index++; |
| } |
| } |
| nlohmann::json::object_t* obj = |
| jsonResponse.get_ptr<nlohmann::json::object_t*>(); |
| if (obj == nullptr) |
| { |
| return; |
| } |
| // Navigation References only ever have a single element |
| if (obj->size() == 1) |
| { |
| if (obj->begin()->first == "@odata.id") |
| { |
| const std::string* uri = |
| obj->begin()->second.get_ptr<const std::string*>(); |
| if (uri != nullptr) |
| { |
| BMCWEB_LOG_DEBUG << "Found " << *uri << " at " << p.to_string(); |
| out.push_back({p, *uri}); |
| return; |
| } |
| } |
| } |
| |
| int newDepth = depth; |
| auto odataId = obj->find("@odata.id"); |
| if (odataId != obj->end()) |
| { |
| // The Redfish spec requires all resources to include the resource |
| // identifier. If the object has multiple elements and one of them is |
| // "@odata.id" then that means we have entered a new level / expanded |
| // resource. We need to stop traversing if we're already at the desired |
| // depth |
| if ((obj->size() > 1) && (depth == 0)) |
| { |
| return; |
| } |
| newDepth--; |
| } |
| |
| // Loop the object and look for links |
| for (auto& element : *obj) |
| { |
| bool localInLinks = inLinks; |
| if (!localInLinks) |
| { |
| // Check if this is a links node |
| localInLinks = element.first == "Links"; |
| } |
| // Only traverse the parts of the tree the user asked for |
| // Per section 7.3 of the redfish specification |
| if (localInLinks && eType == ExpandType::NotLinks) |
| { |
| continue; |
| } |
| if (!localInLinks && eType == ExpandType::Links) |
| { |
| continue; |
| } |
| nlohmann::json::json_pointer newPtr = p / element.first; |
| BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr; |
| |
| findNavigationReferencesRecursive(eType, element.second, newPtr, |
| newDepth, localInLinks, out); |
| } |
| } |
| |
| // TODO: When aggregation is enabled and we receive a partially expanded |
| // response we may need need additional handling when the original URI was |
| // up tree from a top level collection. |
| // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556 |
| // lands. May want to avoid forwarding query params when request is uptree from |
| // a top level collection. |
| std::vector<ExpandNode> findNavigationReferences(ExpandType eType, int depth, |
| nlohmann::json& jsonResponse) |
| { |
| std::vector<ExpandNode> ret; |
| const nlohmann::json::json_pointer root = nlohmann::json::json_pointer(""); |
| findNavigationReferencesRecursive(eType, jsonResponse, root, depth, false, |
| ret); |
| return ret; |
| } |
| |
| // Formats a query parameter string for the sub-query. |
| // Returns std::nullopt on failures. |
| // This function shall handle $select when it is added. |
| // There is no need to handle parameters that's not campatible with $expand, |
| // e.g., $only, since this function will only be called in side $expand handlers |
| std::optional<std::string> formatQueryForExpand(const Query& query) |
| { |
| // query.expandLevel<=1: no need to do subqueries |
| if (query.expandLevel <= 1) |
| { |
| return ""; |
| } |
| std::string str = "?$expand="; |
| bool queryTypeExpected = false; |
| switch (query.expandType) |
| { |
| case ExpandType::None: |
| return ""; |
| case ExpandType::Links: |
| queryTypeExpected = true; |
| str += '~'; |
| break; |
| case ExpandType::NotLinks: |
| queryTypeExpected = true; |
| str += '.'; |
| break; |
| case ExpandType::Both: |
| queryTypeExpected = true; |
| str += '*'; |
| break; |
| } |
| if (!queryTypeExpected) |
| { |
| return std::nullopt; |
| } |
| str += "($levels="; |
| str += std::to_string(query.expandLevel - 1); |
| str += ')'; |
| return str; |
| } |
| |
| // Propogates the worst error code to the final response. |
| // The order of error code is (from high to low) |
| // 500 Internal Server Error |
| // 511 Network Authentication Required |
| // 510 Not Extended |
| // 508 Loop Detected |
| // 507 Insufficient Storage |
| // 506 Variant Also Negotiates |
| // 505 HTTP Version Not Supported |
| // 504 Gateway Timeout |
| // 503 Service Unavailable |
| // 502 Bad Gateway |
| // 501 Not Implemented |
| // 401 Unauthorized |
| // 451 - 409 Error codes (not listed explictly) |
| // 408 Request Timeout |
| // 407 Proxy Authentication Required |
| // 406 Not Acceptable |
| // 405 Method Not Allowed |
| // 404 Not Found |
| // 403 Forbidden |
| // 402 Payment Required |
| // 400 Bad Request |
| unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode) |
| { |
| // We keep a explicit list for error codes that this project often uses |
| // Higer priority codes are in lower indexes |
| constexpr std::array<unsigned, 13> orderedCodes = { |
| 500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400}; |
| size_t finalCodeIndex = std::numeric_limits<size_t>::max(); |
| size_t subResponseCodeIndex = std::numeric_limits<size_t>::max(); |
| for (size_t i = 0; i < orderedCodes.size(); ++i) |
| { |
| if (orderedCodes[i] == finalCode) |
| { |
| finalCodeIndex = i; |
| } |
| if (orderedCodes[i] == subResponseCode) |
| { |
| subResponseCodeIndex = i; |
| } |
| } |
| if (finalCodeIndex != std::numeric_limits<size_t>::max() && |
| subResponseCodeIndex != std::numeric_limits<size_t>::max()) |
| { |
| return finalCodeIndex <= subResponseCodeIndex ? finalCode |
| : subResponseCode; |
| } |
| if (subResponseCode == 500 || finalCode == 500) |
| { |
| return 500; |
| } |
| if (subResponseCode > 500 || finalCode > 500) |
| { |
| return std::max(finalCode, subResponseCode); |
| } |
| if (subResponseCode == 401) |
| { |
| return subResponseCode; |
| } |
| return std::max(finalCode, subResponseCode); |
| } |
| |
| // Propogates all error messages into |finalResponse| |
| void propogateError(crow::Response& finalResponse, crow::Response& subResponse) |
| { |
| // no errors |
| if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400) |
| { |
| return; |
| } |
| messages::moveErrorsToErrorJson(finalResponse.jsonValue, |
| subResponse.jsonValue); |
| finalResponse.result( |
| propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt())); |
| } |
| |
| class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp> |
| { |
| public: |
| // This object takes a single asyncResp object as the "final" one, then |
| // allows callers to attach sub-responses within the json tree that need |
| // to be executed and filled into their appropriate locations. This |
| // class manages the final "merge" of the json resources. |
| MultiAsyncResp(crow::App& appIn, |
| std::shared_ptr<bmcweb::AsyncResp> finalResIn, |
| const crow::Request& request) : |
| app(appIn), finalRes(std::move(finalResIn)), fromGrpc(request.fromGrpc), |
| with_trust_bundle(request.with_trust_bundle), |
| peer_authenticated(request.peer_authenticated), |
| peer_privileges(request.peer_privileges) |
| {} |
| |
| void addAwaitingResponse( |
| const std::shared_ptr<bmcweb::AsyncResp>& res, |
| const nlohmann::json::json_pointer& finalExpandLocation) |
| { |
| res->res.setCompleteRequestHandler(std::bind_front( |
| placeResultStatic, shared_from_this(), finalExpandLocation)); |
| } |
| |
| void placeResult(const nlohmann::json::json_pointer& locationToPlace, |
| crow::Response& res) |
| { |
| propogateError(finalRes->res, res); |
| if (!res.jsonValue.is_object() || res.jsonValue.empty()) |
| { |
| return; |
| } |
| nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace]; |
| finalObj = std::move(res.jsonValue); |
| BMCWEB_LOG_DEBUG << "placeResult for " << locationToPlace; |
| } |
| |
| // Handles the very first level of Expand, and starts a chain of sub-queries |
| // for deeper levels. |
| void startQuery(const Query& query) |
| { |
| std::vector<ExpandNode> nodes = findNavigationReferences( |
| query.expandType, query.expandLevel, finalRes->res.jsonValue); |
| BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse"; |
| const std::optional<std::string> queryStr = formatQueryForExpand(query); |
| if (!queryStr) |
| { |
| messages::internalError(finalRes->res); |
| return; |
| } |
| for (const ExpandNode& node : nodes) |
| { |
| const std::string subQuery = node.uri + *queryStr; |
| BMCWEB_LOG_DEBUG << "URL of subquery: " << subQuery; |
| std::error_code ec; |
| crow::Request newReq({boost::beast::http::verb::get, subQuery, 11}, |
| ec); |
| if (ec) |
| { |
| messages::internalError(finalRes->res); |
| return; |
| } |
| |
| newReq.fromGrpc = fromGrpc; |
| newReq.with_trust_bundle = with_trust_bundle; |
| newReq.peer_authenticated = peer_authenticated; |
| newReq.peer_privileges = peer_privileges; |
| |
| auto asyncResp = std::make_shared<bmcweb::AsyncResp>(finalRes->strand_); |
| BMCWEB_LOG_DEBUG << "setting completion handler on " |
| << &asyncResp->res; |
| |
| addAwaitingResponse(asyncResp, node.location); |
| |
| BMCWEB_LOG_DEBUG << "Single-threaded expand"; |
| app.handle(newReq, asyncResp); |
| } |
| } |
| |
| private: |
| static void |
| placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi, |
| const nlohmann::json::json_pointer& locationToPlace, |
| crow::Response& res) |
| { |
| multi->placeResult(locationToPlace, res); |
| } |
| |
| crow::App& app; |
| std::shared_ptr<bmcweb::AsyncResp> finalRes; |
| |
| bool fromGrpc; |
| bool with_trust_bundle; |
| bool peer_authenticated; |
| std::unordered_set<std::string> peer_privileges; |
| absl::Mutex mutex_; |
| }; |
| |
| void processTopAndSkip(const Query& query, crow::Response& res) |
| { |
| if (!query.skip && !query.top) |
| { |
| // No work to do. |
| return; |
| } |
| nlohmann::json::object_t* obj = |
| res.jsonValue.get_ptr<nlohmann::json::object_t*>(); |
| if (obj == nullptr) |
| { |
| // Shouldn't be possible. All responses should be objects. |
| messages::internalError(res); |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Handling top/skip"; |
| nlohmann::json::object_t::iterator members = obj->find("Members"); |
| if (members == obj->end()) |
| { |
| // From the Redfish specification 7.3.1 |
| // ... the HTTP 400 Bad Request status code with the |
| // QueryNotSupportedOnResource message from the Base Message Registry |
| // for any supported query parameters that apply only to resource |
| // collections but are used on singular resources. |
| messages::queryNotSupportedOnResource(res); |
| return; |
| } |
| |
| nlohmann::json::array_t* arr = |
| members->second.get_ptr<nlohmann::json::array_t*>(); |
| if (arr == nullptr) |
| { |
| messages::internalError(res); |
| return; |
| } |
| |
| if (query.skip) |
| { |
| // Per section 7.3.1 of the Redfish specification, $skip is run before |
| // $top Can only skip as many values as we have |
| size_t skip = std::min(arr->size(), *query.skip); |
| arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip)); |
| } |
| if (query.top) |
| { |
| size_t top = std::min(arr->size(), *query.top); |
| arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end()); |
| } |
| } |
| |
| // Given a JSON subtree |currRoot|, this function erases leaves whose keys are |
| // not in the |currNode| Trie node. |
| void recursiveSelect(nlohmann::json& currRoot, const SelectTrieNode& currNode) |
| { |
| nlohmann::json::object_t* object = |
| currRoot.get_ptr<nlohmann::json::object_t*>(); |
| if (object != nullptr) |
| { |
| BMCWEB_LOG_DEBUG << "Current JSON is an object"; |
| auto it = currRoot.begin(); |
| while (it != currRoot.end()) |
| { |
| auto nextIt = std::next(it); |
| BMCWEB_LOG_DEBUG << "key=" << it.key(); |
| const SelectTrieNode* nextNode = currNode.find(it.key()); |
| // Per the Redfish spec section 7.3.3, the service shall select |
| // certain properties as if $select was omitted. This applies to |
| // every TrieNode that contains leaves and the root. |
| constexpr std::array<std::string_view, 5> reservedProperties = { |
| "@odata.id", "@odata.type", "@odata.context", "@odata.etag", |
| "error"}; |
| bool reserved = |
| std::find(reservedProperties.begin(), reservedProperties.end(), |
| it.key()) != reservedProperties.end(); |
| if (reserved || (nextNode != nullptr && nextNode->isSelected())) |
| { |
| it = nextIt; |
| continue; |
| } |
| if (nextNode != nullptr) |
| { |
| BMCWEB_LOG_DEBUG << "Recursively select: " << it.key(); |
| recursiveSelect(*it, *nextNode); |
| it = nextIt; |
| continue; |
| } |
| BMCWEB_LOG_DEBUG << it.key() << " is getting removed!"; |
| it = currRoot.erase(it); |
| } |
| } |
| nlohmann::json::array_t* array = |
| currRoot.get_ptr<nlohmann::json::array_t*>(); |
| if (array != nullptr) |
| { |
| BMCWEB_LOG_DEBUG << "Current JSON is an array"; |
| // Array index is omitted, so reuse the same Trie node |
| for (nlohmann::json& nextRoot : *array) |
| { |
| recursiveSelect(nextRoot, currNode); |
| } |
| } |
| } |
| |
| // The current implementation of $select still has the following TODOs due to |
| // ambiguity and/or complexity. |
| // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was |
| // created for clarification. |
| // 2. respect the full odata spec; e.g., deduplication, namespace, star (*), |
| // etc. |
| void processSelect(crow::Response& intermediateResponse, |
| const SelectTrieNode& trieRoot) |
| { |
| BMCWEB_LOG_DEBUG << "Process $select quary parameter"; |
| recursiveSelect(intermediateResponse.jsonValue, trieRoot); |
| } |
| |
| void processAllParams(crow::App& app, const Query& query, |
| const crow::Request& request, |
| std::function<void(crow::Response&)>& completionHandler, |
| crow::Response& intermediateResponse, |
| const std::shared_ptr<boost::asio::io_context::strand>& strand) |
| { |
| if (!completionHandler) |
| { |
| BMCWEB_LOG_DEBUG << "Function was invalid?"; |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Processing query params"; |
| // If the request failed, there's no reason to even try to run query |
| // params. |
| if (intermediateResponse.resultInt() < 200 || |
| intermediateResponse.resultInt() >= 400) |
| { |
| completionHandler(intermediateResponse); |
| return; |
| } |
| if (query.isOnly) |
| { |
| processOnly(app, intermediateResponse, completionHandler, strand); |
| return; |
| } |
| |
| if (query.top || query.skip) |
| { |
| processTopAndSkip(query, intermediateResponse); |
| } |
| |
| if (query.expandType != ExpandType::None) |
| { |
| BMCWEB_LOG_DEBUG << "Executing expand query"; |
| auto asyncResp = std::make_shared<bmcweb::AsyncResp>( |
| std::move(intermediateResponse), strand); |
| |
| asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); |
| auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp, request); |
| |
| multi->startQuery(query); |
| return; |
| } |
| |
| // According to Redfish Spec Section 7.3.1, $select is the last parameter to |
| // to process |
| if (!query.selectTrie.root.empty()) |
| { |
| processSelect(intermediateResponse, query.selectTrie.root); |
| } |
| |
| completionHandler(intermediateResponse); |
| } |
| |
| } // namespace query_param |
| |
| namespace |
| { |
| |
| void afterIfMatchRequest(crow::App& app, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| crow::Request& req, const std::string& ifMatchHeader, |
| const crow::Response& resIn) |
| { |
| std::string computedEtag = resIn.computeEtag(); |
| BMCWEB_LOG_DEBUG << "User provided if-match etag " << ifMatchHeader |
| << " computed etag " << computedEtag; |
| if (computedEtag != ifMatchHeader) |
| { |
| messages::preconditionFailed(asyncResp->res); |
| return; |
| } |
| // Restart the request without if-match |
| req.req.erase(boost::beast::http::field::if_match); |
| BMCWEB_LOG_DEBUG << "Restarting request"; |
| app.handle(req, asyncResp); |
| } |
| |
| bool handleIfMatch(crow::App& app, const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) |
| { |
| if (req.session == nullptr) |
| { |
| // If the user isn't authenticated, don't even attempt to parse match |
| // parameters |
| return true; |
| } |
| |
| std::string ifMatch{ |
| req.getHeaderValue(boost::beast::http::field::if_match)}; |
| if (ifMatch.empty()) |
| { |
| // No If-Match header. Nothing to do |
| return true; |
| } |
| if (req.req.method() != boost::beast::http::verb::patch && |
| req.req.method() != boost::beast::http::verb::post && |
| req.req.method() != boost::beast::http::verb::delete_) |
| { |
| messages::preconditionFailed(asyncResp->res); |
| return false; |
| } |
| boost::system::error_code ec; |
| |
| // Try to GET the same resource |
| crow::Request newReq( |
| {boost::beast::http::verb::get, req.url().encoded_path(), 11}, ec); |
| |
| if (ec) |
| { |
| messages::internalError(asyncResp->res); |
| return false; |
| } |
| |
| // New request has the same credentials as the old request |
| newReq.session = req.session; |
| |
| // Construct a new response object to fill in, and check the hash of before |
| // we modify the Resource. |
| std::shared_ptr<bmcweb::AsyncResp> getReqAsyncResp = |
| std::make_shared<bmcweb::AsyncResp>(asyncResp->strand_); |
| |
| getReqAsyncResp->res.setCompleteRequestHandler(std::bind_front( |
| afterIfMatchRequest, std::ref(app), asyncResp, req, ifMatch)); |
| |
| app.handle(newReq, getReqAsyncResp); |
| return false; |
| } |
| |
| } // namespace |
| |
| bool setUpRedfishRouteWithDelegation( |
| crow::App& app, const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| query_param::Query& delegated, |
| const query_param::QueryCapabilities& queryCapabilities) |
| { |
| BMCWEB_LOG_DEBUG << "setup redfish route"; |
| |
| if (asyncResp->response_type == |
| bmcweb::AsyncResp::ResponseType::kSubscription && |
| !queryCapabilities.canHandleSubscription) |
| { |
| BMCWEB_LOG_ALWAYS << "Cannot delegate subscription for route: " |
| << req.url(); |
| return false; |
| } |
| |
| // Section 7.4 of the redfish spec "Redfish Services shall process the |
| // [OData-Version header] in the following table as defined by the HTTP 1.1 |
| // specification..." |
| // Required to pass redfish-protocol-validator REQ_HEADERS_ODATA_VERSION |
| std::string_view odataHeader = req.getHeaderValue("OData-Version"); |
| if (!odataHeader.empty() && odataHeader != "4.0") |
| { |
| messages::preconditionFailed(asyncResp->res); |
| return false; |
| } |
| |
| asyncResp->res.addHeader("OData-Version", "4.0"); |
| |
| std::optional<query_param::Query> queryOpt = |
| query_param::parseParameters(req.url().params(), asyncResp->res); |
| if (queryOpt == std::nullopt) |
| { |
| return false; |
| } |
| |
| if (!handleIfMatch(app, req, asyncResp)) |
| { |
| return false; |
| } |
| |
| bool needToCallHandlers = true; |
| |
| #ifdef BMCWEB_ENABLE_REDFISH_AGGREGATION |
| needToCallHandlers = RedfishAggregator::getInstance().beginAggregation( |
| req, asyncResp) == Result::LocalHandle; |
| |
| // If the request should be forwarded to a satellite BMC then we don't want |
| // to write anything to the asyncResp since it will get overwritten later. |
| #endif |
| |
| // If this isn't a get, no need to do anything with parameters |
| if (req.method() != boost::beast::http::verb::get) |
| { |
| return needToCallHandlers; |
| } |
| |
| delegated = query_param::delegate(queryCapabilities, *queryOpt, asyncResp); |
| std::function<void(crow::Response&)> handler = |
| asyncResp->res.releaseCompleteRequestHandler(); |
| auto strand = asyncResp->strand_; |
| |
| asyncResp->res.setCompleteRequestHandler( |
| [&app, handler(std::move(handler)), query{std::move(*queryOpt)}, |
| request(req), strand](crow::Response& resIn) mutable { |
| query_param::processAllParams(app, query, request, handler, resIn, strand); |
| }); |
| |
| return needToCallHandlers; |
| } |
| |
| } // namespace redfish |