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