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