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