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