xref: /openbmc/bmcweb/redfish-core/include/utils/query_param.hpp (revision 76c2ad64440e2864a0cc3d66a3b8bb0f5ac2f288)
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     const nlohmann::json::object_t* obj =
490         res.jsonValue.get_ptr<const nlohmann::json::object_t*>();
491     if (obj == nullptr)
492     {
493         messages::queryNotSupportedOnResource(res);
494         completionHandler(res);
495         return false;
496     }
497     auto itMembers = obj->find("Members");
498     if (itMembers == obj->end())
499     {
500         messages::queryNotSupportedOnResource(res);
501         completionHandler(res);
502         return false;
503     }
504     const nlohmann::json::array_t* arrMembers =
505         itMembers->second.get_ptr<const nlohmann::json::array_t*>();
506     if (arrMembers == nullptr)
507     {
508         messages::queryNotSupportedOnResource(res);
509         completionHandler(res);
510         return false;
511     }
512     auto itMemBegin = arrMembers->begin();
513     if (itMemBegin == arrMembers->end() || arrMembers->size() != 1)
514     {
515         BMCWEB_LOG_DEBUG(
516             "Members contains {} element, returning full collection.",
517             arrMembers->size());
518         completionHandler(res);
519         return false;
520     }
521 
522     const nlohmann::json::object_t* member =
523         itMemBegin->get_ptr<const nlohmann::json::object_t*>();
524     if (member == nullptr)
525     {
526         messages::internalError(res);
527         completionHandler(res);
528         return false;
529     }
530     auto itUrl = member->find("@odata.id");
531     if (itUrl == member->end())
532     {
533         BMCWEB_LOG_DEBUG("No found odata.id");
534         messages::internalError(res);
535         completionHandler(res);
536         return false;
537     }
538     const std::string* url = itUrl->second.get_ptr<const std::string*>();
539     if (url == nullptr)
540     {
541         BMCWEB_LOG_DEBUG("@odata.id wasn't a string????");
542         messages::internalError(res);
543         completionHandler(res);
544         return false;
545     }
546     // TODO(Ed) copy request headers?
547     // newReq.session = req.session;
548     std::error_code ec;
549     auto newReq = std::make_shared<crow::Request>(
550         crow::Request::Body{boost::beast::http::verb::get, *url, 11}, ec);
551     if (ec)
552     {
553         messages::internalError(res);
554         completionHandler(res);
555         return false;
556     }
557 
558     auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
559     BMCWEB_LOG_DEBUG("setting completion handler on {}",
560                      logPtr(&asyncResp->res));
561     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
562     app.handle(newReq, asyncResp);
563     return true;
564 }
565 
566 struct ExpandNode
567 {
568     nlohmann::json::json_pointer location;
569     std::string uri;
570 
operator ==redfish::query_param::ExpandNode571     bool operator==(const ExpandNode& other) const
572     {
573         return location == other.location && uri == other.uri;
574     }
575 };
576 
577 inline void findNavigationReferencesInArrayRecursive(
578     ExpandType eType, nlohmann::json::array_t& array,
579     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
580     bool inLinks, std::vector<ExpandNode>& out);
581 
582 inline void findNavigationReferencesInObjectRecursive(
583     ExpandType eType, nlohmann::json::object_t& obj,
584     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
585     bool inLinks, std::vector<ExpandNode>& out);
586 
587 // Walks a json object looking for Redfish NavigationReference entries that
588 // might need resolved.  It recursively walks the jsonResponse object, looking
589 // for links at every level, and returns a list (out) of locations within the
590 // tree that need to be expanded.  The current json pointer location p is passed
591 // in to reference the current node that's being expanded, so it can be combined
592 // 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)593 inline void findNavigationReferencesRecursive(
594     ExpandType eType, nlohmann::json& jsonResponse,
595     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
596     bool inLinks, std::vector<ExpandNode>& out)
597 {
598     // If no expand is needed, return early
599     if (eType == ExpandType::None)
600     {
601         return;
602     }
603 
604     nlohmann::json::array_t* array =
605         jsonResponse.get_ptr<nlohmann::json::array_t*>();
606     if (array != nullptr)
607     {
608         findNavigationReferencesInArrayRecursive(eType, *array, jsonPtr, depth,
609                                                  skipDepth, inLinks, out);
610     }
611     nlohmann::json::object_t* obj =
612         jsonResponse.get_ptr<nlohmann::json::object_t*>();
613     if (obj == nullptr)
614     {
615         return;
616     }
617     findNavigationReferencesInObjectRecursive(eType, *obj, jsonPtr, depth,
618                                               skipDepth, inLinks, out);
619 }
620 
findNavigationReferencesInArrayRecursive(ExpandType eType,nlohmann::json::array_t & array,const nlohmann::json::json_pointer & jsonPtr,int depth,int skipDepth,bool inLinks,std::vector<ExpandNode> & out)621 inline void findNavigationReferencesInArrayRecursive(
622     ExpandType eType, nlohmann::json::array_t& array,
623     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
624     bool inLinks, std::vector<ExpandNode>& out)
625 {
626     size_t index = 0;
627     // For arrays, walk every element in the array
628     for (auto& element : array)
629     {
630         nlohmann::json::json_pointer newPtr = jsonPtr / index;
631         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr.to_string());
632         findNavigationReferencesRecursive(eType, element, newPtr, depth,
633                                           skipDepth, inLinks, out);
634         index++;
635     }
636 }
637 
findNavigationReferencesInObjectRecursive(ExpandType eType,nlohmann::json::object_t & obj,const nlohmann::json::json_pointer & jsonPtr,int depth,int skipDepth,bool inLinks,std::vector<ExpandNode> & out)638 inline void findNavigationReferencesInObjectRecursive(
639     ExpandType eType, nlohmann::json::object_t& obj,
640     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
641     bool inLinks, std::vector<ExpandNode>& out)
642 {
643     // Navigation References only ever have a single element
644     if (obj.size() == 1)
645     {
646         if (obj.begin()->first == "@odata.id")
647         {
648             const std::string* uri =
649                 obj.begin()->second.get_ptr<const std::string*>();
650             if (uri != nullptr)
651             {
652                 BMCWEB_LOG_DEBUG("Found {} at {}", *uri, jsonPtr.to_string());
653                 if (skipDepth == 0)
654                 {
655                     out.push_back({jsonPtr, *uri});
656                 }
657                 return;
658             }
659         }
660     }
661 
662     int newDepth = depth;
663     auto odataId = obj.find("@odata.id");
664     if (odataId != obj.end())
665     {
666         // The Redfish spec requires all resources to include the resource
667         // identifier.  If the object has multiple elements and one of them is
668         // "@odata.id" then that means we have entered a new level / expanded
669         // resource.  We need to stop traversing if we're already at the desired
670         // depth
671         if (obj.size() > 1)
672         {
673             if (depth == 0)
674             {
675                 return;
676             }
677             if (skipDepth > 0)
678             {
679                 skipDepth--;
680             }
681         }
682 
683         if (skipDepth == 0)
684         {
685             newDepth--;
686         }
687     }
688 
689     // Loop the object and look for links
690     for (auto& element : obj)
691     {
692         bool localInLinks = inLinks;
693         if (!localInLinks)
694         {
695             // Check if this is a links node
696             localInLinks = element.first == "Links";
697         }
698         // Only traverse the parts of the tree the user asked for
699         // Per section 7.3 of the redfish specification
700         if (localInLinks && eType == ExpandType::NotLinks)
701         {
702             continue;
703         }
704         if (!localInLinks && eType == ExpandType::Links)
705         {
706             continue;
707         }
708         nlohmann::json::json_pointer newPtr = jsonPtr / element.first;
709         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr);
710 
711         findNavigationReferencesRecursive(eType, element.second, newPtr,
712                                           newDepth, skipDepth, localInLinks,
713                                           out);
714     }
715 }
716 
717 // TODO: When aggregation is enabled and we receive a partially expanded
718 // response we may need need additional handling when the original URI was
719 // up tree from a top level collection.
720 // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556
721 // lands.  May want to avoid forwarding query params when request is uptree from
722 // a top level collection.
findNavigationReferences(ExpandType eType,int depth,int skipDepth,nlohmann::json & jsonResponse)723 inline std::vector<ExpandNode> findNavigationReferences(
724     ExpandType eType, int depth, int skipDepth, nlohmann::json& jsonResponse)
725 {
726     std::vector<ExpandNode> ret;
727     const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
728     // SkipDepth +1 since we are skipping the root by default.
729     findNavigationReferencesRecursive(eType, jsonResponse, root, depth,
730                                       skipDepth + 1, false, ret);
731     return ret;
732 }
733 
734 // Formats a query parameter string for the sub-query.
735 // Returns std::nullopt on failures.
736 // This function shall handle $select when it is added.
737 // There is no need to handle parameters that's not compatible with $expand,
738 // e.g., $only, since this function will only be called in side $expand handlers
formatQueryForExpand(const Query & query)739 inline std::optional<std::string> formatQueryForExpand(const Query& query)
740 {
741     // query.expandLevel<=1: no need to do subqueries
742     if (query.expandLevel <= 1)
743     {
744         return "";
745     }
746     std::string str = "?$expand=";
747     switch (query.expandType)
748     {
749         case ExpandType::Links:
750             str += '~';
751             break;
752         case ExpandType::NotLinks:
753             str += '.';
754             break;
755         case ExpandType::Both:
756             str += '*';
757             break;
758         case ExpandType::None:
759             return "";
760         default:
761             return std::nullopt;
762     }
763     str += "($levels=";
764     str += std::to_string(query.expandLevel - 1);
765     str += ')';
766     return str;
767 }
768 
769 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
770 {
771   public:
772     // This object takes a single asyncResp object as the "final" one, then
773     // allows callers to attach sub-responses within the json tree that need
774     // to be executed and filled into their appropriate locations.  This
775     // class manages the final "merge" of the json resources.
MultiAsyncResp(crow::App & appIn,std::shared_ptr<bmcweb::AsyncResp> finalResIn)776     MultiAsyncResp(crow::App& appIn,
777                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
778         app(&appIn), finalRes(std::move(finalResIn))
779     {}
780 
MultiAsyncResp(std::shared_ptr<bmcweb::AsyncResp> finalResIn)781     explicit MultiAsyncResp(std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
782         app(nullptr), finalRes(std::move(finalResIn))
783     {}
784 
addAwaitingResponse(const std::shared_ptr<bmcweb::AsyncResp> & res,const nlohmann::json::json_pointer & finalExpandLocation)785     void addAwaitingResponse(
786         const std::shared_ptr<bmcweb::AsyncResp>& res,
787         const nlohmann::json::json_pointer& finalExpandLocation)
788     {
789         res->res.setCompleteRequestHandler(std::bind_front(
790             placeResultStatic, shared_from_this(), finalExpandLocation));
791     }
792 
placeResult(const nlohmann::json::json_pointer & locationToPlace,crow::Response & res)793     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
794                      crow::Response& res)
795     {
796         BMCWEB_LOG_DEBUG("placeResult for {}", locationToPlace);
797         propogateError(finalRes->res, res);
798         nlohmann::json::object_t* obj =
799             res.jsonValue.get_ptr<nlohmann::json::object_t*>();
800         if (obj == nullptr || res.jsonValue.empty())
801         {
802             return;
803         }
804         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
805         finalObj = std::move(*obj);
806     }
807 
808     // Handles the very first level of Expand, and starts a chain of sub-queries
809     // for deeper levels.
startQuery(const Query & query,const Query & delegated,const crow::Request & req)810     void startQuery(const Query& query, const Query& delegated,
811                     const crow::Request& req)
812     {
813         std::vector<ExpandNode> nodes = findNavigationReferences(
814             query.expandType, query.expandLevel, delegated.expandLevel,
815             finalRes->res.jsonValue);
816         BMCWEB_LOG_DEBUG("{} nodes to traverse", nodes.size());
817         const std::optional<std::string> queryStr = formatQueryForExpand(query);
818         if (!queryStr)
819         {
820             messages::internalError(finalRes->res);
821             return;
822         }
823         for (const ExpandNode& node : nodes)
824         {
825             const std::string subQuery = node.uri + *queryStr;
826             BMCWEB_LOG_DEBUG("URL of subquery:  {}", subQuery);
827             std::error_code ec;
828             auto newReq = std::make_shared<crow::Request>(
829                 crow::Request::Body{boost::beast::http::verb::get, subQuery,
830                                     11},
831                 ec);
832             if (ec)
833             {
834                 messages::internalError(finalRes->res);
835                 return;
836             }
837 
838             if (req.session == nullptr)
839             {
840                 BMCWEB_LOG_ERROR("Session is null");
841                 messages::internalError(finalRes->res);
842                 return;
843             }
844             // Share the session from the original request
845             newReq->session = req.session;
846 
847             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
848             BMCWEB_LOG_DEBUG("setting completion handler on {}",
849                              logPtr(&asyncResp->res));
850 
851             addAwaitingResponse(asyncResp, node.location);
852             if (app != nullptr)
853             {
854                 app->handle(newReq, asyncResp);
855             }
856         }
857     }
858 
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)859     static void startMultiFragmentHandle(
860         const std::shared_ptr<redfish::SubRequest>& req,
861         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
862         const std::shared_ptr<std::vector<OemBaseRule*>>& fragments,
863         const std::shared_ptr<std::vector<std::string>>& params,
864         const crow::Response& resIn)
865     {
866         asyncResp->res.jsonValue = resIn.jsonValue;
867         auto multi = std::make_shared<MultiAsyncResp>(asyncResp);
868         for (OemBaseRule* fragment : *fragments)
869         {
870             if (fragment != nullptr)
871             {
872                 OemBaseRule& fragmentRule = *fragment;
873                 auto rsp = std::make_shared<bmcweb::AsyncResp>();
874                 BMCWEB_LOG_DEBUG("Matched fragment rule '{}' method '{}'",
875                                  fragmentRule.rule,
876                                  boost::beast::http::to_string(req->method()));
877                 BMCWEB_LOG_DEBUG(
878                     "Handling fragment rules: setting completion handler on {}",
879                     logPtr(&rsp->res));
880                 std::optional<nlohmann::json::json_pointer> jsonFragmentPtr =
881                     json_util::createJsonPointerFromFragment(fragmentRule.rule);
882                 if (jsonFragmentPtr)
883                 {
884                     multi->addAwaitingResponse(rsp, *jsonFragmentPtr);
885                     fragmentRule.handle(*req, rsp, *params);
886                 }
887             }
888         }
889     }
890 
891   private:
placeResultStatic(const std::shared_ptr<MultiAsyncResp> & multi,const nlohmann::json::json_pointer & locationToPlace,crow::Response & res)892     static void placeResultStatic(
893         const std::shared_ptr<MultiAsyncResp>& multi,
894         const nlohmann::json::json_pointer& locationToPlace,
895         crow::Response& res)
896     {
897         multi->placeResult(locationToPlace, res);
898     }
899 
900     crow::App* app;
901     std::shared_ptr<bmcweb::AsyncResp> finalRes;
902 };
903 
processTopAndSkip(const Query & query,crow::Response & res)904 inline void processTopAndSkip(const Query& query, crow::Response& res)
905 {
906     if (!query.skip && !query.top)
907     {
908         // No work to do.
909         return;
910     }
911     nlohmann::json::object_t* obj =
912         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
913     if (obj == nullptr)
914     {
915         // Shouldn't be possible.  All responses should be objects.
916         messages::internalError(res);
917         return;
918     }
919 
920     BMCWEB_LOG_DEBUG("Handling top/skip");
921     nlohmann::json::object_t::iterator members = obj->find("Members");
922     if (members == obj->end())
923     {
924         // From the Redfish specification 7.3.1
925         // ... the HTTP 400 Bad Request status code with the
926         // QueryNotSupportedOnResource message from the Base Message Registry
927         // for any supported query parameters that apply only to resource
928         // collections but are used on singular resources.
929         messages::queryNotSupportedOnResource(res);
930         return;
931     }
932 
933     nlohmann::json::array_t* arr =
934         members->second.get_ptr<nlohmann::json::array_t*>();
935     if (arr == nullptr)
936     {
937         messages::internalError(res);
938         return;
939     }
940 
941     if (query.skip)
942     {
943         // Per section 7.3.1 of the Redfish specification, $skip is run before
944         // $top Can only skip as many values as we have
945         size_t skip = std::min(arr->size(), *query.skip);
946         arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
947     }
948     if (query.top)
949     {
950         size_t top = std::min(arr->size(), *query.top);
951         arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
952     }
953 }
954 
955 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are
956 // not in the |currNode| Trie node.
recursiveSelect(nlohmann::json & currRoot,const SelectTrieNode & currNode)957 inline void recursiveSelect(nlohmann::json& currRoot,
958                             const SelectTrieNode& currNode)
959 {
960     nlohmann::json::object_t* object =
961         currRoot.get_ptr<nlohmann::json::object_t*>();
962     if (object != nullptr)
963     {
964         BMCWEB_LOG_DEBUG("Current JSON is an object");
965         auto it = object->begin();
966         while (it != object->end())
967         {
968             auto nextIt = std::next(it);
969             BMCWEB_LOG_DEBUG("key={}", it->first);
970             const SelectTrieNode* nextNode = currNode.find(it->first);
971             // Per the Redfish spec section 7.3.3, the service shall select
972             // certain properties as if $select was omitted. This applies to
973             // every TrieNode that contains leaves and the root.
974             constexpr std::array<std::string_view, 5> reservedProperties = {
975                 "@odata.id", "@odata.type", "@odata.context", "@odata.etag",
976                 "error"};
977             bool reserved = std::ranges::find(reservedProperties, it->first) !=
978                             reservedProperties.end();
979             if (reserved || (nextNode != nullptr && nextNode->isSelected()))
980             {
981                 it = nextIt;
982                 continue;
983             }
984             if (nextNode != nullptr)
985             {
986                 BMCWEB_LOG_DEBUG("Recursively select: {}", it->first);
987                 recursiveSelect(it->second, *nextNode);
988                 it = nextIt;
989                 continue;
990             }
991             BMCWEB_LOG_DEBUG("{} is getting removed!", it->first);
992             it = object->erase(it);
993         }
994         return;
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 
processAllParams(crow::App & app,const Query & query,const Query & delegated,std::function<void (crow::Response &)> & completionHandler,crow::Response & intermediateResponse,const crow::Request & req)1022 inline void processAllParams(
1023     crow::App& app, const Query& query, const Query& delegated,
1024     std::function<void(crow::Response&)>& completionHandler,
1025     crow::Response& intermediateResponse, const crow::Request& req)
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, req);
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