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