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