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