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