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