xref: /openbmc/bmcweb/features/redfish/include/utils/query_param.hpp (revision ce8ea743055f1b82c60790db40aa3295e03bdf9c)
1 #pragma once
2 #include "app.hpp"
3 #include "async_resp.hpp"
4 #include "error_messages.hpp"
5 #include "http_request.hpp"
6 #include "http_response.hpp"
7 #include "routing.hpp"
8 
9 #include <charconv>
10 #include <string>
11 #include <string_view>
12 #include <utility>
13 #include <vector>
14 
15 namespace redfish
16 {
17 namespace query_param
18 {
19 inline constexpr size_t maxEntriesPerPage = 1000;
20 
21 enum class ExpandType : uint8_t
22 {
23     None,
24     Links,
25     NotLinks,
26     Both,
27 };
28 
29 // The struct stores the parsed query parameters of the default Redfish route.
30 struct Query
31 {
32     // Only
33     bool isOnly = false;
34     // Expand
35     uint8_t expandLevel = 0;
36     ExpandType expandType = ExpandType::None;
37 
38     // Skip
39     size_t skip = 0;
40 
41     // Top
42     size_t top = maxEntriesPerPage;
43 };
44 
45 // The struct defines how resource handlers in redfish-core/lib/ can handle
46 // query parameters themselves, so that the default Redfish route will delegate
47 // the processing.
48 struct QueryCapabilities
49 {
50     bool canDelegateOnly = false;
51     bool canDelegateTop = false;
52     bool canDelegateSkip = false;
53     uint8_t canDelegateExpandLevel = 0;
54 };
55 
56 // Delegates query parameters according to the given |queryCapabilities|
57 // This function doesn't check query parameter conflicts since the parse
58 // function will take care of it.
59 // Returns a delegated query object which can be used by individual resource
60 // handlers so that handlers don't need to query again.
61 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query)
62 {
63     Query delegated;
64     // delegate only
65     if (query.isOnly && queryCapabilities.canDelegateOnly)
66     {
67         delegated.isOnly = true;
68         query.isOnly = false;
69     }
70     // delegate expand as much as we can
71     if (query.expandType != ExpandType::None)
72     {
73         delegated.expandType = query.expandType;
74         if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel)
75         {
76             query.expandType = ExpandType::None;
77             delegated.expandLevel = query.expandLevel;
78             query.expandLevel = 0;
79         }
80         else
81         {
82             query.expandLevel -= queryCapabilities.canDelegateExpandLevel;
83             delegated.expandLevel = queryCapabilities.canDelegateExpandLevel;
84         }
85     }
86 
87     // delegate top
88     if (queryCapabilities.canDelegateTop)
89     {
90         delegated.top = query.top;
91         query.top = maxEntriesPerPage;
92     }
93 
94     // delegate skip
95     if (queryCapabilities.canDelegateSkip)
96     {
97         delegated.skip = query.skip;
98         query.skip = 0;
99     }
100     return delegated;
101 }
102 
103 inline bool getExpandType(std::string_view value, Query& query)
104 {
105     if (value.empty())
106     {
107         return false;
108     }
109     switch (value[0])
110     {
111         case '*':
112             query.expandType = ExpandType::Both;
113             break;
114         case '.':
115             query.expandType = ExpandType::NotLinks;
116             break;
117         case '~':
118             query.expandType = ExpandType::Links;
119             break;
120         default:
121             return false;
122 
123             break;
124     }
125     value.remove_prefix(1);
126     if (value.empty())
127     {
128         query.expandLevel = 1;
129         return true;
130     }
131     constexpr std::string_view levels = "($levels=";
132     if (!value.starts_with(levels))
133     {
134         return false;
135     }
136     value.remove_prefix(levels.size());
137 
138     auto it = std::from_chars(value.data(), value.data() + value.size(),
139                               query.expandLevel);
140     if (it.ec != std::errc())
141     {
142         return false;
143     }
144     value.remove_prefix(static_cast<size_t>(it.ptr - value.data()));
145     return value == ")";
146 }
147 
148 enum class QueryError
149 {
150     Ok,
151     OutOfRange,
152     ValueFormat,
153 };
154 
155 inline QueryError getNumericParam(std::string_view value, size_t& param)
156 {
157     std::from_chars_result r =
158         std::from_chars(value.data(), value.data() + value.size(), param);
159 
160     // If the number wasn't representable in the type, it's out of range
161     if (r.ec == std::errc::result_out_of_range)
162     {
163         return QueryError::OutOfRange;
164     }
165     // All other errors are value format
166     if (r.ec != std::errc())
167     {
168         return QueryError::ValueFormat;
169     }
170     return QueryError::Ok;
171 }
172 
173 inline QueryError getSkipParam(std::string_view value, Query& query)
174 {
175     return getNumericParam(value, query.skip);
176 }
177 
178 inline QueryError getTopParam(std::string_view value, Query& query)
179 {
180     QueryError ret = getNumericParam(value, query.top);
181     if (ret != QueryError::Ok)
182     {
183         return ret;
184     }
185 
186     // Range check for sanity.
187     if (query.top > maxEntriesPerPage)
188     {
189         return QueryError::OutOfRange;
190     }
191 
192     return QueryError::Ok;
193 }
194 
195 inline std::optional<Query>
196     parseParameters(const boost::urls::params_view& urlParams,
197                     crow::Response& res)
198 {
199     Query ret;
200     for (const boost::urls::params_view::value_type& it : urlParams)
201     {
202         std::string_view key(it.key.data(), it.key.size());
203         std::string_view value(it.value.data(), it.value.size());
204         if (key == "only")
205         {
206             if (!it.value.empty())
207             {
208                 messages::queryParameterValueFormatError(res, value, key);
209                 return std::nullopt;
210             }
211             ret.isOnly = true;
212         }
213         else if (key == "$expand" && bmcwebInsecureEnableQueryParams)
214         {
215             if (!getExpandType(value, ret))
216             {
217                 messages::queryParameterValueFormatError(res, value, key);
218                 return std::nullopt;
219             }
220         }
221         else if (key == "$top")
222         {
223             QueryError topRet = getTopParam(value, ret);
224             if (topRet == QueryError::ValueFormat)
225             {
226                 messages::queryParameterValueFormatError(res, value, key);
227                 return std::nullopt;
228             }
229             if (topRet == QueryError::OutOfRange)
230             {
231                 messages::queryParameterOutOfRange(
232                     res, value, "$top",
233                     "0-" + std::to_string(maxEntriesPerPage));
234                 return std::nullopt;
235             }
236         }
237         else if (key == "$skip")
238         {
239             QueryError topRet = getSkipParam(value, ret);
240             if (topRet == QueryError::ValueFormat)
241             {
242                 messages::queryParameterValueFormatError(res, value, key);
243                 return std::nullopt;
244             }
245             if (topRet == QueryError::OutOfRange)
246             {
247                 messages::queryParameterOutOfRange(
248                     res, value, key,
249                     "0-" + std::to_string(std::numeric_limits<size_t>::max()));
250                 return std::nullopt;
251             }
252         }
253         else
254         {
255             // Intentionally ignore other errors Redfish spec, 7.3.1
256             if (key.starts_with("$"))
257             {
258                 // Services shall return... The HTTP 501 Not Implemented
259                 // status code for any unsupported query parameters that
260                 // start with $ .
261                 messages::queryParameterValueFormatError(res, value, key);
262                 res.result(boost::beast::http::status::not_implemented);
263                 return std::nullopt;
264             }
265             // "Shall ignore unknown or unsupported query parameters that do
266             // not begin with $ ."
267         }
268     }
269 
270     return ret;
271 }
272 
273 inline bool processOnly(crow::App& app, crow::Response& res,
274                         std::function<void(crow::Response&)>& completionHandler)
275 {
276     BMCWEB_LOG_DEBUG << "Processing only query param";
277     auto itMembers = res.jsonValue.find("Members");
278     if (itMembers == res.jsonValue.end())
279     {
280         messages::queryNotSupportedOnResource(res);
281         completionHandler(res);
282         return false;
283     }
284     auto itMemBegin = itMembers->begin();
285     if (itMemBegin == itMembers->end() || itMembers->size() != 1)
286     {
287         BMCWEB_LOG_DEBUG << "Members contains " << itMembers->size()
288                          << " element, returning full collection.";
289         completionHandler(res);
290         return false;
291     }
292 
293     auto itUrl = itMemBegin->find("@odata.id");
294     if (itUrl == itMemBegin->end())
295     {
296         BMCWEB_LOG_DEBUG << "No found odata.id";
297         messages::internalError(res);
298         completionHandler(res);
299         return false;
300     }
301     const std::string* url = itUrl->get_ptr<const std::string*>();
302     if (url == nullptr)
303     {
304         BMCWEB_LOG_DEBUG << "@odata.id wasn't a string????";
305         messages::internalError(res);
306         completionHandler(res);
307         return false;
308     }
309     // TODO(Ed) copy request headers?
310     // newReq.session = req.session;
311     std::error_code ec;
312     crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec);
313     if (ec)
314     {
315         messages::internalError(res);
316         completionHandler(res);
317         return false;
318     }
319 
320     auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
321     BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res;
322     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
323     asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper());
324     app.handle(newReq, asyncResp);
325     return true;
326 }
327 
328 struct ExpandNode
329 {
330     nlohmann::json::json_pointer location;
331     std::string uri;
332 
333     inline bool operator==(const ExpandNode& other) const
334     {
335         return location == other.location && uri == other.uri;
336     }
337 };
338 
339 // Walks a json object looking for Redfish NavigationReference entries that
340 // might need resolved.  It recursively walks the jsonResponse object, looking
341 // for links at every level, and returns a list (out) of locations within the
342 // tree that need to be expanded.  The current json pointer location p is passed
343 // in to reference the current node that's being expanded, so it can be combined
344 // with the keys from the jsonResponse object
345 inline void findNavigationReferencesRecursive(
346     ExpandType eType, nlohmann::json& jsonResponse,
347     const nlohmann::json::json_pointer& p, bool inLinks,
348     std::vector<ExpandNode>& out)
349 {
350     // If no expand is needed, return early
351     if (eType == ExpandType::None)
352     {
353         return;
354     }
355     nlohmann::json::array_t* array =
356         jsonResponse.get_ptr<nlohmann::json::array_t*>();
357     if (array != nullptr)
358     {
359         size_t index = 0;
360         // For arrays, walk every element in the array
361         for (auto& element : *array)
362         {
363             nlohmann::json::json_pointer newPtr = p / index;
364             BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr.to_string();
365             findNavigationReferencesRecursive(eType, element, newPtr, inLinks,
366                                               out);
367             index++;
368         }
369     }
370     nlohmann::json::object_t* obj =
371         jsonResponse.get_ptr<nlohmann::json::object_t*>();
372     if (obj == nullptr)
373     {
374         return;
375     }
376     // Navigation References only ever have a single element
377     if (obj->size() == 1)
378     {
379         if (obj->begin()->first == "@odata.id")
380         {
381             const std::string* uri =
382                 obj->begin()->second.get_ptr<const std::string*>();
383             if (uri != nullptr)
384             {
385                 BMCWEB_LOG_DEBUG << "Found element at " << p.to_string();
386                 out.push_back({p, *uri});
387             }
388         }
389     }
390     // Loop the object and look for links
391     for (auto& element : *obj)
392     {
393         bool localInLinks = inLinks;
394         if (!localInLinks)
395         {
396             // Check if this is a links node
397             localInLinks = element.first == "Links";
398         }
399         // Only traverse the parts of the tree the user asked for
400         // Per section 7.3 of the redfish specification
401         if (localInLinks && eType == ExpandType::NotLinks)
402         {
403             continue;
404         }
405         if (!localInLinks && eType == ExpandType::Links)
406         {
407             continue;
408         }
409         nlohmann::json::json_pointer newPtr = p / element.first;
410         BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr;
411 
412         findNavigationReferencesRecursive(eType, element.second, newPtr,
413                                           localInLinks, out);
414     }
415 }
416 
417 inline std::vector<ExpandNode>
418     findNavigationReferences(ExpandType eType, nlohmann::json& jsonResponse)
419 {
420     std::vector<ExpandNode> ret;
421     const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
422     findNavigationReferencesRecursive(eType, jsonResponse, root, false, ret);
423     return ret;
424 }
425 
426 // Formats a query parameter string for the sub-query.
427 // Returns std::nullopt on failures.
428 // This function shall handle $select when it is added.
429 // There is no need to handle parameters that's not campatible with $expand,
430 // e.g., $only, since this function will only be called in side $expand handlers
431 inline std::optional<std::string> formatQueryForExpand(const Query& query)
432 {
433     // query.expandLevel<=1: no need to do subqueries
434     if (query.expandLevel <= 1)
435     {
436         return "";
437     }
438     std::string str = "?$expand=";
439     bool queryTypeExpected = false;
440     switch (query.expandType)
441     {
442         case ExpandType::None:
443             return "";
444         case ExpandType::Links:
445             queryTypeExpected = true;
446             str += '~';
447             break;
448         case ExpandType::NotLinks:
449             queryTypeExpected = true;
450             str += '.';
451             break;
452         case ExpandType::Both:
453             queryTypeExpected = true;
454             str += '*';
455             break;
456     }
457     if (!queryTypeExpected)
458     {
459         return std::nullopt;
460     }
461     str += "($levels=";
462     str += std::to_string(query.expandLevel - 1);
463     str += ')';
464     return str;
465 }
466 
467 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
468 {
469   public:
470     // This object takes a single asyncResp object as the "final" one, then
471     // allows callers to attach sub-responses within the json tree that need
472     // to be executed and filled into their appropriate locations.  This
473     // class manages the final "merge" of the json resources.
474     MultiAsyncResp(crow::App& appIn,
475                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
476         app(appIn),
477         finalRes(std::move(finalResIn))
478     {}
479 
480     void addAwaitingResponse(
481         const std::shared_ptr<bmcweb::AsyncResp>& res,
482         const nlohmann::json::json_pointer& finalExpandLocation)
483     {
484         res->res.setCompleteRequestHandler(std::bind_front(
485             placeResultStatic, shared_from_this(), finalExpandLocation));
486     }
487 
488     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
489                      crow::Response& res)
490     {
491         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
492         finalObj = std::move(res.jsonValue);
493     }
494 
495     // Handles the very first level of Expand, and starts a chain of sub-queries
496     // for deeper levels.
497     void startQuery(const Query& query)
498     {
499         std::vector<ExpandNode> nodes =
500             findNavigationReferences(query.expandType, finalRes->res.jsonValue);
501         BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse";
502         const std::optional<std::string> queryStr = formatQueryForExpand(query);
503         if (!queryStr)
504         {
505             messages::internalError(finalRes->res);
506             return;
507         }
508         for (const ExpandNode& node : nodes)
509         {
510             const std::string subQuery = node.uri + *queryStr;
511             BMCWEB_LOG_DEBUG << "URL of subquery:  " << subQuery;
512             std::error_code ec;
513             crow::Request newReq({boost::beast::http::verb::get, subQuery, 11},
514                                  ec);
515             if (ec)
516             {
517                 messages::internalError(finalRes->res);
518                 return;
519             }
520 
521             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
522             BMCWEB_LOG_DEBUG << "setting completion handler on "
523                              << &asyncResp->res;
524 
525             addAwaitingResponse(asyncResp, node.location);
526             app.handle(newReq, asyncResp);
527         }
528     }
529 
530   private:
531     static void
532         placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi,
533                           const nlohmann::json::json_pointer& locationToPlace,
534                           crow::Response& res)
535     {
536         multi->placeResult(locationToPlace, res);
537     }
538 
539     crow::App& app;
540     std::shared_ptr<bmcweb::AsyncResp> finalRes;
541 };
542 
543 inline void processTopAndSkip(const Query& query, crow::Response& res)
544 {
545     nlohmann::json::object_t* obj =
546         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
547     if (obj == nullptr)
548     {
549         // Shouldn't be possible.  All responses should be objects.
550         messages::internalError(res);
551         return;
552     }
553 
554     BMCWEB_LOG_DEBUG << "Handling top/skip";
555     nlohmann::json::object_t::iterator members = obj->find("Members");
556     if (members == obj->end())
557     {
558         // From the Redfish specification 7.3.1
559         // ... the HTTP 400 Bad Request status code with the
560         // QueryNotSupportedOnResource message from the Base Message Registry
561         // for any supported query parameters that apply only to resource
562         // collections but are used on singular resources.
563         messages::queryNotSupportedOnResource(res);
564         return;
565     }
566 
567     nlohmann::json::array_t* arr =
568         members->second.get_ptr<nlohmann::json::array_t*>();
569     if (arr == nullptr)
570     {
571         messages::internalError(res);
572         return;
573     }
574 
575     // Per section 7.3.1 of the Redfish specification, $skip is run before $top
576     // Can only skip as many values as we have
577     size_t skip = std::min(arr->size(), query.skip);
578     arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
579 
580     size_t top = std::min(arr->size(), query.top);
581     arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
582 }
583 
584 inline void
585     processAllParams(crow::App& app, const Query& query,
586                      std::function<void(crow::Response&)>& completionHandler,
587                      crow::Response& intermediateResponse)
588 {
589     if (!completionHandler)
590     {
591         BMCWEB_LOG_DEBUG << "Function was invalid?";
592         return;
593     }
594 
595     BMCWEB_LOG_DEBUG << "Processing query params";
596     // If the request failed, there's no reason to even try to run query
597     // params.
598     if (intermediateResponse.resultInt() < 200 ||
599         intermediateResponse.resultInt() >= 400)
600     {
601         completionHandler(intermediateResponse);
602         return;
603     }
604     if (query.isOnly)
605     {
606         processOnly(app, intermediateResponse, completionHandler);
607         return;
608     }
609 
610     if (query.top <= maxEntriesPerPage || query.skip != 0)
611     {
612         processTopAndSkip(query, intermediateResponse);
613     }
614 
615     if (query.expandType != ExpandType::None)
616     {
617         BMCWEB_LOG_DEBUG << "Executing expand query";
618         // TODO(ed) this is a copy of the response object.  Admittedly,
619         // we're inherently doing something inefficient, but we shouldn't
620         // have to do a full copy
621         auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
622         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
623         asyncResp->res.jsonValue = std::move(intermediateResponse.jsonValue);
624         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
625 
626         multi->startQuery(query);
627         return;
628     }
629     completionHandler(intermediateResponse);
630 }
631 
632 } // namespace query_param
633 } // namespace redfish
634