xref: /openbmc/bmcweb/features/redfish/lib/event_service.hpp (revision 1ebe3e419d8eda7f478f2fd4ce828731025e2887)
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 "event_service_manager.hpp"
18 
19 #include <app.hpp>
20 #include <boost/beast/http/fields.hpp>
21 #include <registries/privilege_registry.hpp>
22 
23 #include <span>
24 
25 namespace redfish
26 {
27 
28 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = {
29     eventFormatType, metricReportFormatType};
30 static constexpr const std::array<const char*, 3> supportedRegPrefixes = {
31     "Base", "OpenBMC", "TaskEvent"};
32 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
33     "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
34 
35 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
36 static constexpr const std::array<const char*, 2> supportedResourceTypes = {
37     "IBMConfigFile", "Task"};
38 #else
39 static constexpr const std::array<const char*, 1> supportedResourceTypes = {
40     "Task"};
41 #endif
42 
43 static constexpr const uint8_t maxNoOfSubscriptions = 20;
44 
45 inline void requestRoutesEventService(App& app)
46 {
47     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
48         .privileges(redfish::privileges::getEventService)
49         .methods(
50             boost::beast::http::verb::
51                 get)([](const crow::Request&,
52                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
53             asyncResp->res.jsonValue = {
54                 {"@odata.type", "#EventService.v1_5_0.EventService"},
55                 {"Id", "EventService"},
56                 {"Name", "Event Service"},
57                 {"Subscriptions",
58                  {{"@odata.id", "/redfish/v1/EventService/Subscriptions"}}},
59                 {"Actions",
60                  {{"#EventService.SubmitTestEvent",
61                    {{"target",
62                      "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent"}}}}},
63                 {"@odata.id", "/redfish/v1/EventService"}};
64 
65             const persistent_data::EventServiceConfig eventServiceConfig =
66                 persistent_data::EventServiceStore::getInstance()
67                     .getEventServiceConfig();
68 
69             asyncResp->res.jsonValue["Status"]["State"] =
70                 (eventServiceConfig.enabled ? "Enabled" : "Disabled");
71             asyncResp->res.jsonValue["ServiceEnabled"] =
72                 eventServiceConfig.enabled;
73             asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
74                 eventServiceConfig.retryAttempts;
75             asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
76                 eventServiceConfig.retryTimeoutInterval;
77             asyncResp->res.jsonValue["EventFormatTypes"] =
78                 supportedEvtFormatTypes;
79             asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
80             asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes;
81 
82             nlohmann::json supportedSSEFilters = {
83                 {"EventFormatType", true},        {"MessageId", true},
84                 {"MetricReportDefinition", true}, {"RegistryPrefix", true},
85                 {"OriginResource", false},        {"ResourceType", false}};
86 
87             asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
88                 supportedSSEFilters;
89         });
90 
91     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
92         .privileges(redfish::privileges::patchEventService)
93         .methods(boost::beast::http::verb::patch)(
94             [](const crow::Request& req,
95                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
96 
97             {
98                 std::optional<bool> serviceEnabled;
99                 std::optional<uint32_t> retryAttemps;
100                 std::optional<uint32_t> retryInterval;
101 
102                 if (!json_util::readJson(
103                         req, asyncResp->res, "ServiceEnabled", serviceEnabled,
104                         "DeliveryRetryAttempts", retryAttemps,
105                         "DeliveryRetryIntervalSeconds", retryInterval))
106                 {
107                     return;
108                 }
109 
110                 persistent_data::EventServiceConfig eventServiceConfig =
111                     persistent_data::EventServiceStore::getInstance()
112                         .getEventServiceConfig();
113 
114                 if (serviceEnabled)
115                 {
116                     eventServiceConfig.enabled = *serviceEnabled;
117                 }
118 
119                 if (retryAttemps)
120                 {
121                     // Supported range [1-3]
122                     if ((*retryAttemps < 1) || (*retryAttemps > 3))
123                     {
124                         messages::queryParameterOutOfRange(
125                             asyncResp->res, std::to_string(*retryAttemps),
126                             "DeliveryRetryAttempts", "[1-3]");
127                     }
128                     else
129                     {
130                         eventServiceConfig.retryAttempts = *retryAttemps;
131                     }
132                 }
133 
134                 if (retryInterval)
135                 {
136                     // Supported range [30 - 180]
137                     if ((*retryInterval < 30) || (*retryInterval > 180))
138                     {
139                         messages::queryParameterOutOfRange(
140                             asyncResp->res, std::to_string(*retryInterval),
141                             "DeliveryRetryIntervalSeconds", "[30-180]");
142                     }
143                     else
144                     {
145                         eventServiceConfig.retryTimeoutInterval =
146                             *retryInterval;
147                     }
148                 }
149 
150                 EventServiceManager::getInstance().setEventServiceConfig(
151                     eventServiceConfig);
152             });
153 }
154 
155 inline void requestRoutesSubmitTestEvent(App& app)
156 {
157 
158     BMCWEB_ROUTE(
159         app, "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
160         .privileges(redfish::privileges::postEventService)
161         .methods(boost::beast::http::verb::post)(
162             [](const crow::Request&,
163                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
164                 if (!EventServiceManager::getInstance().sendTestEventLog())
165                 {
166                     messages::serviceDisabled(asyncResp->res,
167                                               "/redfish/v1/EventService/");
168                     return;
169                 }
170                 asyncResp->res.result(boost::beast::http::status::no_content);
171             });
172 }
173 
174 inline void requestRoutesEventDestinationCollection(App& app)
175 {
176     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
177         .privileges(redfish::privileges::getEventDestinationCollection)
178         .methods(boost::beast::http::verb::get)(
179             [](const crow::Request&,
180                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
181                 asyncResp->res.jsonValue = {
182                     {"@odata.type",
183                      "#EventDestinationCollection.EventDestinationCollection"},
184                     {"@odata.id", "/redfish/v1/EventService/Subscriptions"},
185                     {"Name", "Event Destination Collections"}};
186 
187                 nlohmann::json& memberArray =
188                     asyncResp->res.jsonValue["Members"];
189 
190                 std::vector<std::string> subscripIds =
191                     EventServiceManager::getInstance().getAllIDs();
192                 memberArray = nlohmann::json::array();
193                 asyncResp->res.jsonValue["Members@odata.count"] =
194                     subscripIds.size();
195 
196                 for (const std::string& id : subscripIds)
197                 {
198                     memberArray.push_back(
199                         {{"@odata.id",
200                           "/redfish/v1/EventService/Subscriptions/" + id}});
201                 }
202             });
203     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
204         .privileges(redfish::privileges::postEventDestinationCollection)
205         .methods(boost::beast::http::verb::post)(
206             [](const crow::Request& req,
207                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
208                 if (EventServiceManager::getInstance()
209                         .getNumberOfSubscriptions() >= maxNoOfSubscriptions)
210                 {
211                     messages::eventSubscriptionLimitExceeded(asyncResp->res);
212                     return;
213                 }
214                 std::string destUrl;
215                 std::string protocol;
216                 std::optional<std::string> context;
217                 std::optional<std::string> subscriptionType;
218                 std::optional<std::string> eventFormatType2;
219                 std::optional<std::string> retryPolicy;
220                 std::optional<std::vector<std::string>> msgIds;
221                 std::optional<std::vector<std::string>> regPrefixes;
222                 std::optional<std::vector<std::string>> resTypes;
223                 std::optional<std::vector<nlohmann::json>> headers;
224                 std::optional<std::vector<nlohmann::json>> mrdJsonArray;
225 
226                 if (!json_util::readJson(
227                         req, asyncResp->res, "Destination", destUrl, "Context",
228                         context, "Protocol", protocol, "SubscriptionType",
229                         subscriptionType, "EventFormatType", eventFormatType2,
230                         "HttpHeaders", headers, "RegistryPrefixes", regPrefixes,
231                         "MessageIds", msgIds, "DeliveryRetryPolicy",
232                         retryPolicy, "MetricReportDefinitions", mrdJsonArray,
233                         "ResourceTypes", resTypes))
234                 {
235                     return;
236                 }
237 
238                 if (regPrefixes && msgIds)
239                 {
240                     if (!regPrefixes->empty() && !msgIds->empty())
241                     {
242                         messages::mutualExclusiveProperties(
243                             asyncResp->res, "RegistryPrefixes", "MessageIds");
244                         return;
245                     }
246                 }
247 
248                 // Validate the URL using regex expression
249                 // Format: <protocol>://<host>:<port>/<uri>
250                 // protocol: http/https
251                 // host: Exclude ' ', ':', '#', '?'
252                 // port: Empty or numeric value with ':' separator.
253                 // uri: Start with '/' and Exclude '#', ' '
254                 //      Can include query params(ex: '/event?test=1')
255                 // TODO: Need to validate hostname extensively(as per rfc)
256                 const std::regex urlRegex(
257                     "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
258                     "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
259                 std::cmatch match;
260                 if (!std::regex_match(destUrl.c_str(), match, urlRegex))
261                 {
262                     messages::propertyValueFormatError(asyncResp->res, destUrl,
263                                                        "Destination");
264                     return;
265                 }
266 
267                 std::string uriProto =
268                     std::string(match[1].first, match[1].second);
269                 if (uriProto == "http")
270                 {
271 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
272                     messages::propertyValueFormatError(asyncResp->res, destUrl,
273                                                        "Destination");
274                     return;
275 #endif
276                 }
277 
278                 std::string host = std::string(match[2].first, match[2].second);
279                 std::string port = std::string(match[3].first, match[3].second);
280                 std::string path = std::string(match[4].first, match[4].second);
281                 if (port.empty())
282                 {
283                     if (uriProto == "http")
284                     {
285                         port = "80";
286                     }
287                     else
288                     {
289                         port = "443";
290                     }
291                 }
292                 if (path.empty())
293                 {
294                     path = "/";
295                 }
296 
297                 std::shared_ptr<Subscription> subValue =
298                     std::make_shared<Subscription>(host, port, path, uriProto);
299 
300                 subValue->destinationUrl = destUrl;
301 
302                 if (subscriptionType)
303                 {
304                     if (*subscriptionType != "RedfishEvent")
305                     {
306                         messages::propertyValueNotInList(asyncResp->res,
307                                                          *subscriptionType,
308                                                          "SubscriptionType");
309                         return;
310                     }
311                     subValue->subscriptionType = *subscriptionType;
312                 }
313                 else
314                 {
315                     subValue->subscriptionType = "RedfishEvent"; // Default
316                 }
317 
318                 if (protocol != "Redfish")
319                 {
320                     messages::propertyValueNotInList(asyncResp->res, protocol,
321                                                      "Protocol");
322                     return;
323                 }
324                 subValue->protocol = protocol;
325 
326                 if (eventFormatType2)
327                 {
328                     if (std::find(supportedEvtFormatTypes.begin(),
329                                   supportedEvtFormatTypes.end(),
330                                   *eventFormatType2) ==
331                         supportedEvtFormatTypes.end())
332                     {
333                         messages::propertyValueNotInList(asyncResp->res,
334                                                          *eventFormatType2,
335                                                          "EventFormatType");
336                         return;
337                     }
338                     subValue->eventFormatType = *eventFormatType2;
339                 }
340                 else
341                 {
342                     // If not specified, use default "Event"
343                     subValue->eventFormatType = "Event";
344                 }
345 
346                 if (context)
347                 {
348                     subValue->customText = *context;
349                 }
350 
351                 if (headers)
352                 {
353                     for (const nlohmann::json& headerChunk : *headers)
354                     {
355                         for (const auto& item : headerChunk.items())
356                         {
357                             const std::string* value =
358                                 item.value().get_ptr<const std::string*>();
359                             if (value == nullptr)
360                             {
361                                 messages::propertyValueFormatError(
362                                     asyncResp->res, item.value().dump(2, true),
363                                     "HttpHeaders/" + item.key());
364                                 return;
365                             }
366                             subValue->httpHeaders.set(item.key(), *value);
367                         }
368                     }
369                 }
370 
371                 if (regPrefixes)
372                 {
373                     for (const std::string& it : *regPrefixes)
374                     {
375                         if (std::find(supportedRegPrefixes.begin(),
376                                       supportedRegPrefixes.end(),
377                                       it) == supportedRegPrefixes.end())
378                         {
379                             messages::propertyValueNotInList(
380                                 asyncResp->res, it, "RegistryPrefixes");
381                             return;
382                         }
383                     }
384                     subValue->registryPrefixes = *regPrefixes;
385                 }
386 
387                 if (resTypes)
388                 {
389                     for (const std::string& it : *resTypes)
390                     {
391                         if (std::find(supportedResourceTypes.begin(),
392                                       supportedResourceTypes.end(),
393                                       it) == supportedResourceTypes.end())
394                         {
395                             messages::propertyValueNotInList(asyncResp->res, it,
396                                                              "ResourceTypes");
397                             return;
398                         }
399                     }
400                     subValue->resourceTypes = *resTypes;
401                 }
402 
403                 if (msgIds)
404                 {
405                     std::vector<std::string> registryPrefix;
406 
407                     // If no registry prefixes are mentioned, consider all
408                     // supported prefixes
409                     if (subValue->registryPrefixes.empty())
410                     {
411                         registryPrefix.assign(supportedRegPrefixes.begin(),
412                                               supportedRegPrefixes.end());
413                     }
414                     else
415                     {
416                         registryPrefix = subValue->registryPrefixes;
417                     }
418 
419                     for (const std::string& id : *msgIds)
420                     {
421                         bool validId = false;
422 
423                         // Check for Message ID in each of the selected Registry
424                         for (const std::string& it : registryPrefix)
425                         {
426                             const std::span<
427                                 const redfish::message_registries::MessageEntry>
428                                 registry = redfish::message_registries::
429                                     getRegistryFromPrefix(it);
430 
431                             if (std::any_of(
432                                     registry.begin(), registry.end(),
433                                     [&id](const redfish::message_registries::
434                                               MessageEntry& messageEntry) {
435                                         return !id.compare(messageEntry.first);
436                                     }))
437                             {
438                                 validId = true;
439                                 break;
440                             }
441                         }
442 
443                         if (!validId)
444                         {
445                             messages::propertyValueNotInList(asyncResp->res, id,
446                                                              "MessageIds");
447                             return;
448                         }
449                     }
450 
451                     subValue->registryMsgIds = *msgIds;
452                 }
453 
454                 if (retryPolicy)
455                 {
456                     if (std::find(supportedRetryPolicies.begin(),
457                                   supportedRetryPolicies.end(),
458                                   *retryPolicy) == supportedRetryPolicies.end())
459                     {
460                         messages::propertyValueNotInList(asyncResp->res,
461                                                          *retryPolicy,
462                                                          "DeliveryRetryPolicy");
463                         return;
464                     }
465                     subValue->retryPolicy = *retryPolicy;
466                 }
467                 else
468                 {
469                     // Default "TerminateAfterRetries"
470                     subValue->retryPolicy = "TerminateAfterRetries";
471                 }
472 
473                 if (mrdJsonArray)
474                 {
475                     for (nlohmann::json& mrdObj : *mrdJsonArray)
476                     {
477                         std::string mrdUri;
478                         if (json_util::getValueFromJsonObject(
479                                 mrdObj, "@odata.id", mrdUri))
480                         {
481                             subValue->metricReportDefinitions.emplace_back(
482                                 mrdUri);
483                         }
484                         else
485                         {
486                             messages::propertyValueFormatError(
487                                 asyncResp->res,
488                                 mrdObj.dump(
489                                     2, ' ', true,
490                                     nlohmann::json::error_handler_t::replace),
491                                 "MetricReportDefinitions");
492                             return;
493                         }
494                     }
495                 }
496 
497                 std::string id =
498                     EventServiceManager::getInstance().addSubscription(
499                         subValue);
500                 if (id.empty())
501                 {
502                     messages::internalError(asyncResp->res);
503                     return;
504                 }
505 
506                 messages::created(asyncResp->res);
507                 asyncResp->res.addHeader(
508                     "Location", "/redfish/v1/EventService/Subscriptions/" + id);
509             });
510 }
511 
512 inline void requestRoutesEventDestination(App& app)
513 {
514     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
515         .privileges(redfish::privileges::getEventDestination)
516         .methods(boost::beast::http::verb::get)(
517             [](const crow::Request&,
518                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
519                const std::string& param) {
520                 std::shared_ptr<Subscription> subValue =
521                     EventServiceManager::getInstance().getSubscription(param);
522                 if (subValue == nullptr)
523                 {
524                     asyncResp->res.result(
525                         boost::beast::http::status::not_found);
526                     return;
527                 }
528                 const std::string& id = param;
529 
530                 asyncResp->res.jsonValue = {
531                     {"@odata.type",
532                      "#EventDestination.v1_7_0.EventDestination"},
533                     {"Protocol", "Redfish"}};
534                 asyncResp->res.jsonValue["@odata.id"] =
535                     "/redfish/v1/EventService/Subscriptions/" + id;
536                 asyncResp->res.jsonValue["Id"] = id;
537                 asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
538                 asyncResp->res.jsonValue["Destination"] =
539                     subValue->destinationUrl;
540                 asyncResp->res.jsonValue["Context"] = subValue->customText;
541                 asyncResp->res.jsonValue["SubscriptionType"] =
542                     subValue->subscriptionType;
543                 asyncResp->res.jsonValue["HttpHeaders"] =
544                     nlohmann::json::array();
545                 asyncResp->res.jsonValue["EventFormatType"] =
546                     subValue->eventFormatType;
547                 asyncResp->res.jsonValue["RegistryPrefixes"] =
548                     subValue->registryPrefixes;
549                 asyncResp->res.jsonValue["ResourceTypes"] =
550                     subValue->resourceTypes;
551 
552                 asyncResp->res.jsonValue["MessageIds"] =
553                     subValue->registryMsgIds;
554                 asyncResp->res.jsonValue["DeliveryRetryPolicy"] =
555                     subValue->retryPolicy;
556 
557                 std::vector<nlohmann::json> mrdJsonArray;
558                 for (const auto& mdrUri : subValue->metricReportDefinitions)
559                 {
560                     mrdJsonArray.push_back({{"@odata.id", mdrUri}});
561                 }
562                 asyncResp->res.jsonValue["MetricReportDefinitions"] =
563                     mrdJsonArray;
564             });
565     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
566         // The below privilege is wrong, it should be ConfigureManager OR
567         // ConfigureSelf
568         // https://github.com/openbmc/bmcweb/issues/220
569         //.privileges(redfish::privileges::patchEventDestination)
570         .privileges({{"ConfigureManager"}})
571         .methods(boost::beast::http::verb::patch)(
572             [](const crow::Request& req,
573                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
574                const std::string& param) {
575                 std::shared_ptr<Subscription> subValue =
576                     EventServiceManager::getInstance().getSubscription(param);
577                 if (subValue == nullptr)
578                 {
579                     asyncResp->res.result(
580                         boost::beast::http::status::not_found);
581                     return;
582                 }
583 
584                 std::optional<std::string> context;
585                 std::optional<std::string> retryPolicy;
586                 std::optional<std::vector<nlohmann::json>> headers;
587 
588                 if (!json_util::readJson(req, asyncResp->res, "Context",
589                                          context, "DeliveryRetryPolicy",
590                                          retryPolicy, "HttpHeaders", headers))
591                 {
592                     return;
593                 }
594 
595                 if (context)
596                 {
597                     subValue->customText = *context;
598                 }
599 
600                 if (headers)
601                 {
602                     boost::beast::http::fields fields;
603                     for (const nlohmann::json& headerChunk : *headers)
604                     {
605                         for (auto& it : headerChunk.items())
606                         {
607                             const std::string* value =
608                                 it.value().get_ptr<const std::string*>();
609                             if (value == nullptr)
610                             {
611                                 messages::propertyValueFormatError(
612                                     asyncResp->res,
613                                     it.value().dump(2, ' ', true),
614                                     "HttpHeaders/" + it.key());
615                                 return;
616                             }
617                             fields.set(it.key(), *value);
618                         }
619                     }
620                     subValue->httpHeaders = fields;
621                 }
622 
623                 if (retryPolicy)
624                 {
625                     if (std::find(supportedRetryPolicies.begin(),
626                                   supportedRetryPolicies.end(),
627                                   *retryPolicy) == supportedRetryPolicies.end())
628                     {
629                         messages::propertyValueNotInList(asyncResp->res,
630                                                          *retryPolicy,
631                                                          "DeliveryRetryPolicy");
632                         return;
633                     }
634                     subValue->retryPolicy = *retryPolicy;
635                     subValue->updateRetryPolicy();
636                 }
637 
638                 EventServiceManager::getInstance().updateSubscriptionData();
639             });
640     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
641         // The below privilege is wrong, it should be ConfigureManager OR
642         // ConfigureSelf
643         // https://github.com/openbmc/bmcweb/issues/220
644         //.privileges(redfish::privileges::deleteEventDestination)
645         .privileges({{"ConfigureManager"}})
646         .methods(boost::beast::http::verb::delete_)(
647             [](const crow::Request&,
648                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
649                const std::string& param) {
650                 if (!EventServiceManager::getInstance().isSubscriptionExist(
651                         param))
652                 {
653                     asyncResp->res.result(
654                         boost::beast::http::status::not_found);
655                     return;
656                 }
657                 EventServiceManager::getInstance().deleteSubscription(param);
658             });
659 }
660 
661 } // namespace redfish
662