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