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