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