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