xref: /openbmc/bmcweb/redfish-core/lib/event_service.hpp (revision 10693fa500d4406c362ef0703f25d5caea226728)
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         boost::urls::url_view::params_type::iterator it =
475             req.urlParams.find("$filter");
476         if (it == req.urlParams.end())
477         {
478             subValue->eventFormatType = "Event";
479         }
480 
481         else
482         {
483             std::string filters = it->value();
484             // Reading from query params.
485             bool status = readSSEQueryParams(
486                 filters, subValue->eventFormatType, subValue->registryMsgIds,
487                 subValue->registryPrefixes, subValue->metricReportDefinitions);
488 
489             if (!status)
490             {
491                 messages::invalidObject(res, filters);
492                 return;
493             }
494 
495             if (!subValue->eventFormatType.empty())
496             {
497                 if (std::find(supportedEvtFormatTypes.begin(),
498                               supportedEvtFormatTypes.end(),
499                               subValue->eventFormatType) ==
500                     supportedEvtFormatTypes.end())
501                 {
502                     messages::propertyValueNotInList(
503                         res, subValue->eventFormatType, "EventFormatType");
504                     return;
505                 }
506             }
507             else
508             {
509                 // If nothing specified, using default "Event"
510                 subValue->eventFormatType.assign({"Event"});
511             }
512 
513             if (!subValue->registryPrefixes.empty())
514             {
515                 for (const std::string& it : subValue->registryPrefixes)
516                 {
517                     if (std::find(supportedRegPrefixes.begin(),
518                                   supportedRegPrefixes.end(),
519                                   it) == supportedRegPrefixes.end())
520                     {
521                         messages::propertyValueNotInList(res, it,
522                                                          "RegistryPrefixes");
523                         return;
524                     }
525                 }
526             }
527         }
528 
529         std::string id =
530             EventServiceManager::getInstance().addSubscription(subValue, false);
531         if (id.empty())
532         {
533             messages::internalError(res);
534             res.end();
535             return;
536         }
537     }
538 };
539 
540 class EventDestination : public Node
541 {
542   public:
543     EventDestination(CrowApp& app) :
544         Node(app, "/redfish/v1/EventService/Subscriptions/<str>/",
545              std::string())
546     {
547         entityPrivileges = {
548             {boost::beast::http::verb::get, {{"Login"}}},
549             {boost::beast::http::verb::head, {{"Login"}}},
550             {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
551             {boost::beast::http::verb::put, {{"ConfigureManager"}}},
552             {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
553             {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
554     }
555 
556   private:
557     void doGet(crow::Response& res, const crow::Request& req,
558                const std::vector<std::string>& params) override
559     {
560         auto asyncResp = std::make_shared<AsyncResp>(res);
561         if (params.size() != 1)
562         {
563             messages::internalError(asyncResp->res);
564             return;
565         }
566 
567         std::shared_ptr<Subscription> subValue =
568             EventServiceManager::getInstance().getSubscription(params[0]);
569         if (subValue == nullptr)
570         {
571             res.result(boost::beast::http::status::not_found);
572             res.end();
573             return;
574         }
575         const std::string& id = params[0];
576 
577         res.jsonValue = {
578             {"@odata.type", "#EventDestination.v1_7_0.EventDestination"},
579             {"Protocol", "Redfish"}};
580         asyncResp->res.jsonValue["@odata.id"] =
581             "/redfish/v1/EventService/Subscriptions/" + id;
582         asyncResp->res.jsonValue["Id"] = id;
583         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
584         asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl;
585         asyncResp->res.jsonValue["Context"] = subValue->customText;
586         asyncResp->res.jsonValue["SubscriptionType"] =
587             subValue->subscriptionType;
588         asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders;
589         asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType;
590         asyncResp->res.jsonValue["RegistryPrefixes"] =
591             subValue->registryPrefixes;
592         asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes;
593 
594         asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds;
595         asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy;
596         asyncResp->res.jsonValue["MetricReportDefinitions"] =
597             subValue->metricReportDefinitions;
598     }
599 
600     void doPatch(crow::Response& res, const crow::Request& req,
601                  const std::vector<std::string>& params) override
602     {
603         auto asyncResp = std::make_shared<AsyncResp>(res);
604         if (params.size() != 1)
605         {
606             messages::internalError(asyncResp->res);
607             return;
608         }
609 
610         std::shared_ptr<Subscription> subValue =
611             EventServiceManager::getInstance().getSubscription(params[0]);
612         if (subValue == nullptr)
613         {
614             res.result(boost::beast::http::status::not_found);
615             res.end();
616             return;
617         }
618 
619         std::optional<std::string> context;
620         std::optional<std::string> retryPolicy;
621         std::optional<std::vector<nlohmann::json>> headers;
622 
623         if (!json_util::readJson(req, res, "Context", context,
624                                  "DeliveryRetryPolicy", retryPolicy,
625                                  "HttpHeaders", headers))
626         {
627             return;
628         }
629 
630         if (context)
631         {
632             subValue->customText = *context;
633         }
634 
635         if (headers)
636         {
637             subValue->httpHeaders = *headers;
638         }
639 
640         if (retryPolicy)
641         {
642             if (std::find(supportedRetryPolicies.begin(),
643                           supportedRetryPolicies.end(),
644                           *retryPolicy) == supportedRetryPolicies.end())
645             {
646                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
647                                                  "DeliveryRetryPolicy");
648                 return;
649             }
650             subValue->retryPolicy = *retryPolicy;
651             subValue->updateRetryPolicy();
652         }
653 
654         EventServiceManager::getInstance().updateSubscriptionData();
655     }
656 
657     void doDelete(crow::Response& res, const crow::Request& req,
658                   const std::vector<std::string>& params) override
659     {
660         auto asyncResp = std::make_shared<AsyncResp>(res);
661 
662         if (params.size() != 1)
663         {
664             messages::internalError(asyncResp->res);
665             return;
666         }
667 
668         if (!EventServiceManager::getInstance().isSubscriptionExist(params[0]))
669         {
670             res.result(boost::beast::http::status::not_found);
671             res.end();
672             return;
673         }
674         EventServiceManager::getInstance().deleteSubscription(params[0]);
675     }
676 };
677 
678 } // namespace redfish
679