xref: /openbmc/bmcweb/redfish-core/include/utils/query_param.hpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 #include "bmcweb_config.h"
5 
6 #include "app.hpp"
7 #include "async_resp.hpp"
8 #include "error_messages.hpp"
9 #include "filter_expr_executor.hpp"
10 #include "filter_expr_printer.hpp"
11 #include "http_request.hpp"
12 #include "http_response.hpp"
13 #include "json_formatters.hpp"
14 #include "logging.hpp"
15 #include "str_utility.hpp"
16 
17 #include <sys/types.h>
18 
19 #include <boost/beast/http/message.hpp>
20 #include <boost/beast/http/status.hpp>
21 #include <boost/beast/http/verb.hpp>
22 #include <boost/url/params_view.hpp>
23 #include <nlohmann/json.hpp>
24 
25 #include <algorithm>
26 #include <array>
27 #include <cctype>
28 #include <charconv>
29 #include <compare>
30 #include <cstdint>
31 #include <functional>
32 #include <iterator>
33 #include <limits>
34 #include <map>
35 #include <memory>
36 #include <optional>
37 #include <ranges>
38 #include <string>
39 #include <string_view>
40 #include <system_error>
41 #include <utility>
42 #include <vector>
43 
44 namespace redfish
45 {
46 namespace query_param
47 {
48 
49 enum class ExpandType : uint8_t
50 {
51     None,
52     Links,
53     NotLinks,
54     Both,
55 };
56 
57 // A simple implementation of Trie to help |recursiveSelect|.
58 class SelectTrieNode
59 {
60   public:
61     SelectTrieNode() = default;
62 
63     const SelectTrieNode* find(const std::string& jsonKey) const
64     {
65         auto it = children.find(jsonKey);
66         if (it == children.end())
67         {
68             return nullptr;
69         }
70         return &it->second;
71     }
72 
73     // Creates a new node if the key doesn't exist, returns the reference to the
74     // newly created node; otherwise, return the reference to the existing node
75     SelectTrieNode* emplace(std::string_view jsonKey)
76     {
77         auto [it, _] = children.emplace(jsonKey, SelectTrieNode{});
78         return &it->second;
79     }
80 
81     bool empty() const
82     {
83         return children.empty();
84     }
85 
86     void clear()
87     {
88         children.clear();
89     }
90 
91     void setToSelected()
92     {
93         selected = true;
94     }
95 
96     bool isSelected() const
97     {
98         return selected;
99     }
100 
101   private:
102     std::map<std::string, SelectTrieNode, std::less<>> children;
103     bool selected = false;
104 };
105 
106 // Validates the property in the $select parameter. Every character is among
107 // [a-zA-Z0-9#@_.] (taken from Redfish spec, section 9.6 Properties)
108 inline bool isSelectedPropertyAllowed(std::string_view property)
109 {
110     // These a magic number, but with it it's less likely that this code
111     // introduces CVE; e.g., too large properties crash the service.
112     constexpr int maxPropertyLength = 60;
113     if (property.empty() || property.size() > maxPropertyLength)
114     {
115         return false;
116     }
117     for (char ch : property)
118     {
119         if (std::isalnum(static_cast<unsigned char>(ch)) == 0 && ch != '#' &&
120             ch != '@' && ch != '.')
121         {
122             return false;
123         }
124     }
125     return true;
126 }
127 
128 struct SelectTrie
129 {
130     SelectTrie() = default;
131 
132     // Inserts a $select value; returns false if the nestedProperty is illegal.
133     bool insertNode(std::string_view nestedProperty)
134     {
135         if (nestedProperty.empty())
136         {
137             return false;
138         }
139         SelectTrieNode* currNode = &root;
140         size_t index = nestedProperty.find_first_of('/');
141         while (!nestedProperty.empty())
142         {
143             std::string_view property = nestedProperty.substr(0, index);
144             if (!isSelectedPropertyAllowed(property))
145             {
146                 return false;
147             }
148             currNode = currNode->emplace(property);
149             if (index == std::string::npos)
150             {
151                 break;
152             }
153             nestedProperty.remove_prefix(index + 1);
154             index = nestedProperty.find_first_of('/');
155         }
156         currNode->setToSelected();
157         return true;
158     }
159 
160     SelectTrieNode root;
161 };
162 
163 // The struct stores the parsed query parameters of the default Redfish route.
164 struct Query
165 {
166     // Only
167     bool isOnly = false;
168 
169     // Expand
170     uint8_t expandLevel = 0;
171     ExpandType expandType = ExpandType::None;
172 
173     // Skip
174     std::optional<size_t> skip = std::nullopt;
175 
176     // Top
177     static constexpr size_t maxTop = 1000; // Max entries a response contain
178     std::optional<size_t> top = std::nullopt;
179 
180     // Filter
181     std::optional<filter_ast::LogicalAnd> filter = std::nullopt;
182 
183     // Select
184     // Unclear how to make this use structured initialization without this.
185     // Might be a tidy bug?  Ignore for now
186     // NOLINTNEXTLINE(readability-redundant-member-init)
187     SelectTrie selectTrie{};
188 };
189 
190 // The struct defines how resource handlers in redfish-core/lib/ can handle
191 // query parameters themselves, so that the default Redfish route will delegate
192 // the processing.
193 struct QueryCapabilities
194 {
195     bool canDelegateOnly = false;
196     bool canDelegateTop = false;
197     bool canDelegateSkip = false;
198     uint8_t canDelegateExpandLevel = 0;
199     bool canDelegateSelect = false;
200 };
201 
202 // Delegates query parameters according to the given |queryCapabilities|
203 // This function doesn't check query parameter conflicts since the parse
204 // function will take care of it.
205 // Returns a delegated query object which can be used by individual resource
206 // handlers so that handlers don't need to query again.
207 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query)
208 {
209     Query delegated{};
210     // delegate only
211     if (query.isOnly && queryCapabilities.canDelegateOnly)
212     {
213         delegated.isOnly = true;
214         query.isOnly = false;
215     }
216     // delegate expand as much as we can
217     if (query.expandType != ExpandType::None)
218     {
219         delegated.expandType = query.expandType;
220         if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel)
221         {
222             query.expandType = ExpandType::None;
223             delegated.expandLevel = query.expandLevel;
224             query.expandLevel = 0;
225         }
226         else
227         {
228             delegated.expandLevel = queryCapabilities.canDelegateExpandLevel;
229         }
230     }
231 
232     // delegate top
233     if (query.top && queryCapabilities.canDelegateTop)
234     {
235         delegated.top = query.top;
236         query.top = std::nullopt;
237     }
238 
239     // delegate skip
240     if (query.skip && queryCapabilities.canDelegateSkip)
241     {
242         delegated.skip = query.skip;
243         query.skip = 0;
244     }
245 
246     // delegate select
247     if (!query.selectTrie.root.empty() && queryCapabilities.canDelegateSelect)
248     {
249         delegated.selectTrie = std::move(query.selectTrie);
250         query.selectTrie.root.clear();
251     }
252     return delegated;
253 }
254 
255 inline bool getExpandType(std::string_view value, Query& query)
256 {
257     if (value.empty())
258     {
259         return false;
260     }
261     switch (value[0])
262     {
263         case '*':
264             query.expandType = ExpandType::Both;
265             break;
266         case '.':
267             query.expandType = ExpandType::NotLinks;
268             break;
269         case '~':
270             query.expandType = ExpandType::Links;
271             break;
272         default:
273             return false;
274     }
275     value.remove_prefix(1);
276     if (value.empty())
277     {
278         query.expandLevel = 1;
279         return true;
280     }
281     constexpr std::string_view levels = "($levels=";
282     if (!value.starts_with(levels))
283     {
284         return false;
285     }
286     value.remove_prefix(levels.size());
287 
288     auto it = std::from_chars(value.begin(), value.end(), query.expandLevel);
289     if (it.ec != std::errc())
290     {
291         return false;
292     }
293     value.remove_prefix(
294         static_cast<size_t>(std::distance(value.begin(), it.ptr)));
295     return value == ")";
296 }
297 
298 enum class QueryError
299 {
300     Ok,
301     OutOfRange,
302     ValueFormat,
303 };
304 
305 inline QueryError getNumericParam(std::string_view value, size_t& param)
306 {
307     std::from_chars_result r =
308         std::from_chars(value.begin(), value.end(), param);
309 
310     // If the number wasn't representable in the type, it's out of range
311     if (r.ec == std::errc::result_out_of_range)
312     {
313         return QueryError::OutOfRange;
314     }
315     // All other errors are value format
316     if (r.ec != std::errc())
317     {
318         return QueryError::ValueFormat;
319     }
320     return QueryError::Ok;
321 }
322 
323 inline QueryError getSkipParam(std::string_view value, Query& query)
324 {
325     return getNumericParam(value, query.skip.emplace());
326 }
327 
328 inline QueryError getTopParam(std::string_view value, Query& query)
329 {
330     QueryError ret = getNumericParam(value, query.top.emplace());
331     if (ret != QueryError::Ok)
332     {
333         return ret;
334     }
335 
336     // Range check for sanity.
337     if (query.top > Query::maxTop)
338     {
339         return QueryError::OutOfRange;
340     }
341 
342     return QueryError::Ok;
343 }
344 
345 // Parses and validates the $select parameter.
346 // As per OData URL Conventions and Redfish Spec, the $select values shall be
347 // comma separated Resource Path
348 // Ref:
349 // 1. https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
350 // 2.
351 // https://docs.oasis-open.org/odata/odata/v4.01/os/abnf/odata-abnf-construction-rules.txt
352 inline bool getSelectParam(std::string_view value, Query& query)
353 {
354     std::vector<std::string> properties;
355     bmcweb::split(properties, value, ',');
356     if (properties.empty())
357     {
358         return false;
359     }
360     // These a magic number, but with it it's less likely that this code
361     // introduces CVE; e.g., too large properties crash the service.
362     constexpr int maxNumProperties = 10;
363     if (properties.size() > maxNumProperties)
364     {
365         return false;
366     }
367     for (const auto& property : properties)
368     {
369         if (!query.selectTrie.insertNode(property))
370         {
371             return false;
372         }
373     }
374     return true;
375 }
376 
377 // Parses and validates the $filter parameter.
378 inline bool getFilterParam(std::string_view value, Query& query)
379 {
380     query.filter = parseFilter(value);
381     return query.filter.has_value();
382 }
383 
384 inline std::optional<Query>
385     parseParameters(boost::urls::params_view urlParams, crow::Response& res)
386 {
387     Query ret{};
388     for (const boost::urls::params_view::value_type& it : urlParams)
389     {
390         if (it.key == "only")
391         {
392             if (!it.value.empty())
393             {
394                 messages::queryParameterValueFormatError(res, it.value, it.key);
395                 return std::nullopt;
396             }
397             ret.isOnly = true;
398         }
399         else if (it.key == "$expand" && BMCWEB_INSECURE_ENABLE_REDFISH_QUERY)
400         {
401             if (!getExpandType(it.value, ret))
402             {
403                 messages::queryParameterValueFormatError(res, it.value, it.key);
404                 return std::nullopt;
405             }
406         }
407         else if (it.key == "$top")
408         {
409             QueryError topRet = getTopParam(it.value, ret);
410             if (topRet == QueryError::ValueFormat)
411             {
412                 messages::queryParameterValueFormatError(res, it.value, it.key);
413                 return std::nullopt;
414             }
415             if (topRet == QueryError::OutOfRange)
416             {
417                 messages::queryParameterOutOfRange(
418                     res, it.value, "$top",
419                     "0-" + std::to_string(Query::maxTop));
420                 return std::nullopt;
421             }
422         }
423         else if (it.key == "$skip")
424         {
425             QueryError topRet = getSkipParam(it.value, ret);
426             if (topRet == QueryError::ValueFormat)
427             {
428                 messages::queryParameterValueFormatError(res, it.value, it.key);
429                 return std::nullopt;
430             }
431             if (topRet == QueryError::OutOfRange)
432             {
433                 messages::queryParameterOutOfRange(
434                     res, it.value, it.key,
435                     "0-" + std::to_string(std::numeric_limits<size_t>::max()));
436                 return std::nullopt;
437             }
438         }
439         else if (it.key == "$select")
440         {
441             if (!getSelectParam(it.value, ret))
442             {
443                 messages::queryParameterValueFormatError(res, it.value, it.key);
444                 return std::nullopt;
445             }
446         }
447         else if (it.key == "$filter" && BMCWEB_INSECURE_ENABLE_REDFISH_QUERY)
448         {
449             if (!getFilterParam(it.value, ret))
450             {
451                 messages::queryParameterValueFormatError(res, it.value, it.key);
452                 return std::nullopt;
453             }
454         }
455         else
456         {
457             // Intentionally ignore other errors Redfish spec, 7.3.1
458             if (it.key.starts_with("$"))
459             {
460                 // Services shall return... The HTTP 501 Not Implemented
461                 // status code for any unsupported query parameters that
462                 // start with $ .
463                 messages::queryParameterValueFormatError(res, it.value, it.key);
464                 res.result(boost::beast::http::status::not_implemented);
465                 return std::nullopt;
466             }
467             // "Shall ignore unknown or unsupported query parameters that do
468             // not begin with $ ."
469         }
470     }
471 
472     if (ret.expandType != ExpandType::None && !ret.selectTrie.root.empty())
473     {
474         messages::queryCombinationInvalid(res);
475         return std::nullopt;
476     }
477 
478     return ret;
479 }
480 
481 inline bool processOnly(crow::App& app, crow::Response& res,
482                         std::function<void(crow::Response&)>& completionHandler)
483 {
484     BMCWEB_LOG_DEBUG("Processing only query param");
485     auto itMembers = res.jsonValue.find("Members");
486     if (itMembers == res.jsonValue.end())
487     {
488         messages::queryNotSupportedOnResource(res);
489         completionHandler(res);
490         return false;
491     }
492     auto itMemBegin = itMembers->begin();
493     if (itMemBegin == itMembers->end() || itMembers->size() != 1)
494     {
495         BMCWEB_LOG_DEBUG(
496             "Members contains {} element, returning full collection.",
497             itMembers->size());
498         completionHandler(res);
499         return false;
500     }
501 
502     auto itUrl = itMemBegin->find("@odata.id");
503     if (itUrl == itMemBegin->end())
504     {
505         BMCWEB_LOG_DEBUG("No found odata.id");
506         messages::internalError(res);
507         completionHandler(res);
508         return false;
509     }
510     const std::string* url = itUrl->get_ptr<const std::string*>();
511     if (url == nullptr)
512     {
513         BMCWEB_LOG_DEBUG("@odata.id wasn't a string????");
514         messages::internalError(res);
515         completionHandler(res);
516         return false;
517     }
518     // TODO(Ed) copy request headers?
519     // newReq.session = req.session;
520     std::error_code ec;
521     auto newReq = std::make_shared<crow::Request>(
522         crow::Request::Body{boost::beast::http::verb::get, *url, 11}, ec);
523     if (ec)
524     {
525         messages::internalError(res);
526         completionHandler(res);
527         return false;
528     }
529 
530     auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
531     BMCWEB_LOG_DEBUG("setting completion handler on {}",
532                      logPtr(&asyncResp->res));
533     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
534     app.handle(newReq, asyncResp);
535     return true;
536 }
537 
538 struct ExpandNode
539 {
540     nlohmann::json::json_pointer location;
541     std::string uri;
542 
543     bool operator==(const ExpandNode& other) const
544     {
545         return location == other.location && uri == other.uri;
546     }
547 };
548 
549 inline void findNavigationReferencesInArrayRecursive(
550     ExpandType eType, nlohmann::json::array_t& array,
551     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
552     bool inLinks, std::vector<ExpandNode>& out);
553 
554 inline void findNavigationReferencesInObjectRecursive(
555     ExpandType eType, nlohmann::json::object_t& obj,
556     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
557     bool inLinks, std::vector<ExpandNode>& out);
558 
559 // Walks a json object looking for Redfish NavigationReference entries that
560 // might need resolved.  It recursively walks the jsonResponse object, looking
561 // for links at every level, and returns a list (out) of locations within the
562 // tree that need to be expanded.  The current json pointer location p is passed
563 // in to reference the current node that's being expanded, so it can be combined
564 // with the keys from the jsonResponse object
565 inline void findNavigationReferencesRecursive(
566     ExpandType eType, nlohmann::json& jsonResponse,
567     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
568     bool inLinks, std::vector<ExpandNode>& out)
569 {
570     // If no expand is needed, return early
571     if (eType == ExpandType::None)
572     {
573         return;
574     }
575 
576     nlohmann::json::array_t* array =
577         jsonResponse.get_ptr<nlohmann::json::array_t*>();
578     if (array != nullptr)
579     {
580         findNavigationReferencesInArrayRecursive(eType, *array, jsonPtr, depth,
581                                                  skipDepth, inLinks, out);
582     }
583     nlohmann::json::object_t* obj =
584         jsonResponse.get_ptr<nlohmann::json::object_t*>();
585     if (obj == nullptr)
586     {
587         return;
588     }
589     findNavigationReferencesInObjectRecursive(eType, *obj, jsonPtr, depth,
590                                               skipDepth, inLinks, out);
591 }
592 
593 inline void findNavigationReferencesInArrayRecursive(
594     ExpandType eType, nlohmann::json::array_t& array,
595     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
596     bool inLinks, std::vector<ExpandNode>& out)
597 {
598     size_t index = 0;
599     // For arrays, walk every element in the array
600     for (auto& element : array)
601     {
602         nlohmann::json::json_pointer newPtr = jsonPtr / index;
603         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr.to_string());
604         findNavigationReferencesRecursive(eType, element, newPtr, depth,
605                                           skipDepth, inLinks, out);
606         index++;
607     }
608 }
609 
610 inline void findNavigationReferencesInObjectRecursive(
611     ExpandType eType, nlohmann::json::object_t& obj,
612     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
613     bool inLinks, std::vector<ExpandNode>& out)
614 {
615     // Navigation References only ever have a single element
616     if (obj.size() == 1)
617     {
618         if (obj.begin()->first == "@odata.id")
619         {
620             const std::string* uri =
621                 obj.begin()->second.get_ptr<const std::string*>();
622             if (uri != nullptr)
623             {
624                 BMCWEB_LOG_DEBUG("Found {} at {}", *uri, jsonPtr.to_string());
625                 if (skipDepth == 0)
626                 {
627                     out.push_back({jsonPtr, *uri});
628                 }
629                 return;
630             }
631         }
632     }
633 
634     int newDepth = depth;
635     auto odataId = obj.find("@odata.id");
636     if (odataId != obj.end())
637     {
638         // The Redfish spec requires all resources to include the resource
639         // identifier.  If the object has multiple elements and one of them is
640         // "@odata.id" then that means we have entered a new level / expanded
641         // resource.  We need to stop traversing if we're already at the desired
642         // depth
643         if (obj.size() > 1)
644         {
645             if (depth == 0)
646             {
647                 return;
648             }
649             if (skipDepth > 0)
650             {
651                 skipDepth--;
652             }
653         }
654 
655         if (skipDepth == 0)
656         {
657             newDepth--;
658         }
659     }
660 
661     // Loop the object and look for links
662     for (auto& element : obj)
663     {
664         bool localInLinks = inLinks;
665         if (!localInLinks)
666         {
667             // Check if this is a links node
668             localInLinks = element.first == "Links";
669         }
670         // Only traverse the parts of the tree the user asked for
671         // Per section 7.3 of the redfish specification
672         if (localInLinks && eType == ExpandType::NotLinks)
673         {
674             continue;
675         }
676         if (!localInLinks && eType == ExpandType::Links)
677         {
678             continue;
679         }
680         nlohmann::json::json_pointer newPtr = jsonPtr / element.first;
681         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr);
682 
683         findNavigationReferencesRecursive(eType, element.second, newPtr,
684                                           newDepth, skipDepth, localInLinks,
685                                           out);
686     }
687 }
688 
689 // TODO: When aggregation is enabled and we receive a partially expanded
690 // response we may need need additional handling when the original URI was
691 // up tree from a top level collection.
692 // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556
693 // lands.  May want to avoid forwarding query params when request is uptree from
694 // a top level collection.
695 inline std::vector<ExpandNode> findNavigationReferences(
696     ExpandType eType, int depth, int skipDepth, nlohmann::json& jsonResponse)
697 {
698     std::vector<ExpandNode> ret;
699     const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
700     // SkipDepth +1 since we are skipping the root by default.
701     findNavigationReferencesRecursive(eType, jsonResponse, root, depth,
702                                       skipDepth + 1, false, ret);
703     return ret;
704 }
705 
706 // Formats a query parameter string for the sub-query.
707 // Returns std::nullopt on failures.
708 // This function shall handle $select when it is added.
709 // There is no need to handle parameters that's not compatible with $expand,
710 // e.g., $only, since this function will only be called in side $expand handlers
711 inline std::optional<std::string> formatQueryForExpand(const Query& query)
712 {
713     // query.expandLevel<=1: no need to do subqueries
714     if (query.expandLevel <= 1)
715     {
716         return "";
717     }
718     std::string str = "?$expand=";
719     switch (query.expandType)
720     {
721         case ExpandType::Links:
722             str += '~';
723             break;
724         case ExpandType::NotLinks:
725             str += '.';
726             break;
727         case ExpandType::Both:
728             str += '*';
729             break;
730         case ExpandType::None:
731             return "";
732         default:
733             return std::nullopt;
734     }
735     str += "($levels=";
736     str += std::to_string(query.expandLevel - 1);
737     str += ')';
738     return str;
739 }
740 
741 // Propagates the worst error code to the final response.
742 // The order of error code is (from high to low)
743 // 500 Internal Server Error
744 // 511 Network Authentication Required
745 // 510 Not Extended
746 // 508 Loop Detected
747 // 507 Insufficient Storage
748 // 506 Variant Also Negotiates
749 // 505 HTTP Version Not Supported
750 // 504 Gateway Timeout
751 // 503 Service Unavailable
752 // 502 Bad Gateway
753 // 501 Not Implemented
754 // 401 Unauthorized
755 // 451 - 409 Error codes (not listed explicitly)
756 // 408 Request Timeout
757 // 407 Proxy Authentication Required
758 // 406 Not Acceptable
759 // 405 Method Not Allowed
760 // 404 Not Found
761 // 403 Forbidden
762 // 402 Payment Required
763 // 400 Bad Request
764 inline unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode)
765 {
766     // We keep a explicit list for error codes that this project often uses
767     // Higher priority codes are in lower indexes
768     constexpr std::array<unsigned, 13> orderedCodes = {
769         500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400};
770     size_t finalCodeIndex = std::numeric_limits<size_t>::max();
771     size_t subResponseCodeIndex = std::numeric_limits<size_t>::max();
772     for (size_t i = 0; i < orderedCodes.size(); ++i)
773     {
774         if (orderedCodes[i] == finalCode)
775         {
776             finalCodeIndex = i;
777         }
778         if (orderedCodes[i] == subResponseCode)
779         {
780             subResponseCodeIndex = i;
781         }
782     }
783     if (finalCodeIndex != std::numeric_limits<size_t>::max() &&
784         subResponseCodeIndex != std::numeric_limits<size_t>::max())
785     {
786         return finalCodeIndex <= subResponseCodeIndex
787                    ? finalCode
788                    : subResponseCode;
789     }
790     if (subResponseCode == 500 || finalCode == 500)
791     {
792         return 500;
793     }
794     if (subResponseCode > 500 || finalCode > 500)
795     {
796         return std::max(finalCode, subResponseCode);
797     }
798     if (subResponseCode == 401)
799     {
800         return subResponseCode;
801     }
802     return std::max(finalCode, subResponseCode);
803 }
804 
805 // Propagates all error messages into |finalResponse|
806 inline void propogateError(crow::Response& finalResponse,
807                            crow::Response& subResponse)
808 {
809     // no errors
810     if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400)
811     {
812         return;
813     }
814     messages::moveErrorsToErrorJson(finalResponse.jsonValue,
815                                     subResponse.jsonValue);
816     finalResponse.result(
817         propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt()));
818 }
819 
820 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
821 {
822   public:
823     // This object takes a single asyncResp object as the "final" one, then
824     // allows callers to attach sub-responses within the json tree that need
825     // to be executed and filled into their appropriate locations.  This
826     // class manages the final "merge" of the json resources.
827     MultiAsyncResp(crow::App& appIn,
828                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
829         app(appIn), finalRes(std::move(finalResIn))
830     {}
831 
832     void addAwaitingResponse(
833         const std::shared_ptr<bmcweb::AsyncResp>& res,
834         const nlohmann::json::json_pointer& finalExpandLocation)
835     {
836         res->res.setCompleteRequestHandler(std::bind_front(
837             placeResultStatic, shared_from_this(), finalExpandLocation));
838     }
839 
840     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
841                      crow::Response& res)
842     {
843         BMCWEB_LOG_DEBUG("placeResult for {}", locationToPlace);
844         propogateError(finalRes->res, res);
845         if (!res.jsonValue.is_object() || res.jsonValue.empty())
846         {
847             return;
848         }
849         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
850         finalObj = std::move(res.jsonValue);
851     }
852 
853     // Handles the very first level of Expand, and starts a chain of sub-queries
854     // for deeper levels.
855     void startQuery(const Query& query, const Query& delegated)
856     {
857         std::vector<ExpandNode> nodes = findNavigationReferences(
858             query.expandType, query.expandLevel, delegated.expandLevel,
859             finalRes->res.jsonValue);
860         BMCWEB_LOG_DEBUG("{} nodes to traverse", nodes.size());
861         const std::optional<std::string> queryStr = formatQueryForExpand(query);
862         if (!queryStr)
863         {
864             messages::internalError(finalRes->res);
865             return;
866         }
867         for (const ExpandNode& node : nodes)
868         {
869             const std::string subQuery = node.uri + *queryStr;
870             BMCWEB_LOG_DEBUG("URL of subquery:  {}", subQuery);
871             std::error_code ec;
872             auto newReq = std::make_shared<crow::Request>(
873                 crow::Request::Body{boost::beast::http::verb::get, subQuery,
874                                     11},
875                 ec);
876             if (ec)
877             {
878                 messages::internalError(finalRes->res);
879                 return;
880             }
881 
882             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
883             BMCWEB_LOG_DEBUG("setting completion handler on {}",
884                              logPtr(&asyncResp->res));
885 
886             addAwaitingResponse(asyncResp, node.location);
887             app.handle(newReq, asyncResp);
888         }
889     }
890 
891   private:
892     static void
893         placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi,
894                           const nlohmann::json::json_pointer& locationToPlace,
895                           crow::Response& res)
896     {
897         multi->placeResult(locationToPlace, res);
898     }
899 
900     crow::App& app;
901     std::shared_ptr<bmcweb::AsyncResp> finalRes;
902 };
903 
904 inline void processTopAndSkip(const Query& query, crow::Response& res)
905 {
906     if (!query.skip && !query.top)
907     {
908         // No work to do.
909         return;
910     }
911     nlohmann::json::object_t* obj =
912         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
913     if (obj == nullptr)
914     {
915         // Shouldn't be possible.  All responses should be objects.
916         messages::internalError(res);
917         return;
918     }
919 
920     BMCWEB_LOG_DEBUG("Handling top/skip");
921     nlohmann::json::object_t::iterator members = obj->find("Members");
922     if (members == obj->end())
923     {
924         // From the Redfish specification 7.3.1
925         // ... the HTTP 400 Bad Request status code with the
926         // QueryNotSupportedOnResource message from the Base Message Registry
927         // for any supported query parameters that apply only to resource
928         // collections but are used on singular resources.
929         messages::queryNotSupportedOnResource(res);
930         return;
931     }
932 
933     nlohmann::json::array_t* arr =
934         members->second.get_ptr<nlohmann::json::array_t*>();
935     if (arr == nullptr)
936     {
937         messages::internalError(res);
938         return;
939     }
940 
941     if (query.skip)
942     {
943         // Per section 7.3.1 of the Redfish specification, $skip is run before
944         // $top Can only skip as many values as we have
945         size_t skip = std::min(arr->size(), *query.skip);
946         arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
947     }
948     if (query.top)
949     {
950         size_t top = std::min(arr->size(), *query.top);
951         arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
952     }
953 }
954 
955 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are
956 // not in the |currNode| Trie node.
957 inline void recursiveSelect(nlohmann::json& currRoot,
958                             const SelectTrieNode& currNode)
959 {
960     nlohmann::json::object_t* object =
961         currRoot.get_ptr<nlohmann::json::object_t*>();
962     if (object != nullptr)
963     {
964         BMCWEB_LOG_DEBUG("Current JSON is an object");
965         auto it = currRoot.begin();
966         while (it != currRoot.end())
967         {
968             auto nextIt = std::next(it);
969             BMCWEB_LOG_DEBUG("key={}", it.key());
970             const SelectTrieNode* nextNode = currNode.find(it.key());
971             // Per the Redfish spec section 7.3.3, the service shall select
972             // certain properties as if $select was omitted. This applies to
973             // every TrieNode that contains leaves and the root.
974             constexpr std::array<std::string_view, 5> reservedProperties = {
975                 "@odata.id", "@odata.type", "@odata.context", "@odata.etag",
976                 "error"};
977             bool reserved = std::ranges::find(reservedProperties, it.key()) !=
978                             reservedProperties.end();
979             if (reserved || (nextNode != nullptr && nextNode->isSelected()))
980             {
981                 it = nextIt;
982                 continue;
983             }
984             if (nextNode != nullptr)
985             {
986                 BMCWEB_LOG_DEBUG("Recursively select: {}", it.key());
987                 recursiveSelect(*it, *nextNode);
988                 it = nextIt;
989                 continue;
990             }
991             BMCWEB_LOG_DEBUG("{} is getting removed!", it.key());
992             it = currRoot.erase(it);
993         }
994     }
995     nlohmann::json::array_t* array =
996         currRoot.get_ptr<nlohmann::json::array_t*>();
997     if (array != nullptr)
998     {
999         BMCWEB_LOG_DEBUG("Current JSON is an array");
1000         // Array index is omitted, so reuse the same Trie node
1001         for (nlohmann::json& nextRoot : *array)
1002         {
1003             recursiveSelect(nextRoot, currNode);
1004         }
1005     }
1006 }
1007 
1008 // The current implementation of $select still has the following TODOs due to
1009 //  ambiguity and/or complexity.
1010 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was
1011 // created for clarification.
1012 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*),
1013 // etc.
1014 inline void processSelect(crow::Response& intermediateResponse,
1015                           const SelectTrieNode& trieRoot)
1016 {
1017     BMCWEB_LOG_DEBUG("Process $select quary parameter");
1018     recursiveSelect(intermediateResponse.jsonValue, trieRoot);
1019 }
1020 
1021 inline void
1022     processAllParams(crow::App& app, const Query& query, const Query& delegated,
1023                      std::function<void(crow::Response&)>& completionHandler,
1024                      crow::Response& intermediateResponse)
1025 {
1026     if (!completionHandler)
1027     {
1028         BMCWEB_LOG_DEBUG("Function was invalid?");
1029         return;
1030     }
1031 
1032     BMCWEB_LOG_DEBUG("Processing query params");
1033     // If the request failed, there's no reason to even try to run query
1034     // params.
1035     if (intermediateResponse.resultInt() < 200 ||
1036         intermediateResponse.resultInt() >= 400)
1037     {
1038         completionHandler(intermediateResponse);
1039         return;
1040     }
1041     if (query.isOnly)
1042     {
1043         processOnly(app, intermediateResponse, completionHandler);
1044         return;
1045     }
1046 
1047     if (query.top || query.skip)
1048     {
1049         processTopAndSkip(query, intermediateResponse);
1050     }
1051 
1052     if (query.expandType != ExpandType::None)
1053     {
1054         BMCWEB_LOG_DEBUG("Executing expand query");
1055         auto asyncResp = std::make_shared<bmcweb::AsyncResp>(
1056             std::move(intermediateResponse));
1057 
1058         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
1059         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
1060         multi->startQuery(query, delegated);
1061         return;
1062     }
1063 
1064     if (query.filter)
1065     {
1066         applyFilterToCollection(intermediateResponse.jsonValue, *query.filter);
1067     }
1068 
1069     // According to Redfish Spec Section 7.3.1, $select is the last parameter to
1070     // to process
1071     if (!query.selectTrie.root.empty())
1072     {
1073         processSelect(intermediateResponse, query.selectTrie.root);
1074     }
1075 
1076     completionHandler(intermediateResponse);
1077 }
1078 
1079 } // namespace query_param
1080 } // namespace redfish
1081