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