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