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 "app.hpp"
18 #include "event_service_manager.hpp"
19 #include "http/utility.hpp"
20 #include "logging.hpp"
21 #include "query.hpp"
22 #include "registries/privilege_registry.hpp"
23 
24 #include <boost/beast/http/fields.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 inline void requestRoutesEventService(App& app)
47 {
48     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
49         .privileges(redfish::privileges::getEventService)
50         .methods(boost::beast::http::verb::get)(
51             [&app](const crow::Request& req,
52                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
53         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
54         {
55             return;
56         }
57 
58         asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/EventService";
59         asyncResp->res.jsonValue["@odata.type"] =
60             "#EventService.v1_5_0.EventService";
61         asyncResp->res.jsonValue["Id"] = "EventService";
62         asyncResp->res.jsonValue["Name"] = "Event Service";
63         asyncResp->res.jsonValue["ServerSentEventUri"] =
64             "/redfish/v1/EventService/SSE";
65 
66         asyncResp->res.jsonValue["Subscriptions"]["@odata.id"] =
67             "/redfish/v1/EventService/Subscriptions";
68         asyncResp->res
69             .jsonValue["Actions"]["#EventService.SubmitTestEvent"]["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"] = eventServiceConfig.enabled;
79         asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
80             eventServiceConfig.retryAttempts;
81         asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
82             eventServiceConfig.retryTimeoutInterval;
83         asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes;
84         asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
85         asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes;
86 
87         nlohmann::json::object_t supportedSSEFilters;
88         supportedSSEFilters["EventFormatType"] = true;
89         supportedSSEFilters["MessageId"] = true;
90         supportedSSEFilters["MetricReportDefinition"] = true;
91         supportedSSEFilters["RegistryPrefix"] = true;
92         supportedSSEFilters["OriginResource"] = false;
93         supportedSSEFilters["ResourceType"] = false;
94 
95         asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
96             std::move(supportedSSEFilters);
97         });
98 
99     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
100         .privileges(redfish::privileges::patchEventService)
101         .methods(boost::beast::http::verb::patch)(
102             [&app](const crow::Request& req,
103                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
104         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
105         {
106             return;
107         }
108         std::optional<bool> serviceEnabled;
109         std::optional<uint32_t> retryAttemps;
110         std::optional<uint32_t> retryInterval;
111 
112         if (!json_util::readJsonPatch(
113                 req, asyncResp->res, "ServiceEnabled", serviceEnabled,
114                 "DeliveryRetryAttempts", retryAttemps,
115                 "DeliveryRetryIntervalSeconds", retryInterval))
116         {
117             return;
118         }
119 
120         persistent_data::EventServiceConfig eventServiceConfig =
121             persistent_data::EventServiceStore::getInstance()
122                 .getEventServiceConfig();
123 
124         if (serviceEnabled)
125         {
126             eventServiceConfig.enabled = *serviceEnabled;
127         }
128 
129         if (retryAttemps)
130         {
131             // Supported range [1-3]
132             if ((*retryAttemps < 1) || (*retryAttemps > 3))
133             {
134                 messages::queryParameterOutOfRange(
135                     asyncResp->res, std::to_string(*retryAttemps),
136                     "DeliveryRetryAttempts", "[1-3]");
137             }
138             else
139             {
140                 eventServiceConfig.retryAttempts = *retryAttemps;
141             }
142         }
143 
144         if (retryInterval)
145         {
146             // Supported range [5 - 180]
147             if ((*retryInterval < 5) || (*retryInterval > 180))
148             {
149                 messages::queryParameterOutOfRange(
150                     asyncResp->res, std::to_string(*retryInterval),
151                     "DeliveryRetryIntervalSeconds", "[5-180]");
152             }
153             else
154             {
155                 eventServiceConfig.retryTimeoutInterval = *retryInterval;
156             }
157         }
158 
159         EventServiceManager::getInstance().setEventServiceConfig(
160             eventServiceConfig);
161         });
162 }
163 
164 inline void requestRoutesSubmitTestEvent(App& app)
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"] = "/redfish/v1/EventService/Subscriptions/" +
214                                   id;
215             memberArray.emplace_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         // https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
257         static constexpr const uint16_t maxDestinationSize = 2000;
258         if (destUrl.size() > maxDestinationSize)
259         {
260             messages::stringValueTooLong(asyncResp->res, "Destination",
261                                          maxDestinationSize);
262             return;
263         }
264 
265         if (regPrefixes && msgIds)
266         {
267             if (!regPrefixes->empty() && !msgIds->empty())
268             {
269                 messages::propertyValueConflict(asyncResp->res, "MessageIds",
270                                                 "RegistryPrefixes");
271                 return;
272             }
273         }
274 
275         std::string host;
276         std::string urlProto;
277         uint16_t port = 0;
278         std::string path;
279 
280         if (!crow::utility::validateAndSplitUrl(destUrl, urlProto, host, port,
281                                                 path))
282         {
283             BMCWEB_LOG_WARNING
284                 << "Failed to validate and split destination url";
285             messages::propertyValueFormatError(asyncResp->res, destUrl,
286                                                "Destination");
287             return;
288         }
289 
290         if (path.empty())
291         {
292             path = "/";
293         }
294         std::shared_ptr<Subscription> subValue = std::make_shared<Subscription>(
295             host, port, path, urlProto, app.ioContext());
296 
297         subValue->destinationUrl = destUrl;
298 
299         if (subscriptionType)
300         {
301             if (*subscriptionType != "RedfishEvent")
302             {
303                 messages::propertyValueNotInList(
304                     asyncResp->res, *subscriptionType, "SubscriptionType");
305                 return;
306             }
307             subValue->subscriptionType = *subscriptionType;
308         }
309         else
310         {
311             subValue->subscriptionType = "RedfishEvent"; // Default
312         }
313 
314         if (protocol != "Redfish")
315         {
316             messages::propertyValueNotInList(asyncResp->res, protocol,
317                                              "Protocol");
318             return;
319         }
320         subValue->protocol = protocol;
321 
322         if (eventFormatType2)
323         {
324             if (std::find(supportedEvtFormatTypes.begin(),
325                           supportedEvtFormatTypes.end(),
326                           *eventFormatType2) == supportedEvtFormatTypes.end())
327             {
328                 messages::propertyValueNotInList(
329                     asyncResp->res, *eventFormatType2, "EventFormatType");
330                 return;
331             }
332             subValue->eventFormatType = *eventFormatType2;
333         }
334         else
335         {
336             // If not specified, use default "Event"
337             subValue->eventFormatType = "Event";
338         }
339 
340         if (context)
341         {
342             // This value is selected aribitrarily.
343             constexpr const size_t maxContextSize = 256;
344             if (context->size() > maxContextSize)
345             {
346                 messages::stringValueTooLong(asyncResp->res, "Context",
347                                              maxContextSize);
348                 return;
349             }
350             subValue->customText = *context;
351         }
352 
353         if (headers)
354         {
355             size_t cumulativeLen = 0;
356 
357             for (const nlohmann::json& headerChunk : *headers)
358             {
359                 std::string hdr{headerChunk.dump(
360                     -1, ' ', true, nlohmann::json::error_handler_t::replace)};
361                 cumulativeLen += hdr.length();
362 
363                 // This value is selected to mirror http_connection.hpp
364                 constexpr const uint16_t maxHeaderSizeED = 8096;
365                 if (cumulativeLen > maxHeaderSizeED)
366                 {
367                     messages::arraySizeTooLong(asyncResp->res, "HttpHeaders",
368                                                maxHeaderSizeED);
369                     return;
370                 }
371                 for (const auto& item : headerChunk.items())
372                 {
373                     const std::string* value =
374                         item.value().get_ptr<const std::string*>();
375                     if (value == nullptr)
376                     {
377                         messages::propertyValueFormatError(
378                             asyncResp->res, item.value(),
379                             "HttpHeaders/" + item.key());
380                         return;
381                     }
382                     subValue->httpHeaders.set(item.key(), *value);
383                 }
384             }
385         }
386 
387         if (regPrefixes)
388         {
389             for (const std::string& it : *regPrefixes)
390             {
391                 if (std::find(supportedRegPrefixes.begin(),
392                               supportedRegPrefixes.end(),
393                               it) == supportedRegPrefixes.end())
394                 {
395                     messages::propertyValueNotInList(asyncResp->res, it,
396                                                      "RegistryPrefixes");
397                     return;
398                 }
399             }
400             subValue->registryPrefixes = *regPrefixes;
401         }
402 
403         if (resTypes)
404         {
405             for (const std::string& it : *resTypes)
406             {
407                 if (std::find(supportedResourceTypes.begin(),
408                               supportedResourceTypes.end(),
409                               it) == supportedResourceTypes.end())
410                 {
411                     messages::propertyValueNotInList(asyncResp->res, it,
412                                                      "ResourceTypes");
413                     return;
414                 }
415             }
416             subValue->resourceTypes = *resTypes;
417         }
418 
419         if (msgIds)
420         {
421             std::vector<std::string> registryPrefix;
422 
423             // If no registry prefixes are mentioned, consider all
424             // supported prefixes
425             if (subValue->registryPrefixes.empty())
426             {
427                 registryPrefix.assign(supportedRegPrefixes.begin(),
428                                       supportedRegPrefixes.end());
429             }
430             else
431             {
432                 registryPrefix = subValue->registryPrefixes;
433             }
434 
435             for (const std::string& id : *msgIds)
436             {
437                 bool validId = false;
438 
439                 // Check for Message ID in each of the selected Registry
440                 for (const std::string& it : registryPrefix)
441                 {
442                     const std::span<const redfish::registries::MessageEntry>
443                         registry =
444                             redfish::registries::getRegistryFromPrefix(it);
445 
446                     if (std::any_of(
447                             registry.begin(), registry.end(),
448                             [&id](const redfish::registries::MessageEntry&
449                                       messageEntry) {
450                         return id == messageEntry.first;
451                             }))
452                     {
453                         validId = true;
454                         break;
455                     }
456                 }
457 
458                 if (!validId)
459                 {
460                     messages::propertyValueNotInList(asyncResp->res, id,
461                                                      "MessageIds");
462                     return;
463                 }
464             }
465 
466             subValue->registryMsgIds = *msgIds;
467         }
468 
469         if (retryPolicy)
470         {
471             if (std::find(supportedRetryPolicies.begin(),
472                           supportedRetryPolicies.end(),
473                           *retryPolicy) == supportedRetryPolicies.end())
474             {
475                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
476                                                  "DeliveryRetryPolicy");
477                 return;
478             }
479             subValue->retryPolicy = *retryPolicy;
480         }
481         else
482         {
483             // Default "TerminateAfterRetries"
484             subValue->retryPolicy = "TerminateAfterRetries";
485         }
486 
487         if (mrdJsonArray)
488         {
489             for (nlohmann::json& mrdObj : *mrdJsonArray)
490             {
491                 std::string mrdUri;
492 
493                 if (!json_util::readJson(mrdObj, asyncResp->res, "@odata.id",
494                                          mrdUri))
495 
496                 {
497                     return;
498                 }
499                 subValue->metricReportDefinitions.emplace_back(mrdUri);
500             }
501         }
502 
503         std::string id =
504             EventServiceManager::getInstance().addSubscription(subValue);
505         if (id.empty())
506         {
507             messages::internalError(asyncResp->res);
508             return;
509         }
510 
511         messages::created(asyncResp->res);
512         asyncResp->res.addHeader(
513             "Location", "/redfish/v1/EventService/Subscriptions/" + id);
514         });
515 }
516 
517 inline void requestRoutesEventDestination(App& app)
518 {
519     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
520         .privileges(redfish::privileges::getEventDestination)
521         .methods(boost::beast::http::verb::get)(
522             [&app](const crow::Request& req,
523                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
524                    const std::string& param) {
525         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
526         {
527             return;
528         }
529         std::shared_ptr<Subscription> subValue =
530             EventServiceManager::getInstance().getSubscription(param);
531         if (subValue == nullptr)
532         {
533             asyncResp->res.result(boost::beast::http::status::not_found);
534             return;
535         }
536         const std::string& id = param;
537 
538         asyncResp->res.jsonValue["@odata.type"] =
539             "#EventDestination.v1_7_0.EventDestination";
540         asyncResp->res.jsonValue["Protocol"] = "Redfish";
541         asyncResp->res.jsonValue["@odata.id"] =
542             "/redfish/v1/EventService/Subscriptions/" + id;
543         asyncResp->res.jsonValue["Id"] = id;
544         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
545         asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl;
546         asyncResp->res.jsonValue["Context"] = subValue->customText;
547         asyncResp->res.jsonValue["SubscriptionType"] =
548             subValue->subscriptionType;
549         asyncResp->res.jsonValue["HttpHeaders"] = nlohmann::json::array();
550         asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType;
551         asyncResp->res.jsonValue["RegistryPrefixes"] =
552             subValue->registryPrefixes;
553         asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes;
554 
555         asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds;
556         asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy;
557 
558         nlohmann::json::array_t mrdJsonArray;
559         for (const auto& mdrUri : subValue->metricReportDefinitions)
560         {
561             nlohmann::json::object_t mdr;
562             mdr["@odata.id"] = mdrUri;
563             mrdJsonArray.emplace_back(std::move(mdr));
564         }
565         asyncResp->res.jsonValue["MetricReportDefinitions"] = mrdJsonArray;
566         });
567     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
568         // The below privilege is wrong, it should be ConfigureManager OR
569         // ConfigureSelf
570         // https://github.com/openbmc/bmcweb/issues/220
571         //.privileges(redfish::privileges::patchEventDestination)
572         .privileges({{"ConfigureManager"}})
573         .methods(boost::beast::http::verb::patch)(
574             [&app](const crow::Request& req,
575                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
576                    const std::string& param) {
577         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
578         {
579             return;
580         }
581         std::shared_ptr<Subscription> subValue =
582             EventServiceManager::getInstance().getSubscription(param);
583         if (subValue == nullptr)
584         {
585             asyncResp->res.result(boost::beast::http::status::not_found);
586             return;
587         }
588 
589         std::optional<std::string> context;
590         std::optional<std::string> retryPolicy;
591         std::optional<std::vector<nlohmann::json>> headers;
592 
593         if (!json_util::readJsonPatch(req, asyncResp->res, "Context", context,
594                                       "DeliveryRetryPolicy", retryPolicy,
595                                       "HttpHeaders", headers))
596         {
597             return;
598         }
599 
600         if (context)
601         {
602             subValue->customText = *context;
603         }
604 
605         if (headers)
606         {
607             boost::beast::http::fields fields;
608             for (const nlohmann::json& headerChunk : *headers)
609             {
610                 for (const auto& it : headerChunk.items())
611                 {
612                     const std::string* value =
613                         it.value().get_ptr<const std::string*>();
614                     if (value == nullptr)
615                     {
616                         messages::propertyValueFormatError(
617                             asyncResp->res, it.value(),
618                             "HttpHeaders/" + it.key());
619                         return;
620                     }
621                     fields.set(it.key(), *value);
622                 }
623             }
624             subValue->httpHeaders = fields;
625         }
626 
627         if (retryPolicy)
628         {
629             if (std::find(supportedRetryPolicies.begin(),
630                           supportedRetryPolicies.end(),
631                           *retryPolicy) == supportedRetryPolicies.end())
632             {
633                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
634                                                  "DeliveryRetryPolicy");
635                 return;
636             }
637             subValue->retryPolicy = *retryPolicy;
638         }
639 
640         EventServiceManager::getInstance().updateSubscriptionData();
641         });
642     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
643         // The below privilege is wrong, it should be ConfigureManager OR
644         // ConfigureSelf
645         // https://github.com/openbmc/bmcweb/issues/220
646         //.privileges(redfish::privileges::deleteEventDestination)
647         .privileges({{"ConfigureManager"}})
648         .methods(boost::beast::http::verb::delete_)(
649             [&app](const crow::Request& req,
650                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
651                    const std::string& param) {
652         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
653         {
654             return;
655         }
656         if (!EventServiceManager::getInstance().isSubscriptionExist(param))
657         {
658             asyncResp->res.result(boost::beast::http::status::not_found);
659             return;
660         }
661         EventServiceManager::getInstance().deleteSubscription(param);
662         });
663 }
664 
665 } // namespace redfish
666