xref: /openbmc/bmcweb/redfish-core/lib/event_service.hpp (revision e56f254c351e7e2ead2b895eb6117ff6cdf05bdf)
1 /*
2 // Copyright (c) 2020 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 #pragma once
17 #include "event_service_manager.hpp"
18 
19 namespace redfish
20 {
21 
22 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = {
23     eventFormatType, metricReportFormatType};
24 static constexpr const std::array<const char*, 3> supportedRegPrefixes = {
25     "Base", "OpenBMC", "Task"};
26 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
27     "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
28 
29 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
30 static constexpr const std::array<const char*, 2> supportedResourceTypes = {
31     "IBMConfigFile", "Task"};
32 #else
33 static constexpr const std::array<const char*, 1> supportedResourceTypes = {
34     "Task"};
35 #endif
36 
37 static constexpr const uint8_t maxNoOfSubscriptions = 20;
38 
39 class EventService : public Node
40 {
41   public:
42     EventService(CrowApp& app) : Node(app, "/redfish/v1/EventService/")
43     {
44         entityPrivileges = {
45             {boost::beast::http::verb::get, {{"Login"}}},
46             {boost::beast::http::verb::head, {{"Login"}}},
47             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
48             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
49             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
50             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
51     }
52 
53   private:
54     void doGet(crow::Response& res, const crow::Request& req,
55                const std::vector<std::string>& params) override
56     {
57         auto asyncResp = std::make_shared<AsyncResp>(res);
58         res.jsonValue = {
59             {"@odata.type", "#EventService.v1_5_0.EventService"},
60             {"Id", "EventService"},
61             {"Name", "Event Service"},
62             {"ServerSentEventUri",
63              "/redfish/v1/EventService/Subscriptions/SSE"},
64             {"Subscriptions",
65              {{"@odata.id", "/redfish/v1/EventService/Subscriptions"}}},
66             {"Actions",
67              {{"#EventService.SubmitTestEvent",
68                {{"target", "/redfish/v1/EventService/Actions/"
69                            "EventService.SubmitTestEvent"}}}}},
70             {"@odata.id", "/redfish/v1/EventService"}};
71 
72         const auto& [enabled, retryAttempts, retryTimeoutInterval] =
73             EventServiceManager::getInstance().getEventServiceConfig();
74 
75         asyncResp->res.jsonValue["Status"]["State"] =
76             (enabled ? "Enabled" : "Disabled");
77         asyncResp->res.jsonValue["ServiceEnabled"] = enabled;
78         asyncResp->res.jsonValue["DeliveryRetryAttempts"] = retryAttempts;
79         asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
80             retryTimeoutInterval;
81         asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes;
82         asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
83         asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes;
84 
85         nlohmann::json supportedSSEFilters = {
86             {"EventFormatType", true},        {"MessageId", true},
87             {"MetricReportDefinition", true}, {"RegistryPrefix", true},
88             {"OriginResource", false},        {"ResourceType", false}};
89 
90         asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
91             supportedSSEFilters;
92     }
93 
94     void doPatch(crow::Response& res, const crow::Request& req,
95                  const std::vector<std::string>& params) override
96     {
97         auto asyncResp = std::make_shared<AsyncResp>(res);
98 
99         std::optional<bool> serviceEnabled;
100         std::optional<uint32_t> retryAttemps;
101         std::optional<uint32_t> retryInterval;
102 
103         if (!json_util::readJson(req, res, "ServiceEnabled", serviceEnabled,
104                                  "DeliveryRetryAttempts", retryAttemps,
105                                  "DeliveryRetryIntervalSeconds", retryInterval))
106         {
107             return;
108         }
109 
110         auto [enabled, retryCount, retryTimeoutInterval] =
111             EventServiceManager::getInstance().getEventServiceConfig();
112 
113         if (serviceEnabled)
114         {
115             enabled = *serviceEnabled;
116         }
117 
118         if (retryAttemps)
119         {
120             // Supported range [1-3]
121             if ((*retryAttemps < 1) || (*retryAttemps > 3))
122             {
123                 messages::queryParameterOutOfRange(
124                     asyncResp->res, std::to_string(*retryAttemps),
125                     "DeliveryRetryAttempts", "[1-3]");
126             }
127             else
128             {
129                 retryCount = *retryAttemps;
130             }
131         }
132 
133         if (retryInterval)
134         {
135             // Supported range [30 - 180]
136             if ((*retryInterval < 30) || (*retryInterval > 180))
137             {
138                 messages::queryParameterOutOfRange(
139                     asyncResp->res, std::to_string(*retryInterval),
140                     "DeliveryRetryIntervalSeconds", "[30-180]");
141             }
142             else
143             {
144                 retryTimeoutInterval = *retryInterval;
145             }
146         }
147 
148         EventServiceManager::getInstance().setEventServiceConfig(
149             std::make_tuple(enabled, retryCount, retryTimeoutInterval));
150     }
151 };
152 
153 class SubmitTestEvent : public Node
154 {
155   public:
156     SubmitTestEvent(CrowApp& app) :
157         Node(app,
158              "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
159     {
160         entityPrivileges = {
161             {boost::beast::http::verb::get, {{"Login"}}},
162             {boost::beast::http::verb::head, {{"Login"}}},
163             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
164             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
165             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
166             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
167     }
168 
169   private:
170     void doPost(crow::Response& res, const crow::Request& req,
171                 const std::vector<std::string>& params) override
172     {
173         EventServiceManager::getInstance().sendTestEventLog();
174         res.result(boost::beast::http::status::no_content);
175         res.end();
176     }
177 };
178 
179 class EventDestinationCollection : public Node
180 {
181   public:
182     EventDestinationCollection(CrowApp& app) :
183         Node(app, "/redfish/v1/EventService/Subscriptions/")
184     {
185         entityPrivileges = {
186             {boost::beast::http::verb::get, {{"Login"}}},
187             {boost::beast::http::verb::head, {{"Login"}}},
188             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
189             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
190             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
191             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
192     }
193 
194   private:
195     void doGet(crow::Response& res, const crow::Request& req,
196                const std::vector<std::string>& params) override
197     {
198         auto asyncResp = std::make_shared<AsyncResp>(res);
199 
200         res.jsonValue = {
201             {"@odata.type",
202              "#EventDestinationCollection.EventDestinationCollection"},
203             {"@odata.id", "/redfish/v1/EventService/Subscriptions"},
204             {"Name", "Event Destination Collections"}};
205 
206         nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
207 
208         std::vector<std::string> subscripIds =
209             EventServiceManager::getInstance().getAllIDs();
210         memberArray = nlohmann::json::array();
211         asyncResp->res.jsonValue["Members@odata.count"] = subscripIds.size();
212 
213         for (const std::string& id : subscripIds)
214         {
215             memberArray.push_back(
216                 {{"@odata.id",
217                   "/redfish/v1/EventService/Subscriptions/" + id}});
218         }
219     }
220 
221     void doPost(crow::Response& res, const crow::Request& req,
222                 const std::vector<std::string>& params) override
223     {
224         auto asyncResp = std::make_shared<AsyncResp>(res);
225 
226         if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
227             maxNoOfSubscriptions)
228         {
229             messages::eventSubscriptionLimitExceeded(asyncResp->res);
230             return;
231         }
232         std::string destUrl;
233         std::string protocol;
234         std::optional<std::string> context;
235         std::optional<std::string> subscriptionType;
236         std::optional<std::string> eventFormatType;
237         std::optional<std::string> retryPolicy;
238         std::optional<std::vector<std::string>> msgIds;
239         std::optional<std::vector<std::string>> regPrefixes;
240         std::optional<std::vector<std::string>> resTypes;
241         std::optional<std::vector<nlohmann::json>> headers;
242         std::optional<std::vector<nlohmann::json>> metricReportDefinitions;
243 
244         if (!json_util::readJson(
245                 req, res, "Destination", destUrl, "Context", context,
246                 "Protocol", protocol, "SubscriptionType", subscriptionType,
247                 "EventFormatType", eventFormatType, "HttpHeaders", headers,
248                 "RegistryPrefixes", regPrefixes, "MessageIds", msgIds,
249                 "DeliveryRetryPolicy", retryPolicy, "MetricReportDefinitions",
250                 metricReportDefinitions, "ResourceTypes", resTypes))
251         {
252             return;
253         }
254 
255         // Validate the URL using regex expression
256         // Format: <protocol>://<host>:<port>/<uri>
257         // protocol: http/https
258         // host: Exclude ' ', ':', '#', '?'
259         // port: Empty or numeric value with ':' separator.
260         // uri: Start with '/' and Exclude '#', ' '
261         //      Can include query params(ex: '/event?test=1')
262         // TODO: Need to validate hostname extensively(as per rfc)
263         const std::regex urlRegex(
264             "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
265             "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
266         std::cmatch match;
267         if (!std::regex_match(destUrl.c_str(), match, urlRegex))
268         {
269             messages::propertyValueFormatError(asyncResp->res, destUrl,
270                                                "Destination");
271             return;
272         }
273 
274         std::string uriProto = std::string(match[1].first, match[1].second);
275         if (uriProto == "http")
276         {
277 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
278             messages::propertyValueFormatError(asyncResp->res, destUrl,
279                                                "Destination");
280             return;
281 #endif
282         }
283 
284         std::string host = std::string(match[2].first, match[2].second);
285         std::string port = std::string(match[3].first, match[3].second);
286         std::string path = std::string(match[4].first, match[4].second);
287         if (port.empty())
288         {
289             if (uriProto == "http")
290             {
291                 port = "80";
292             }
293             else
294             {
295                 port = "443";
296             }
297         }
298         if (path.empty())
299         {
300             path = "/";
301         }
302 
303         std::shared_ptr<Subscription> subValue =
304             std::make_shared<Subscription>(host, port, path, uriProto);
305 
306         subValue->destinationUrl = destUrl;
307 
308         if (subscriptionType)
309         {
310             if (*subscriptionType != "RedfishEvent")
311             {
312                 messages::propertyValueNotInList(
313                     asyncResp->res, *subscriptionType, "SubscriptionType");
314                 return;
315             }
316             subValue->subscriptionType = *subscriptionType;
317         }
318         else
319         {
320             subValue->subscriptionType = "RedfishEvent"; // Default
321         }
322 
323         if (protocol != "Redfish")
324         {
325             messages::propertyValueNotInList(asyncResp->res, protocol,
326                                              "Protocol");
327             return;
328         }
329         subValue->protocol = protocol;
330 
331         if (eventFormatType)
332         {
333             if (std::find(supportedEvtFormatTypes.begin(),
334                           supportedEvtFormatTypes.end(),
335                           *eventFormatType) == supportedEvtFormatTypes.end())
336             {
337                 messages::propertyValueNotInList(
338                     asyncResp->res, *eventFormatType, "EventFormatType");
339                 return;
340             }
341             subValue->eventFormatType = *eventFormatType;
342         }
343         else
344         {
345             // If not specified, use default "Event"
346             subValue->eventFormatType.assign({"Event"});
347         }
348 
349         if (context)
350         {
351             subValue->customText = *context;
352         }
353 
354         if (headers)
355         {
356             subValue->httpHeaders = *headers;
357         }
358 
359         if (regPrefixes)
360         {
361             for (const std::string& it : *regPrefixes)
362             {
363                 if (std::find(supportedRegPrefixes.begin(),
364                               supportedRegPrefixes.end(),
365                               it) == supportedRegPrefixes.end())
366                 {
367                     messages::propertyValueNotInList(asyncResp->res, it,
368                                                      "RegistryPrefixes");
369                     return;
370                 }
371             }
372             subValue->registryPrefixes = *regPrefixes;
373         }
374 
375         if (resTypes)
376         {
377             for (const std::string& it : *resTypes)
378             {
379                 if (std::find(supportedResourceTypes.begin(),
380                               supportedResourceTypes.end(),
381                               it) == supportedResourceTypes.end())
382                 {
383                     messages::propertyValueNotInList(asyncResp->res, it,
384                                                      "ResourceTypes");
385                     return;
386                 }
387             }
388             subValue->resourceTypes = *resTypes;
389         }
390 
391         if (msgIds)
392         {
393             // Do we need to loop-up MessageRegistry and validate
394             // data for authenticity??? Not mandate, i believe.
395             subValue->registryMsgIds = *msgIds;
396         }
397 
398         if (retryPolicy)
399         {
400             if (std::find(supportedRetryPolicies.begin(),
401                           supportedRetryPolicies.end(),
402                           *retryPolicy) == supportedRetryPolicies.end())
403             {
404                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
405                                                  "DeliveryRetryPolicy");
406                 return;
407             }
408             subValue->retryPolicy = *retryPolicy;
409         }
410         else
411         {
412             // Default "TerminateAfterRetries"
413             subValue->retryPolicy = "TerminateAfterRetries";
414         }
415 
416         if (metricReportDefinitions)
417         {
418             subValue->metricReportDefinitions = *metricReportDefinitions;
419         }
420 
421         std::string id =
422             EventServiceManager::getInstance().addSubscription(subValue);
423         if (id.empty())
424         {
425             messages::internalError(asyncResp->res);
426             return;
427         }
428 
429         messages::created(asyncResp->res);
430         asyncResp->res.addHeader(
431             "Location", "/redfish/v1/EventService/Subscriptions/" + id);
432     }
433 };
434 
435 class EventServiceSSE : public Node
436 {
437   public:
438     EventServiceSSE(CrowApp& app) :
439         Node(app, "/redfish/v1/EventService/Subscriptions/SSE/")
440     {
441         entityPrivileges = {
442             {boost::beast::http::verb::get, {{"ConfigureManager"}}},
443             {boost::beast::http::verb::head, {{"ConfigureManager"}}},
444             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
445             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
446             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
447             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
448     }
449 
450   private:
451     void doGet(crow::Response& res, const crow::Request& req,
452                const std::vector<std::string>& params) override
453     {
454         if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
455             maxNoOfSubscriptions)
456         {
457             messages::eventSubscriptionLimitExceeded(res);
458             res.end();
459             return;
460         }
461 
462         std::shared_ptr<crow::Request::Adaptor> sseConn =
463             std::make_shared<crow::Request::Adaptor>(std::move(req.socket()));
464         std::shared_ptr<Subscription> subValue =
465             std::make_shared<Subscription>(sseConn);
466 
467         // GET on this URI means, Its SSE subscriptionType.
468         subValue->subscriptionType = "SSE";
469 
470         // Default values
471         subValue->protocol = "Redfish";
472         subValue->retryPolicy = "TerminateAfterRetries";
473 
474         char* filters = req.urlParams.get("$filter");
475         if (filters == nullptr)
476         {
477             subValue->eventFormatType = "Event";
478         }
479         else
480         {
481             // Reading from query params.
482             bool status = readSSEQueryParams(
483                 filters, subValue->eventFormatType, subValue->registryMsgIds,
484                 subValue->registryPrefixes, subValue->metricReportDefinitions);
485 
486             if (!status)
487             {
488                 messages::invalidObject(res, filters);
489                 return;
490             }
491 
492             if (!subValue->eventFormatType.empty())
493             {
494                 if (std::find(supportedEvtFormatTypes.begin(),
495                               supportedEvtFormatTypes.end(),
496                               subValue->eventFormatType) ==
497                     supportedEvtFormatTypes.end())
498                 {
499                     messages::propertyValueNotInList(
500                         res, subValue->eventFormatType, "EventFormatType");
501                     return;
502                 }
503             }
504             else
505             {
506                 // If nothing specified, using default "Event"
507                 subValue->eventFormatType.assign({"Event"});
508             }
509 
510             if (!subValue->registryPrefixes.empty())
511             {
512                 for (const std::string& it : subValue->registryPrefixes)
513                 {
514                     if (std::find(supportedRegPrefixes.begin(),
515                                   supportedRegPrefixes.end(),
516                                   it) == supportedRegPrefixes.end())
517                     {
518                         messages::propertyValueNotInList(res, it,
519                                                          "RegistryPrefixes");
520                         return;
521                     }
522                 }
523             }
524         }
525 
526         std::string id =
527             EventServiceManager::getInstance().addSubscription(subValue, false);
528         if (id.empty())
529         {
530             messages::internalError(res);
531             res.end();
532             return;
533         }
534     }
535 };
536 
537 class EventDestination : public Node
538 {
539   public:
540     EventDestination(CrowApp& app) :
541         Node(app, "/redfish/v1/EventService/Subscriptions/<str>/",
542              std::string())
543     {
544         entityPrivileges = {
545             {boost::beast::http::verb::get, {{"Login"}}},
546             {boost::beast::http::verb::head, {{"Login"}}},
547             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
548             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
549             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
550             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
551     }
552 
553   private:
554     void doGet(crow::Response& res, const crow::Request& req,
555                const std::vector<std::string>& params) override
556     {
557         auto asyncResp = std::make_shared<AsyncResp>(res);
558         if (params.size() != 1)
559         {
560             messages::internalError(asyncResp->res);
561             return;
562         }
563 
564         std::shared_ptr<Subscription> subValue =
565             EventServiceManager::getInstance().getSubscription(params[0]);
566         if (subValue == nullptr)
567         {
568             res.result(boost::beast::http::status::not_found);
569             res.end();
570             return;
571         }
572         const std::string& id = params[0];
573 
574         res.jsonValue = {
575             {"@odata.type", "#EventDestination.v1_7_0.EventDestination"},
576             {"Protocol", "Redfish"}};
577         asyncResp->res.jsonValue["@odata.id"] =
578             "/redfish/v1/EventService/Subscriptions/" + id;
579         asyncResp->res.jsonValue["Id"] = id;
580         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
581         asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl;
582         asyncResp->res.jsonValue["Context"] = subValue->customText;
583         asyncResp->res.jsonValue["SubscriptionType"] =
584             subValue->subscriptionType;
585         asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders;
586         asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType;
587         asyncResp->res.jsonValue["RegistryPrefixes"] =
588             subValue->registryPrefixes;
589         asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes;
590 
591         asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds;
592         asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy;
593         asyncResp->res.jsonValue["MetricReportDefinitions"] =
594             subValue->metricReportDefinitions;
595     }
596 
597     void doPatch(crow::Response& res, const crow::Request& req,
598                  const std::vector<std::string>& params) override
599     {
600         auto asyncResp = std::make_shared<AsyncResp>(res);
601         if (params.size() != 1)
602         {
603             messages::internalError(asyncResp->res);
604             return;
605         }
606 
607         std::shared_ptr<Subscription> subValue =
608             EventServiceManager::getInstance().getSubscription(params[0]);
609         if (subValue == nullptr)
610         {
611             res.result(boost::beast::http::status::not_found);
612             res.end();
613             return;
614         }
615 
616         std::optional<std::string> context;
617         std::optional<std::string> retryPolicy;
618         std::optional<std::vector<nlohmann::json>> headers;
619 
620         if (!json_util::readJson(req, res, "Context", context,
621                                  "DeliveryRetryPolicy", retryPolicy,
622                                  "HttpHeaders", headers))
623         {
624             return;
625         }
626 
627         if (context)
628         {
629             subValue->customText = *context;
630         }
631 
632         if (headers)
633         {
634             subValue->httpHeaders = *headers;
635         }
636 
637         if (retryPolicy)
638         {
639             if (std::find(supportedRetryPolicies.begin(),
640                           supportedRetryPolicies.end(),
641                           *retryPolicy) == supportedRetryPolicies.end())
642             {
643                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
644                                                  "DeliveryRetryPolicy");
645                 return;
646             }
647             subValue->retryPolicy = *retryPolicy;
648             subValue->updateRetryPolicy();
649         }
650 
651         EventServiceManager::getInstance().updateSubscriptionData();
652     }
653 
654     void doDelete(crow::Response& res, const crow::Request& req,
655                   const std::vector<std::string>& params) override
656     {
657         auto asyncResp = std::make_shared<AsyncResp>(res);
658 
659         if (params.size() != 1)
660         {
661             messages::internalError(asyncResp->res);
662             return;
663         }
664 
665         if (!EventServiceManager::getInstance().isSubscriptionExist(params[0]))
666         {
667             res.result(boost::beast::http::status::not_found);
668             res.end();
669             return;
670         }
671         EventServiceManager::getInstance().deleteSubscription(params[0]);
672     }
673 };
674 
675 } // namespace redfish
676