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