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