xref: /openbmc/bmcweb/features/redfish/lib/event_service.hpp (revision 55f79e6fe156505cdaddd521212a29b2a977193a)
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::readJsonPatch(
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(
206             boost::beast::http::verb::
207                 post)([](const crow::Request& req,
208                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
209             if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
210                 maxNoOfSubscriptions)
211             {
212                 messages::eventSubscriptionLimitExceeded(asyncResp->res);
213                 return;
214             }
215             std::string destUrl;
216             std::string protocol;
217             std::optional<std::string> context;
218             std::optional<std::string> subscriptionType;
219             std::optional<std::string> eventFormatType2;
220             std::optional<std::string> retryPolicy;
221             std::optional<std::vector<std::string>> msgIds;
222             std::optional<std::vector<std::string>> regPrefixes;
223             std::optional<std::vector<std::string>> resTypes;
224             std::optional<std::vector<nlohmann::json>> headers;
225             std::optional<std::vector<nlohmann::json>> mrdJsonArray;
226 
227             if (!json_util::readJsonPatch(
228                     req, asyncResp->res, "Destination", destUrl, "Context",
229                     context, "Protocol", protocol, "SubscriptionType",
230                     subscriptionType, "EventFormatType", eventFormatType2,
231                     "HttpHeaders", headers, "RegistryPrefixes", regPrefixes,
232                     "MessageIds", msgIds, "DeliveryRetryPolicy", retryPolicy,
233                     "MetricReportDefinitions", mrdJsonArray, "ResourceTypes",
234                     resTypes))
235             {
236                 return;
237             }
238 
239             if (regPrefixes && msgIds)
240             {
241                 if (!regPrefixes->empty() && !msgIds->empty())
242                 {
243                     messages::propertyValueConflict(
244                         asyncResp->res, "MessageIds", "RegistryPrefixes");
245                     return;
246                 }
247             }
248 
249             // Validate the URL using regex expression
250             // Format: <protocol>://<host>:<port>/<uri>
251             // protocol: http/https
252             // host: Exclude ' ', ':', '#', '?'
253             // port: Empty or numeric value with ':' separator.
254             // uri: Start with '/' and Exclude '#', ' '
255             //      Can include query params(ex: '/event?test=1')
256             // TODO: Need to validate hostname extensively(as per rfc)
257             const std::regex urlRegex(
258                 "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
259                 "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
260             std::cmatch match;
261             if (!std::regex_match(destUrl.c_str(), match, urlRegex))
262             {
263                 messages::propertyValueFormatError(asyncResp->res, destUrl,
264                                                    "Destination");
265                 return;
266             }
267 
268             std::string uriProto = 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(
307                         asyncResp->res, *subscriptionType, "SubscriptionType");
308                     return;
309                 }
310                 subValue->subscriptionType = *subscriptionType;
311             }
312             else
313             {
314                 subValue->subscriptionType = "RedfishEvent"; // Default
315             }
316 
317             if (protocol != "Redfish")
318             {
319                 messages::propertyValueNotInList(asyncResp->res, protocol,
320                                                  "Protocol");
321                 return;
322             }
323             subValue->protocol = protocol;
324 
325             if (eventFormatType2)
326             {
327                 if (std::find(supportedEvtFormatTypes.begin(),
328                               supportedEvtFormatTypes.end(),
329                               *eventFormatType2) ==
330                     supportedEvtFormatTypes.end())
331                 {
332                     messages::propertyValueNotInList(
333                         asyncResp->res, *eventFormatType2, "EventFormatType");
334                     return;
335                 }
336                 subValue->eventFormatType = *eventFormatType2;
337             }
338             else
339             {
340                 // If not specified, use default "Event"
341                 subValue->eventFormatType = "Event";
342             }
343 
344             if (context)
345             {
346                 subValue->customText = *context;
347             }
348 
349             if (headers)
350             {
351                 for (const nlohmann::json& headerChunk : *headers)
352                 {
353                     for (const auto& item : headerChunk.items())
354                     {
355                         const std::string* value =
356                             item.value().get_ptr<const std::string*>();
357                         if (value == nullptr)
358                         {
359                             messages::propertyValueFormatError(
360                                 asyncResp->res, item.value().dump(2, 1),
361                                 "HttpHeaders/" + item.key());
362                             return;
363                         }
364                         subValue->httpHeaders.set(item.key(), *value);
365                     }
366                 }
367             }
368 
369             if (regPrefixes)
370             {
371                 for (const std::string& it : *regPrefixes)
372                 {
373                     if (std::find(supportedRegPrefixes.begin(),
374                                   supportedRegPrefixes.end(),
375                                   it) == supportedRegPrefixes.end())
376                     {
377                         messages::propertyValueNotInList(asyncResp->res, it,
378                                                          "RegistryPrefixes");
379                         return;
380                     }
381                 }
382                 subValue->registryPrefixes = *regPrefixes;
383             }
384 
385             if (resTypes)
386             {
387                 for (const std::string& it : *resTypes)
388                 {
389                     if (std::find(supportedResourceTypes.begin(),
390                                   supportedResourceTypes.end(),
391                                   it) == supportedResourceTypes.end())
392                     {
393                         messages::propertyValueNotInList(asyncResp->res, it,
394                                                          "ResourceTypes");
395                         return;
396                     }
397                 }
398                 subValue->resourceTypes = *resTypes;
399             }
400 
401             if (msgIds)
402             {
403                 std::vector<std::string> registryPrefix;
404 
405                 // If no registry prefixes are mentioned, consider all
406                 // supported prefixes
407                 if (subValue->registryPrefixes.empty())
408                 {
409                     registryPrefix.assign(supportedRegPrefixes.begin(),
410                                           supportedRegPrefixes.end());
411                 }
412                 else
413                 {
414                     registryPrefix = subValue->registryPrefixes;
415                 }
416 
417                 for (const std::string& id : *msgIds)
418                 {
419                     bool validId = false;
420 
421                     // Check for Message ID in each of the selected Registry
422                     for (const std::string& it : registryPrefix)
423                     {
424                         const std::span<const redfish::registries::MessageEntry>
425                             registry =
426                                 redfish::registries::getRegistryFromPrefix(it);
427 
428                         if (std::any_of(
429                                 registry.begin(), registry.end(),
430                                 [&id](const redfish::registries::MessageEntry&
431                                           messageEntry) {
432                                     return id == messageEntry.first;
433                                 }))
434                         {
435                             validId = true;
436                             break;
437                         }
438                     }
439 
440                     if (!validId)
441                     {
442                         messages::propertyValueNotInList(asyncResp->res, id,
443                                                          "MessageIds");
444                         return;
445                     }
446                 }
447 
448                 subValue->registryMsgIds = *msgIds;
449             }
450 
451             if (retryPolicy)
452             {
453                 if (std::find(supportedRetryPolicies.begin(),
454                               supportedRetryPolicies.end(),
455                               *retryPolicy) == supportedRetryPolicies.end())
456                 {
457                     messages::propertyValueNotInList(
458                         asyncResp->res, *retryPolicy, "DeliveryRetryPolicy");
459                     return;
460                 }
461                 subValue->retryPolicy = *retryPolicy;
462             }
463             else
464             {
465                 // Default "TerminateAfterRetries"
466                 subValue->retryPolicy = "TerminateAfterRetries";
467             }
468 
469             if (mrdJsonArray)
470             {
471                 for (nlohmann::json& mrdObj : *mrdJsonArray)
472                 {
473                     std::string mrdUri;
474 
475                     if (!json_util::readJson(mrdObj, asyncResp->res,
476                                              "@odata.id", mrdUri))
477 
478                     {
479                         return;
480                     }
481                     subValue->metricReportDefinitions.emplace_back(mrdUri);
482                 }
483             }
484 
485             std::string id =
486                 EventServiceManager::getInstance().addSubscription(subValue);
487             if (id.empty())
488             {
489                 messages::internalError(asyncResp->res);
490                 return;
491             }
492 
493             messages::created(asyncResp->res);
494             asyncResp->res.addHeader(
495                 "Location", "/redfish/v1/EventService/Subscriptions/" + id);
496         });
497 }
498 
499 inline void requestRoutesEventDestination(App& app)
500 {
501     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
502         .privileges(redfish::privileges::getEventDestination)
503         .methods(boost::beast::http::verb::get)(
504             [](const crow::Request&,
505                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
506                const std::string& param) {
507                 std::shared_ptr<Subscription> subValue =
508                     EventServiceManager::getInstance().getSubscription(param);
509                 if (subValue == nullptr)
510                 {
511                     asyncResp->res.result(
512                         boost::beast::http::status::not_found);
513                     return;
514                 }
515                 const std::string& id = param;
516 
517                 asyncResp->res.jsonValue = {
518                     {"@odata.type",
519                      "#EventDestination.v1_7_0.EventDestination"},
520                     {"Protocol", "Redfish"}};
521                 asyncResp->res.jsonValue["@odata.id"] =
522                     "/redfish/v1/EventService/Subscriptions/" + id;
523                 asyncResp->res.jsonValue["Id"] = id;
524                 asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
525                 asyncResp->res.jsonValue["Destination"] =
526                     subValue->destinationUrl;
527                 asyncResp->res.jsonValue["Context"] = subValue->customText;
528                 asyncResp->res.jsonValue["SubscriptionType"] =
529                     subValue->subscriptionType;
530                 asyncResp->res.jsonValue["HttpHeaders"] =
531                     nlohmann::json::array();
532                 asyncResp->res.jsonValue["EventFormatType"] =
533                     subValue->eventFormatType;
534                 asyncResp->res.jsonValue["RegistryPrefixes"] =
535                     subValue->registryPrefixes;
536                 asyncResp->res.jsonValue["ResourceTypes"] =
537                     subValue->resourceTypes;
538 
539                 asyncResp->res.jsonValue["MessageIds"] =
540                     subValue->registryMsgIds;
541                 asyncResp->res.jsonValue["DeliveryRetryPolicy"] =
542                     subValue->retryPolicy;
543 
544                 std::vector<nlohmann::json> mrdJsonArray;
545                 for (const auto& mdrUri : subValue->metricReportDefinitions)
546                 {
547                     mrdJsonArray.push_back({{"@odata.id", mdrUri}});
548                 }
549                 asyncResp->res.jsonValue["MetricReportDefinitions"] =
550                     mrdJsonArray;
551             });
552     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
553         // The below privilege is wrong, it should be ConfigureManager OR
554         // ConfigureSelf
555         // https://github.com/openbmc/bmcweb/issues/220
556         //.privileges(redfish::privileges::patchEventDestination)
557         .privileges({{"ConfigureManager"}})
558         .methods(boost::beast::http::verb::patch)(
559             [](const crow::Request& req,
560                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
561                const std::string& param) {
562                 std::shared_ptr<Subscription> subValue =
563                     EventServiceManager::getInstance().getSubscription(param);
564                 if (subValue == nullptr)
565                 {
566                     asyncResp->res.result(
567                         boost::beast::http::status::not_found);
568                     return;
569                 }
570 
571                 std::optional<std::string> context;
572                 std::optional<std::string> retryPolicy;
573                 std::optional<std::vector<nlohmann::json>> headers;
574 
575                 if (!json_util::readJsonPatch(req, asyncResp->res, "Context",
576                                               context, "DeliveryRetryPolicy",
577                                               retryPolicy, "HttpHeaders",
578                                               headers))
579                 {
580                     return;
581                 }
582 
583                 if (context)
584                 {
585                     subValue->customText = *context;
586                 }
587 
588                 if (headers)
589                 {
590                     boost::beast::http::fields fields;
591                     for (const nlohmann::json& headerChunk : *headers)
592                     {
593                         for (auto& it : headerChunk.items())
594                         {
595                             const std::string* value =
596                                 it.value().get_ptr<const std::string*>();
597                             if (value == nullptr)
598                             {
599                                 messages::propertyValueFormatError(
600                                     asyncResp->res,
601                                     it.value().dump(2, ' ', true),
602                                     "HttpHeaders/" + it.key());
603                                 return;
604                             }
605                             fields.set(it.key(), *value);
606                         }
607                     }
608                     subValue->httpHeaders = fields;
609                 }
610 
611                 if (retryPolicy)
612                 {
613                     if (std::find(supportedRetryPolicies.begin(),
614                                   supportedRetryPolicies.end(),
615                                   *retryPolicy) == supportedRetryPolicies.end())
616                     {
617                         messages::propertyValueNotInList(asyncResp->res,
618                                                          *retryPolicy,
619                                                          "DeliveryRetryPolicy");
620                         return;
621                     }
622                     subValue->retryPolicy = *retryPolicy;
623                     subValue->updateRetryPolicy();
624                 }
625 
626                 EventServiceManager::getInstance().updateSubscriptionData();
627             });
628     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
629         // The below privilege is wrong, it should be ConfigureManager OR
630         // ConfigureSelf
631         // https://github.com/openbmc/bmcweb/issues/220
632         //.privileges(redfish::privileges::deleteEventDestination)
633         .privileges({{"ConfigureManager"}})
634         .methods(boost::beast::http::verb::delete_)(
635             [](const crow::Request&,
636                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
637                const std::string& param) {
638                 if (!EventServiceManager::getInstance().isSubscriptionExist(
639                         param))
640                 {
641                     asyncResp->res.result(
642                         boost::beast::http::status::not_found);
643                     return;
644                 }
645                 EventServiceManager::getInstance().deleteSubscription(param);
646             });
647 }
648 
649 } // namespace redfish
650