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