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