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