1 /* 2 // Copyright (c) 2020 Intel Corporation 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 */ 16 #pragma once 17 #include "app.hpp" 18 #include "event_service_manager.hpp" 19 #include "http/utility.hpp" 20 #include "logging.hpp" 21 #include "query.hpp" 22 #include "registries/privilege_registry.hpp" 23 24 #include <boost/beast/http/fields.hpp> 25 26 #include <span> 27 28 namespace redfish 29 { 30 31 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = { 32 eventFormatType, metricReportFormatType}; 33 static constexpr const std::array<const char*, 3> supportedRegPrefixes = { 34 "Base", "OpenBMC", "TaskEvent"}; 35 static constexpr const std::array<const char*, 3> supportedRetryPolicies = { 36 "TerminateAfterRetries", "SuspendRetries", "RetryForever"}; 37 38 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE 39 static constexpr const std::array<const char*, 2> supportedResourceTypes = { 40 "IBMConfigFile", "Task"}; 41 #else 42 static constexpr const std::array<const char*, 1> supportedResourceTypes = { 43 "Task"}; 44 #endif 45 46 static constexpr const uint8_t maxNoOfSubscriptions = 20; 47 48 inline void requestRoutesEventService(App& app) 49 { 50 BMCWEB_ROUTE(app, "/redfish/v1/EventService/") 51 .privileges(redfish::privileges::getEventService) 52 .methods(boost::beast::http::verb::get)( 53 [&app](const crow::Request& req, 54 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 55 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 56 { 57 return; 58 } 59 60 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/EventService"; 61 asyncResp->res.jsonValue["@odata.type"] = 62 "#EventService.v1_5_0.EventService"; 63 asyncResp->res.jsonValue["Id"] = "EventService"; 64 asyncResp->res.jsonValue["Name"] = "Event Service"; 65 asyncResp->res.jsonValue["Subscriptions"]["@odata.id"] = 66 "/redfish/v1/EventService/Subscriptions"; 67 asyncResp->res 68 .jsonValue["Actions"]["#EventService.SubmitTestEvent"]["target"] = 69 "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent"; 70 71 const persistent_data::EventServiceConfig eventServiceConfig = 72 persistent_data::EventServiceStore::getInstance() 73 .getEventServiceConfig(); 74 75 asyncResp->res.jsonValue["Status"]["State"] = 76 (eventServiceConfig.enabled ? "Enabled" : "Disabled"); 77 asyncResp->res.jsonValue["ServiceEnabled"] = eventServiceConfig.enabled; 78 asyncResp->res.jsonValue["DeliveryRetryAttempts"] = 79 eventServiceConfig.retryAttempts; 80 asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] = 81 eventServiceConfig.retryTimeoutInterval; 82 asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes; 83 asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes; 84 asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes; 85 86 nlohmann::json::object_t supportedSSEFilters; 87 supportedSSEFilters["EventFormatType"] = true; 88 supportedSSEFilters["MessageId"] = true; 89 supportedSSEFilters["MetricReportDefinition"] = true; 90 supportedSSEFilters["RegistryPrefix"] = true; 91 supportedSSEFilters["OriginResource"] = false; 92 supportedSSEFilters["ResourceType"] = false; 93 94 asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] = 95 std::move(supportedSSEFilters); 96 }); 97 98 BMCWEB_ROUTE(app, "/redfish/v1/EventService/") 99 .privileges(redfish::privileges::patchEventService) 100 .methods(boost::beast::http::verb::patch)( 101 [&app](const crow::Request& req, 102 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 103 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 104 { 105 return; 106 } 107 std::optional<bool> serviceEnabled; 108 std::optional<uint32_t> retryAttemps; 109 std::optional<uint32_t> retryInterval; 110 111 if (!json_util::readJsonPatch( 112 req, asyncResp->res, "ServiceEnabled", serviceEnabled, 113 "DeliveryRetryAttempts", retryAttemps, 114 "DeliveryRetryIntervalSeconds", retryInterval)) 115 { 116 return; 117 } 118 119 persistent_data::EventServiceConfig eventServiceConfig = 120 persistent_data::EventServiceStore::getInstance() 121 .getEventServiceConfig(); 122 123 if (serviceEnabled) 124 { 125 eventServiceConfig.enabled = *serviceEnabled; 126 } 127 128 if (retryAttemps) 129 { 130 // Supported range [1-3] 131 if ((*retryAttemps < 1) || (*retryAttemps > 3)) 132 { 133 messages::queryParameterOutOfRange( 134 asyncResp->res, std::to_string(*retryAttemps), 135 "DeliveryRetryAttempts", "[1-3]"); 136 } 137 else 138 { 139 eventServiceConfig.retryAttempts = *retryAttemps; 140 } 141 } 142 143 if (retryInterval) 144 { 145 // Supported range [5 - 180] 146 if ((*retryInterval < 5) || (*retryInterval > 180)) 147 { 148 messages::queryParameterOutOfRange( 149 asyncResp->res, std::to_string(*retryInterval), 150 "DeliveryRetryIntervalSeconds", "[5-180]"); 151 } 152 else 153 { 154 eventServiceConfig.retryTimeoutInterval = *retryInterval; 155 } 156 } 157 158 EventServiceManager::getInstance().setEventServiceConfig( 159 eventServiceConfig); 160 }); 161 } 162 163 inline void requestRoutesSubmitTestEvent(App& app) 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)) 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)) 193 { 194 return; 195 } 196 asyncResp->res.jsonValue["@odata.type"] = 197 "#EventDestinationCollection.EventDestinationCollection"; 198 asyncResp->res.jsonValue["@odata.id"] = 199 "/redfish/v1/EventService/Subscriptions"; 200 asyncResp->res.jsonValue["Name"] = "Event Destination Collections"; 201 202 nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"]; 203 204 std::vector<std::string> subscripIds = 205 EventServiceManager::getInstance().getAllIDs(); 206 memberArray = nlohmann::json::array(); 207 asyncResp->res.jsonValue["Members@odata.count"] = subscripIds.size(); 208 209 for (const std::string& id : subscripIds) 210 { 211 nlohmann::json::object_t member; 212 member["@odata.id"] = "/redfish/v1/EventService/Subscriptions/" + 213 id; 214 memberArray.emplace_back(std::move(member)); 215 } 216 }); 217 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/") 218 .privileges(redfish::privileges::postEventDestinationCollection) 219 .methods(boost::beast::http::verb::post)( 220 [&app](const crow::Request& req, 221 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 222 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 223 { 224 return; 225 } 226 if (EventServiceManager::getInstance().getNumberOfSubscriptions() >= 227 maxNoOfSubscriptions) 228 { 229 messages::eventSubscriptionLimitExceeded(asyncResp->res); 230 return; 231 } 232 std::string destUrl; 233 std::string protocol; 234 std::optional<std::string> context; 235 std::optional<std::string> subscriptionType; 236 std::optional<std::string> eventFormatType2; 237 std::optional<std::string> retryPolicy; 238 std::optional<std::vector<std::string>> msgIds; 239 std::optional<std::vector<std::string>> regPrefixes; 240 std::optional<std::vector<std::string>> resTypes; 241 std::optional<std::vector<nlohmann::json>> headers; 242 std::optional<std::vector<nlohmann::json>> mrdJsonArray; 243 244 if (!json_util::readJsonPatch( 245 req, asyncResp->res, "Destination", destUrl, "Context", context, 246 "Protocol", protocol, "SubscriptionType", subscriptionType, 247 "EventFormatType", eventFormatType2, "HttpHeaders", headers, 248 "RegistryPrefixes", regPrefixes, "MessageIds", msgIds, 249 "DeliveryRetryPolicy", retryPolicy, "MetricReportDefinitions", 250 mrdJsonArray, "ResourceTypes", resTypes)) 251 { 252 return; 253 } 254 255 // https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers 256 static constexpr const uint16_t maxDestinationSize = 2000; 257 if (destUrl.size() > maxDestinationSize) 258 { 259 messages::stringValueTooLong(asyncResp->res, "Destination", 260 maxDestinationSize); 261 return; 262 } 263 264 if (regPrefixes && msgIds) 265 { 266 if (!regPrefixes->empty() && !msgIds->empty()) 267 { 268 messages::propertyValueConflict(asyncResp->res, "MessageIds", 269 "RegistryPrefixes"); 270 return; 271 } 272 } 273 274 std::string host; 275 std::string urlProto; 276 uint16_t port = 0; 277 std::string path; 278 279 if (!crow::utility::validateAndSplitUrl(destUrl, urlProto, host, port, 280 path)) 281 { 282 BMCWEB_LOG_WARNING 283 << "Failed to validate and split destination url"; 284 messages::propertyValueFormatError(asyncResp->res, destUrl, 285 "Destination"); 286 return; 287 } 288 289 if (path.empty()) 290 { 291 path = "/"; 292 } 293 std::shared_ptr<Subscription> subValue = std::make_shared<Subscription>( 294 host, port, path, urlProto, app.ioContext()); 295 296 subValue->destinationUrl = destUrl; 297 298 if (subscriptionType) 299 { 300 if (*subscriptionType != "RedfishEvent") 301 { 302 messages::propertyValueNotInList( 303 asyncResp->res, *subscriptionType, "SubscriptionType"); 304 return; 305 } 306 subValue->subscriptionType = *subscriptionType; 307 } 308 else 309 { 310 subValue->subscriptionType = "RedfishEvent"; // Default 311 } 312 313 if (protocol != "Redfish") 314 { 315 messages::propertyValueNotInList(asyncResp->res, protocol, 316 "Protocol"); 317 return; 318 } 319 subValue->protocol = protocol; 320 321 if (eventFormatType2) 322 { 323 if (std::find(supportedEvtFormatTypes.begin(), 324 supportedEvtFormatTypes.end(), 325 *eventFormatType2) == supportedEvtFormatTypes.end()) 326 { 327 messages::propertyValueNotInList( 328 asyncResp->res, *eventFormatType2, "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 // This value is selected aribitrarily. 342 constexpr const size_t maxContextSize = 256; 343 if (context->size() > maxContextSize) 344 { 345 messages::stringValueTooLong(asyncResp->res, "Context", 346 maxContextSize); 347 return; 348 } 349 subValue->customText = *context; 350 } 351 352 if (headers) 353 { 354 size_t cumulativeLen = 0; 355 356 for (const nlohmann::json& headerChunk : *headers) 357 { 358 std::string hdr{headerChunk.dump( 359 -1, ' ', true, nlohmann::json::error_handler_t::replace)}; 360 cumulativeLen += hdr.length(); 361 362 // This value is selected to mirror http_connection.hpp 363 constexpr const uint16_t maxHeaderSizeED = 8096; 364 if (cumulativeLen > maxHeaderSizeED) 365 { 366 messages::arraySizeTooLong(asyncResp->res, "HttpHeaders", 367 maxHeaderSizeED); 368 return; 369 } 370 for (const auto& item : headerChunk.items()) 371 { 372 const std::string* value = 373 item.value().get_ptr<const std::string*>(); 374 if (value == nullptr) 375 { 376 messages::propertyValueFormatError( 377 asyncResp->res, item.value().dump(2, 1), 378 "HttpHeaders/" + item.key()); 379 return; 380 } 381 subValue->httpHeaders.set(item.key(), *value); 382 } 383 } 384 } 385 386 if (regPrefixes) 387 { 388 for (const std::string& it : *regPrefixes) 389 { 390 if (std::find(supportedRegPrefixes.begin(), 391 supportedRegPrefixes.end(), 392 it) == supportedRegPrefixes.end()) 393 { 394 messages::propertyValueNotInList(asyncResp->res, it, 395 "RegistryPrefixes"); 396 return; 397 } 398 } 399 subValue->registryPrefixes = *regPrefixes; 400 } 401 402 if (resTypes) 403 { 404 for (const std::string& it : *resTypes) 405 { 406 if (std::find(supportedResourceTypes.begin(), 407 supportedResourceTypes.end(), 408 it) == supportedResourceTypes.end()) 409 { 410 messages::propertyValueNotInList(asyncResp->res, it, 411 "ResourceTypes"); 412 return; 413 } 414 } 415 subValue->resourceTypes = *resTypes; 416 } 417 418 if (msgIds) 419 { 420 std::vector<std::string> registryPrefix; 421 422 // If no registry prefixes are mentioned, consider all 423 // supported prefixes 424 if (subValue->registryPrefixes.empty()) 425 { 426 registryPrefix.assign(supportedRegPrefixes.begin(), 427 supportedRegPrefixes.end()); 428 } 429 else 430 { 431 registryPrefix = subValue->registryPrefixes; 432 } 433 434 for (const std::string& id : *msgIds) 435 { 436 bool validId = false; 437 438 // Check for Message ID in each of the selected Registry 439 for (const std::string& it : registryPrefix) 440 { 441 const std::span<const redfish::registries::MessageEntry> 442 registry = 443 redfish::registries::getRegistryFromPrefix(it); 444 445 if (std::any_of( 446 registry.begin(), registry.end(), 447 [&id](const redfish::registries::MessageEntry& 448 messageEntry) { 449 return id == messageEntry.first; 450 })) 451 { 452 validId = true; 453 break; 454 } 455 } 456 457 if (!validId) 458 { 459 messages::propertyValueNotInList(asyncResp->res, id, 460 "MessageIds"); 461 return; 462 } 463 } 464 465 subValue->registryMsgIds = *msgIds; 466 } 467 468 if (retryPolicy) 469 { 470 if (std::find(supportedRetryPolicies.begin(), 471 supportedRetryPolicies.end(), 472 *retryPolicy) == supportedRetryPolicies.end()) 473 { 474 messages::propertyValueNotInList(asyncResp->res, *retryPolicy, 475 "DeliveryRetryPolicy"); 476 return; 477 } 478 subValue->retryPolicy = *retryPolicy; 479 } 480 else 481 { 482 // Default "TerminateAfterRetries" 483 subValue->retryPolicy = "TerminateAfterRetries"; 484 } 485 486 if (mrdJsonArray) 487 { 488 for (nlohmann::json& mrdObj : *mrdJsonArray) 489 { 490 std::string mrdUri; 491 492 if (!json_util::readJson(mrdObj, asyncResp->res, "@odata.id", 493 mrdUri)) 494 495 { 496 return; 497 } 498 subValue->metricReportDefinitions.emplace_back(mrdUri); 499 } 500 } 501 502 std::string id = 503 EventServiceManager::getInstance().addSubscription(subValue); 504 if (id.empty()) 505 { 506 messages::internalError(asyncResp->res); 507 return; 508 } 509 510 messages::created(asyncResp->res); 511 asyncResp->res.addHeader( 512 "Location", "/redfish/v1/EventService/Subscriptions/" + id); 513 }); 514 } 515 516 inline void requestRoutesEventDestination(App& app) 517 { 518 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 519 .privileges(redfish::privileges::getEventDestination) 520 .methods(boost::beast::http::verb::get)( 521 [&app](const crow::Request& req, 522 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 523 const std::string& param) { 524 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 525 { 526 return; 527 } 528 std::shared_ptr<Subscription> subValue = 529 EventServiceManager::getInstance().getSubscription(param); 530 if (subValue == nullptr) 531 { 532 asyncResp->res.result(boost::beast::http::status::not_found); 533 return; 534 } 535 const std::string& id = param; 536 537 asyncResp->res.jsonValue["@odata.type"] = 538 "#EventDestination.v1_7_0.EventDestination"; 539 asyncResp->res.jsonValue["Protocol"] = "Redfish"; 540 asyncResp->res.jsonValue["@odata.id"] = 541 "/redfish/v1/EventService/Subscriptions/" + id; 542 asyncResp->res.jsonValue["Id"] = id; 543 asyncResp->res.jsonValue["Name"] = "Event Destination " + id; 544 asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl; 545 asyncResp->res.jsonValue["Context"] = subValue->customText; 546 asyncResp->res.jsonValue["SubscriptionType"] = 547 subValue->subscriptionType; 548 asyncResp->res.jsonValue["HttpHeaders"] = nlohmann::json::array(); 549 asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType; 550 asyncResp->res.jsonValue["RegistryPrefixes"] = 551 subValue->registryPrefixes; 552 asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes; 553 554 asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds; 555 asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy; 556 557 nlohmann::json::array_t mrdJsonArray; 558 for (const auto& mdrUri : subValue->metricReportDefinitions) 559 { 560 nlohmann::json::object_t mdr; 561 mdr["@odata.id"] = mdrUri; 562 mrdJsonArray.emplace_back(std::move(mdr)); 563 } 564 asyncResp->res.jsonValue["MetricReportDefinitions"] = 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 [&app](const crow::Request& req, 574 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 575 const std::string& param) { 576 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 577 { 578 return; 579 } 580 std::shared_ptr<Subscription> subValue = 581 EventServiceManager::getInstance().getSubscription(param); 582 if (subValue == nullptr) 583 { 584 asyncResp->res.result(boost::beast::http::status::not_found); 585 return; 586 } 587 588 std::optional<std::string> context; 589 std::optional<std::string> retryPolicy; 590 std::optional<std::vector<nlohmann::json>> headers; 591 592 if (!json_util::readJsonPatch(req, asyncResp->res, "Context", context, 593 "DeliveryRetryPolicy", retryPolicy, 594 "HttpHeaders", headers)) 595 { 596 return; 597 } 598 599 if (context) 600 { 601 subValue->customText = *context; 602 } 603 604 if (headers) 605 { 606 boost::beast::http::fields fields; 607 for (const nlohmann::json& headerChunk : *headers) 608 { 609 for (const auto& it : headerChunk.items()) 610 { 611 const std::string* value = 612 it.value().get_ptr<const std::string*>(); 613 if (value == nullptr) 614 { 615 messages::propertyValueFormatError( 616 asyncResp->res, it.value().dump(2, ' ', true), 617 "HttpHeaders/" + it.key()); 618 return; 619 } 620 fields.set(it.key(), *value); 621 } 622 } 623 subValue->httpHeaders = fields; 624 } 625 626 if (retryPolicy) 627 { 628 if (std::find(supportedRetryPolicies.begin(), 629 supportedRetryPolicies.end(), 630 *retryPolicy) == supportedRetryPolicies.end()) 631 { 632 messages::propertyValueNotInList(asyncResp->res, *retryPolicy, 633 "DeliveryRetryPolicy"); 634 return; 635 } 636 subValue->retryPolicy = *retryPolicy; 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 [&app](const crow::Request& req, 649 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 650 const std::string& param) { 651 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 652 { 653 return; 654 } 655 if (!EventServiceManager::getInstance().isSubscriptionExist(param)) 656 { 657 asyncResp->res.result(boost::beast::http::status::not_found); 658 return; 659 } 660 EventServiceManager::getInstance().deleteSubscription(param); 661 }); 662 } 663 664 } // namespace redfish 665