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