xref: /openbmc/bmcweb/features/redfish/lib/event_service.hpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 // SPDX-FileCopyrightText: Copyright 2020 Intel Corporation
4 #pragma once
5 #include "app.hpp"
6 #include "event_service_manager.hpp"
7 #include "generated/enums/event_service.hpp"
8 #include "http/utility.hpp"
9 #include "logging.hpp"
10 #include "query.hpp"
11 #include "registries.hpp"
12 #include "registries/privilege_registry.hpp"
13 #include "registries_selector.hpp"
14 #include "snmp_trap_event_clients.hpp"
15 #include "utils/json_utils.hpp"
16 
17 #include <boost/beast/http/fields.hpp>
18 #include <boost/system/error_code.hpp>
19 #include <boost/url/parse.hpp>
20 #include <sdbusplus/unpack_properties.hpp>
21 #include <utils/dbus_utils.hpp>
22 
23 #include <charconv>
24 #include <memory>
25 #include <optional>
26 #include <ranges>
27 #include <span>
28 #include <string>
29 #include <vector>
30 
31 namespace redfish
32 {
33 
34 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = {
35     eventFormatType, metricReportFormatType};
36 static constexpr const std::array<const char*, 4> supportedRegPrefixes = {
37     "Base", "OpenBMC", "TaskEvent", "HeartbeatEvent"};
38 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
39     "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
40 
41 static constexpr const std::array<const char*, 2> supportedResourceTypes = {
42     "Task", "Heartbeat"};
43 
44 inline void requestRoutesEventService(App& app)
45 {
46     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
47         .privileges(redfish::privileges::getEventService)
48         .methods(
49             boost::beast::http::verb::
50                 get)([&app](
51                          const crow::Request& req,
52                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
53             if (!redfish::setUpRedfishRoute(app, req, asyncResp))
54             {
55                 return;
56             }
57 
58             asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/EventService";
59             asyncResp->res.jsonValue["@odata.type"] =
60                 "#EventService.v1_5_0.EventService";
61             asyncResp->res.jsonValue["Id"] = "EventService";
62             asyncResp->res.jsonValue["Name"] = "Event Service";
63             asyncResp->res.jsonValue["ServerSentEventUri"] =
64                 "/redfish/v1/EventService/SSE";
65 
66             asyncResp->res.jsonValue["Subscriptions"]["@odata.id"] =
67                 "/redfish/v1/EventService/Subscriptions";
68             asyncResp->res.jsonValue["Actions"]["#EventService.SubmitTestEvent"]
69                                     ["target"] =
70                 "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent";
71 
72             const persistent_data::EventServiceConfig eventServiceConfig =
73                 persistent_data::EventServiceStore::getInstance()
74                     .getEventServiceConfig();
75 
76             asyncResp->res.jsonValue["Status"]["State"] =
77                 (eventServiceConfig.enabled ? "Enabled" : "Disabled");
78             asyncResp->res.jsonValue["ServiceEnabled"] =
79                 eventServiceConfig.enabled;
80             asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
81                 eventServiceConfig.retryAttempts;
82             asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
83                 eventServiceConfig.retryTimeoutInterval;
84             asyncResp->res.jsonValue["EventFormatTypes"] =
85                 supportedEvtFormatTypes;
86             asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
87             asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes;
88 
89             nlohmann::json::object_t supportedSSEFilters;
90             supportedSSEFilters["EventFormatType"] = true;
91             supportedSSEFilters["MessageId"] = true;
92             supportedSSEFilters["MetricReportDefinition"] = true;
93             supportedSSEFilters["RegistryPrefix"] = true;
94             supportedSSEFilters["OriginResource"] = false;
95             supportedSSEFilters["ResourceType"] = false;
96 
97             asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
98                 std::move(supportedSSEFilters);
99         });
100 
101     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
102         .privileges(redfish::privileges::patchEventService)
103         .methods(boost::beast::http::verb::patch)(
104             [&app](const crow::Request& req,
105                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
106                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
107                 {
108                     return;
109                 }
110                 std::optional<bool> serviceEnabled;
111                 std::optional<uint32_t> retryAttemps;
112                 std::optional<uint32_t> retryInterval;
113                 if (!json_util::readJsonPatch( //
114                         req, asyncResp->res, //
115                         "DeliveryRetryAttempts", retryAttemps, //
116                         "DeliveryRetryIntervalSeconds", retryInterval, //
117                         "ServiceEnabled", serviceEnabled //
118                         ))
119                 {
120                     return;
121                 }
122 
123                 persistent_data::EventServiceConfig eventServiceConfig =
124                     persistent_data::EventServiceStore::getInstance()
125                         .getEventServiceConfig();
126 
127                 if (serviceEnabled)
128                 {
129                     eventServiceConfig.enabled = *serviceEnabled;
130                 }
131 
132                 if (retryAttemps)
133                 {
134                     // Supported range [1-3]
135                     if ((*retryAttemps < 1) || (*retryAttemps > 3))
136                     {
137                         messages::queryParameterOutOfRange(
138                             asyncResp->res, std::to_string(*retryAttemps),
139                             "DeliveryRetryAttempts", "[1-3]");
140                     }
141                     else
142                     {
143                         eventServiceConfig.retryAttempts = *retryAttemps;
144                     }
145                 }
146 
147                 if (retryInterval)
148                 {
149                     // Supported range [5 - 180]
150                     if ((*retryInterval < 5) || (*retryInterval > 180))
151                     {
152                         messages::queryParameterOutOfRange(
153                             asyncResp->res, std::to_string(*retryInterval),
154                             "DeliveryRetryIntervalSeconds", "[5-180]");
155                     }
156                     else
157                     {
158                         eventServiceConfig.retryTimeoutInterval =
159                             *retryInterval;
160                     }
161                 }
162 
163                 EventServiceManager::getInstance().setEventServiceConfig(
164                     eventServiceConfig);
165             });
166 }
167 
168 inline void requestRoutesSubmitTestEvent(App& app)
169 {
170     BMCWEB_ROUTE(
171         app, "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
172         .privileges(redfish::privileges::postEventService)
173         .methods(boost::beast::http::verb::post)(
174             [&app](const crow::Request& req,
175                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
176                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
177                 {
178                     return;
179                 }
180 
181                 TestEvent testEvent;
182                 // clang-format off
183                 if (!json_util::readJsonAction(
184                         req, asyncResp->res,
185                         "EventGroupId", testEvent.eventGroupId,
186                         "EventId", testEvent.eventId,
187                         "EventTimestamp", testEvent.eventTimestamp,
188                         "Message", testEvent.message,
189                         "MessageArgs", testEvent.messageArgs,
190                         "MessageId", testEvent.messageId,
191                         "OriginOfCondition", testEvent.originOfCondition,
192                         "Resolution", testEvent.resolution,
193                         "Severity", testEvent.severity))
194                 {
195                     return;
196                 }
197                 // clang-format on
198 
199                 if (!EventServiceManager::getInstance().sendTestEventLog(
200                         testEvent))
201                 {
202                     messages::serviceDisabled(asyncResp->res,
203                                               "/redfish/v1/EventService/");
204                     return;
205                 }
206                 asyncResp->res.result(boost::beast::http::status::no_content);
207             });
208 }
209 
210 inline void doSubscriptionCollection(
211     const boost::system::error_code& ec,
212     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
213     const dbus::utility::ManagedObjectType& resp)
214 {
215     if (ec)
216     {
217         if (ec.value() == EBADR || ec.value() == EHOSTUNREACH)
218         {
219             // This is an optional process so just return if it isn't there
220             return;
221         }
222 
223         BMCWEB_LOG_ERROR("D-Bus response error on GetManagedObjects {}", ec);
224         messages::internalError(asyncResp->res);
225         return;
226     }
227     nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
228     for (const auto& objpath : resp)
229     {
230         sdbusplus::message::object_path path(objpath.first);
231         const std::string snmpId = path.filename();
232         if (snmpId.empty())
233         {
234             BMCWEB_LOG_ERROR("The SNMP client ID is wrong");
235             messages::internalError(asyncResp->res);
236             return;
237         }
238 
239         getSnmpSubscriptionList(asyncResp, snmpId, memberArray);
240     }
241 }
242 
243 inline void requestRoutesEventDestinationCollection(App& app)
244 {
245     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
246         .privileges(redfish::privileges::getEventDestinationCollection)
247         .methods(boost::beast::http::verb::get)(
248             [&app](const crow::Request& req,
249                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
250                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
251                 {
252                     return;
253                 }
254                 asyncResp->res.jsonValue["@odata.type"] =
255                     "#EventDestinationCollection.EventDestinationCollection";
256                 asyncResp->res.jsonValue["@odata.id"] =
257                     "/redfish/v1/EventService/Subscriptions";
258                 asyncResp->res.jsonValue["Name"] =
259                     "Event Destination Collections";
260 
261                 nlohmann::json& memberArray =
262                     asyncResp->res.jsonValue["Members"];
263 
264                 std::vector<std::string> subscripIds =
265                     EventServiceManager::getInstance().getAllIDs();
266                 memberArray = nlohmann::json::array();
267                 asyncResp->res.jsonValue["Members@odata.count"] =
268                     subscripIds.size();
269 
270                 for (const std::string& id : subscripIds)
271                 {
272                     nlohmann::json::object_t member;
273                     member["@odata.id"] = boost::urls::format(
274                         "/redfish/v1/EventService/Subscriptions/{}" + id);
275                     memberArray.emplace_back(std::move(member));
276                 }
277                 crow::connections::systemBus->async_method_call(
278                     [asyncResp](const boost::system::error_code& ec,
279                                 const dbus::utility::ManagedObjectType& resp) {
280                         doSubscriptionCollection(ec, asyncResp, resp);
281                     },
282                     "xyz.openbmc_project.Network.SNMP",
283                     "/xyz/openbmc_project/network/snmp/manager",
284                     "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
285             });
286 
287     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
288         .privileges(redfish::privileges::postEventDestinationCollection)
289         .methods(
290             boost::beast::http::verb::
291                 post)([&app](
292                           const crow::Request& req,
293                           const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
294             if (!redfish::setUpRedfishRoute(app, req, asyncResp))
295             {
296                 return;
297             }
298             if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
299                 maxNoOfSubscriptions)
300             {
301                 messages::eventSubscriptionLimitExceeded(asyncResp->res);
302                 return;
303             }
304             std::string destUrl;
305             std::string protocol;
306             std::optional<bool> verifyCertificate;
307             std::optional<std::string> context;
308             std::optional<std::string> subscriptionType;
309             std::optional<std::string> eventFormatType2;
310             std::optional<std::string> retryPolicy;
311             std::optional<bool> sendHeartbeat;
312             std::optional<uint64_t> hbIntervalMinutes;
313             std::optional<std::vector<std::string>> msgIds;
314             std::optional<std::vector<std::string>> regPrefixes;
315             std::optional<std::vector<std::string>> originResources;
316             std::optional<std::vector<std::string>> resTypes;
317             std::optional<std::vector<nlohmann::json::object_t>> headers;
318             std::optional<std::vector<nlohmann::json::object_t>> mrdJsonArray;
319 
320             if (!json_util::readJsonPatch( //
321                     req, asyncResp->res, //
322                     "Context", context, //
323                     "DeliveryRetryPolicy", retryPolicy, //
324                     "Destination", destUrl, //
325                     "EventFormatType", eventFormatType2, //
326                     "HeartbeatIntervalMinutes", hbIntervalMinutes, //
327                     "HttpHeaders", headers, //
328                     "MessageIds", msgIds, //
329                     "MetricReportDefinitions", mrdJsonArray, //
330                     "OriginResources", originResources, //
331                     "Protocol", protocol, //
332                     "RegistryPrefixes", regPrefixes, //
333                     "ResourceTypes", resTypes, //
334                     "SendHeartbeat", sendHeartbeat, //
335                     "SubscriptionType", subscriptionType, //
336                     "VerifyCertificate", verifyCertificate //
337                     ))
338             {
339                 return;
340             }
341             // clang-format on
342 
343             // https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
344             static constexpr const uint16_t maxDestinationSize = 2000;
345             if (destUrl.size() > maxDestinationSize)
346             {
347                 messages::stringValueTooLong(asyncResp->res, "Destination",
348                                              maxDestinationSize);
349                 return;
350             }
351 
352             if (regPrefixes && msgIds)
353             {
354                 if (!regPrefixes->empty() && !msgIds->empty())
355                 {
356                     messages::propertyValueConflict(
357                         asyncResp->res, "MessageIds", "RegistryPrefixes");
358                     return;
359                 }
360             }
361 
362             boost::system::result<boost::urls::url> url =
363                 boost::urls::parse_absolute_uri(destUrl);
364             if (!url)
365             {
366                 BMCWEB_LOG_WARNING(
367                     "Failed to validate and split destination url");
368                 messages::propertyValueFormatError(asyncResp->res, destUrl,
369                                                    "Destination");
370                 return;
371             }
372             url->normalize();
373 
374             // port_number returns zero if it is not a valid representable port
375             if (url->has_port() && url->port_number() == 0)
376             {
377                 BMCWEB_LOG_WARNING("{} is an invalid port in destination url",
378                                    url->port());
379                 messages::propertyValueFormatError(asyncResp->res, destUrl,
380                                                    "Destination");
381                 return;
382             }
383 
384             crow::utility::setProtocolDefaults(*url, protocol);
385             crow::utility::setPortDefaults(*url);
386 
387             if (url->path().empty())
388             {
389                 url->set_path("/");
390             }
391 
392             if (url->has_userinfo())
393             {
394                 messages::propertyValueFormatError(asyncResp->res, destUrl,
395                                                    "Destination");
396                 return;
397             }
398 
399             if (protocol == "SNMPv2c")
400             {
401                 if (context)
402                 {
403                     messages::propertyValueConflict(asyncResp->res, "Context",
404                                                     "Protocol");
405                     return;
406                 }
407                 if (eventFormatType2)
408                 {
409                     messages::propertyValueConflict(
410                         asyncResp->res, "EventFormatType", "Protocol");
411                     return;
412                 }
413                 if (retryPolicy)
414                 {
415                     messages::propertyValueConflict(asyncResp->res,
416                                                     "RetryPolicy", "Protocol");
417                     return;
418                 }
419                 if (sendHeartbeat)
420                 {
421                     messages::propertyValueConflict(
422                         asyncResp->res, "SendHeartbeat", "Protocol");
423                     return;
424                 }
425                 if (hbIntervalMinutes)
426                 {
427                     messages::propertyValueConflict(
428                         asyncResp->res, "HeartbeatIntervalMinutes", "Protocol");
429                     return;
430                 }
431                 if (msgIds)
432                 {
433                     messages::propertyValueConflict(asyncResp->res,
434                                                     "MessageIds", "Protocol");
435                     return;
436                 }
437                 if (regPrefixes)
438                 {
439                     messages::propertyValueConflict(
440                         asyncResp->res, "RegistryPrefixes", "Protocol");
441                     return;
442                 }
443                 if (resTypes)
444                 {
445                     messages::propertyValueConflict(
446                         asyncResp->res, "ResourceTypes", "Protocol");
447                     return;
448                 }
449                 if (headers)
450                 {
451                     messages::propertyValueConflict(asyncResp->res,
452                                                     "HttpHeaders", "Protocol");
453                     return;
454                 }
455                 if (mrdJsonArray)
456                 {
457                     messages::propertyValueConflict(
458                         asyncResp->res, "MetricReportDefinitions", "Protocol");
459                     return;
460                 }
461                 if (url->scheme() != "snmp")
462                 {
463                     messages::propertyValueConflict(asyncResp->res,
464                                                     "Destination", "Protocol");
465                     return;
466                 }
467 
468                 addSnmpTrapClient(asyncResp, url->host_address(),
469                                   url->port_number());
470                 return;
471             }
472 
473             std::shared_ptr<Subscription> subValue =
474                 std::make_shared<Subscription>(
475                     std::make_shared<persistent_data::UserSubscription>(), *url,
476                     app.ioContext());
477 
478             if (subscriptionType)
479             {
480                 if (*subscriptionType != "RedfishEvent")
481                 {
482                     messages::propertyValueNotInList(
483                         asyncResp->res, *subscriptionType, "SubscriptionType");
484                     return;
485                 }
486                 subValue->userSub->subscriptionType = *subscriptionType;
487             }
488             else
489             {
490                 // Default
491                 subValue->userSub->subscriptionType = "RedfishEvent";
492             }
493 
494             if (protocol != "Redfish")
495             {
496                 messages::propertyValueNotInList(asyncResp->res, protocol,
497                                                  "Protocol");
498                 return;
499             }
500             subValue->userSub->protocol = protocol;
501 
502             if (verifyCertificate)
503             {
504                 subValue->userSub->verifyCertificate = *verifyCertificate;
505             }
506 
507             if (eventFormatType2)
508             {
509                 if (std::ranges::find(supportedEvtFormatTypes,
510                                       *eventFormatType2) ==
511                     supportedEvtFormatTypes.end())
512                 {
513                     messages::propertyValueNotInList(
514                         asyncResp->res, *eventFormatType2, "EventFormatType");
515                     return;
516                 }
517                 subValue->userSub->eventFormatType = *eventFormatType2;
518             }
519             else
520             {
521                 // If not specified, use default "Event"
522                 subValue->userSub->eventFormatType = "Event";
523             }
524 
525             if (context)
526             {
527                 // This value is selected arbitrarily.
528                 constexpr const size_t maxContextSize = 256;
529                 if (context->size() > maxContextSize)
530                 {
531                     messages::stringValueTooLong(asyncResp->res, "Context",
532                                                  maxContextSize);
533                     return;
534                 }
535                 subValue->userSub->customText = *context;
536             }
537 
538             if (headers)
539             {
540                 size_t cumulativeLen = 0;
541 
542                 for (const nlohmann::json::object_t& headerChunk : *headers)
543                 {
544                     for (const auto& item : headerChunk)
545                     {
546                         const std::string* value =
547                             item.second.get_ptr<const std::string*>();
548                         if (value == nullptr)
549                         {
550                             messages::propertyValueFormatError(
551                                 asyncResp->res, item.second,
552                                 "HttpHeaders/" + item.first);
553                             return;
554                         }
555                         // Adding a new json value is the size of the key, +
556                         // the size of the value + 2 * 2 quotes for each, +
557                         // the colon and space between. example:
558                         // "key": "value"
559                         cumulativeLen += item.first.size() + value->size() + 6;
560                         // This value is selected to mirror http_connection.hpp
561                         constexpr const uint16_t maxHeaderSizeED = 8096;
562                         if (cumulativeLen > maxHeaderSizeED)
563                         {
564                             messages::arraySizeTooLong(
565                                 asyncResp->res, "HttpHeaders", maxHeaderSizeED);
566                             return;
567                         }
568                         subValue->userSub->httpHeaders.set(item.first, *value);
569                     }
570                 }
571             }
572 
573             if (regPrefixes)
574             {
575                 for (const std::string& it : *regPrefixes)
576                 {
577                     if (std::ranges::find(supportedRegPrefixes, it) ==
578                         supportedRegPrefixes.end())
579                     {
580                         messages::propertyValueNotInList(asyncResp->res, it,
581                                                          "RegistryPrefixes");
582                         return;
583                     }
584                 }
585                 subValue->userSub->registryPrefixes = *regPrefixes;
586             }
587 
588             if (originResources)
589             {
590                 subValue->userSub->originResources = *originResources;
591             }
592 
593             if (resTypes)
594             {
595                 for (const std::string& it : *resTypes)
596                 {
597                     if (std::ranges::find(supportedResourceTypes, it) ==
598                         supportedResourceTypes.end())
599                     {
600                         messages::propertyValueNotInList(asyncResp->res, it,
601                                                          "ResourceTypes");
602                         return;
603                     }
604                 }
605                 subValue->userSub->resourceTypes = *resTypes;
606             }
607 
608             if (msgIds)
609             {
610                 std::vector<std::string> registryPrefix;
611 
612                 // If no registry prefixes are mentioned, consider all
613                 // supported prefixes
614                 if (subValue->userSub->registryPrefixes.empty())
615                 {
616                     registryPrefix.assign(supportedRegPrefixes.begin(),
617                                           supportedRegPrefixes.end());
618                 }
619                 else
620                 {
621                     registryPrefix = subValue->userSub->registryPrefixes;
622                 }
623 
624                 for (const std::string& id : *msgIds)
625                 {
626                     bool validId = false;
627 
628                     // Check for Message ID in each of the selected Registry
629                     for (const std::string& it : registryPrefix)
630                     {
631                         const std::span<const redfish::registries::MessageEntry>
632                             registry =
633                                 redfish::registries::getRegistryFromPrefix(it);
634 
635                         if (std::ranges::any_of(
636                                 registry,
637                                 [&id](const redfish::registries::MessageEntry&
638                                           messageEntry) {
639                                     return id == messageEntry.first;
640                                 }))
641                         {
642                             validId = true;
643                             break;
644                         }
645                     }
646 
647                     if (!validId)
648                     {
649                         messages::propertyValueNotInList(asyncResp->res, id,
650                                                          "MessageIds");
651                         return;
652                     }
653                 }
654 
655                 subValue->userSub->registryMsgIds = *msgIds;
656             }
657 
658             if (retryPolicy)
659             {
660                 if (std::ranges::find(supportedRetryPolicies, *retryPolicy) ==
661                     supportedRetryPolicies.end())
662                 {
663                     messages::propertyValueNotInList(
664                         asyncResp->res, *retryPolicy, "DeliveryRetryPolicy");
665                     return;
666                 }
667                 subValue->userSub->retryPolicy = *retryPolicy;
668             }
669             else
670             {
671                 // Default "TerminateAfterRetries"
672                 subValue->userSub->retryPolicy = "TerminateAfterRetries";
673             }
674             if (sendHeartbeat)
675             {
676                 subValue->userSub->sendHeartbeat = *sendHeartbeat;
677             }
678             if (hbIntervalMinutes)
679             {
680                 if (*hbIntervalMinutes < 1 || *hbIntervalMinutes > 65535)
681                 {
682                     messages::propertyValueOutOfRange(
683                         asyncResp->res, *hbIntervalMinutes,
684                         "HeartbeatIntervalMinutes");
685                     return;
686                 }
687                 subValue->userSub->hbIntervalMinutes = *hbIntervalMinutes;
688             }
689 
690             if (mrdJsonArray)
691             {
692                 for (nlohmann::json::object_t& mrdObj : *mrdJsonArray)
693                 {
694                     std::string mrdUri;
695 
696                     if (!json_util::readJsonObject(mrdObj, asyncResp->res,
697                                                    "@odata.id", mrdUri))
698 
699                     {
700                         return;
701                     }
702                     subValue->userSub->metricReportDefinitions.emplace_back(
703                         mrdUri);
704                 }
705             }
706 
707             std::string id =
708                 EventServiceManager::getInstance().addPushSubscription(
709                     subValue);
710             if (id.empty())
711             {
712                 messages::internalError(asyncResp->res);
713                 return;
714             }
715 
716             messages::created(asyncResp->res);
717             asyncResp->res.addHeader(
718                 "Location", "/redfish/v1/EventService/Subscriptions/" + id);
719 
720             // schedule a heartbeat
721             if (subValue->userSub->sendHeartbeat)
722             {
723                 subValue->scheduleNextHeartbeatEvent();
724             }
725         });
726 }
727 
728 inline void requestRoutesEventDestination(App& app)
729 {
730     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
731         .privileges(redfish::privileges::getEventDestination)
732         .methods(boost::beast::http::verb::get)(
733             [&app](const crow::Request& req,
734                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
735                    const std::string& param) {
736                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
737                 {
738                     return;
739                 }
740 
741                 if (param.starts_with("snmp"))
742                 {
743                     getSnmpTrapClient(asyncResp, param);
744                     return;
745                 }
746 
747                 std::shared_ptr<Subscription> subValue =
748                     EventServiceManager::getInstance().getSubscription(param);
749                 if (subValue == nullptr)
750                 {
751                     asyncResp->res.result(
752                         boost::beast::http::status::not_found);
753                     return;
754                 }
755                 const std::string& id = param;
756 
757                 const persistent_data::UserSubscription& userSub =
758                     *subValue->userSub;
759 
760                 nlohmann::json& jVal = asyncResp->res.jsonValue;
761                 jVal["@odata.type"] =
762                     "#EventDestination.v1_14_1.EventDestination";
763                 jVal["Protocol"] =
764                     event_destination::EventDestinationProtocol::Redfish;
765                 jVal["@odata.id"] = boost::urls::format(
766                     "/redfish/v1/EventService/Subscriptions/{}", id);
767                 jVal["Id"] = id;
768                 jVal["Name"] = "Event Destination " + id;
769                 jVal["Destination"] = userSub.destinationUrl;
770                 jVal["Context"] = userSub.customText;
771                 jVal["SubscriptionType"] = userSub.subscriptionType;
772                 jVal["HttpHeaders"] = nlohmann::json::array();
773                 jVal["EventFormatType"] = userSub.eventFormatType;
774                 jVal["RegistryPrefixes"] = userSub.registryPrefixes;
775                 jVal["ResourceTypes"] = userSub.resourceTypes;
776 
777                 jVal["MessageIds"] = userSub.registryMsgIds;
778                 jVal["DeliveryRetryPolicy"] = userSub.retryPolicy;
779                 jVal["SendHeartbeat"] = userSub.sendHeartbeat;
780                 jVal["HeartbeatIntervalMinutes"] = userSub.hbIntervalMinutes;
781                 jVal["VerifyCertificate"] = userSub.verifyCertificate;
782 
783                 nlohmann::json::array_t mrdJsonArray;
784                 for (const auto& mdrUri : userSub.metricReportDefinitions)
785                 {
786                     nlohmann::json::object_t mdr;
787                     mdr["@odata.id"] = mdrUri;
788                     mrdJsonArray.emplace_back(std::move(mdr));
789                 }
790                 jVal["MetricReportDefinitions"] = mrdJsonArray;
791             });
792     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
793         // The below privilege is wrong, it should be ConfigureManager OR
794         // ConfigureSelf
795         // https://github.com/openbmc/bmcweb/issues/220
796         //.privileges(redfish::privileges::patchEventDestination)
797         .privileges({{"ConfigureManager"}})
798         .methods(boost::beast::http::verb::patch)(
799             [&app](const crow::Request& req,
800                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
801                    const std::string& param) {
802                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
803                 {
804                     return;
805                 }
806                 std::shared_ptr<Subscription> subValue =
807                     EventServiceManager::getInstance().getSubscription(param);
808                 if (subValue == nullptr)
809                 {
810                     asyncResp->res.result(
811                         boost::beast::http::status::not_found);
812                     return;
813                 }
814 
815                 std::optional<std::string> context;
816                 std::optional<std::string> retryPolicy;
817                 std::optional<bool> sendHeartbeat;
818                 std::optional<uint64_t> hbIntervalMinutes;
819                 std::optional<bool> verifyCertificate;
820                 std::optional<std::vector<nlohmann::json::object_t>> headers;
821 
822                 if (!json_util::readJsonPatch( //
823                         req, asyncResp->res, //
824                         "Context", context, //
825                         "DeliveryRetryPolicy", retryPolicy, //
826                         "HeartbeatIntervalMinutes", hbIntervalMinutes, //
827                         "HttpHeaders", headers, //
828                         "SendHeartbeat", sendHeartbeat, //
829                         "VerifyCertificate", verifyCertificate //
830                         ))
831                 {
832                     return;
833                 }
834 
835                 if (context)
836                 {
837                     subValue->userSub->customText = *context;
838                 }
839 
840                 if (headers)
841                 {
842                     boost::beast::http::fields fields;
843                     for (const nlohmann::json::object_t& headerChunk : *headers)
844                     {
845                         for (const auto& it : headerChunk)
846                         {
847                             const std::string* value =
848                                 it.second.get_ptr<const std::string*>();
849                             if (value == nullptr)
850                             {
851                                 messages::propertyValueFormatError(
852                                     asyncResp->res, it.second,
853                                     "HttpHeaders/" + it.first);
854                                 return;
855                             }
856                             fields.set(it.first, *value);
857                         }
858                     }
859                     subValue->userSub->httpHeaders = std::move(fields);
860                 }
861 
862                 if (retryPolicy)
863                 {
864                     if (std::ranges::find(supportedRetryPolicies,
865                                           *retryPolicy) ==
866                         supportedRetryPolicies.end())
867                     {
868                         messages::propertyValueNotInList(asyncResp->res,
869                                                          *retryPolicy,
870                                                          "DeliveryRetryPolicy");
871                         return;
872                     }
873                     subValue->userSub->retryPolicy = *retryPolicy;
874                 }
875 
876                 if (sendHeartbeat)
877                 {
878                     subValue->userSub->sendHeartbeat = *sendHeartbeat;
879                 }
880                 if (hbIntervalMinutes)
881                 {
882                     if (*hbIntervalMinutes < 1 || *hbIntervalMinutes > 65535)
883                     {
884                         messages::propertyValueOutOfRange(
885                             asyncResp->res, *hbIntervalMinutes,
886                             "HeartbeatIntervalMinutes");
887                         return;
888                     }
889                     subValue->userSub->hbIntervalMinutes = *hbIntervalMinutes;
890                 }
891 
892                 if (hbIntervalMinutes || sendHeartbeat)
893                 {
894                     // if Heartbeat interval or send heart were changed, cancel
895                     // the heartbeat timer if running and start a new heartbeat
896                     // if needed
897                     subValue->heartbeatParametersChanged();
898                 }
899 
900                 if (verifyCertificate)
901                 {
902                     subValue->userSub->verifyCertificate = *verifyCertificate;
903                 }
904 
905                 EventServiceManager::getInstance().updateSubscriptionData();
906             });
907     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
908         // The below privilege is wrong, it should be ConfigureManager OR
909         // ConfigureSelf
910         // https://github.com/openbmc/bmcweb/issues/220
911         //.privileges(redfish::privileges::deleteEventDestination)
912         .privileges({{"ConfigureManager"}})
913         .methods(boost::beast::http::verb::delete_)(
914             [&app](const crow::Request& req,
915                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
916                    const std::string& param) {
917                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
918                 {
919                     return;
920                 }
921                 EventServiceManager& event = EventServiceManager::getInstance();
922                 if (param.starts_with("snmp"))
923                 {
924                     deleteSnmpTrapClient(asyncResp, param);
925                     event.deleteSubscription(param);
926                     return;
927                 }
928 
929                 if (!event.deleteSubscription(param))
930                 {
931                     messages::resourceNotFound(asyncResp->res,
932                                                "EventDestination", param);
933                     return;
934                 }
935                 messages::success(asyncResp->res);
936             });
937 }
938 
939 } // namespace redfish
940