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, true), 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 })) 437 { 438 validId = true; 439 break; 440 } 441 } 442 443 if (!validId) 444 { 445 messages::propertyValueNotInList(asyncResp->res, id, 446 "MessageIds"); 447 return; 448 } 449 } 450 451 subValue->registryMsgIds = *msgIds; 452 } 453 454 if (retryPolicy) 455 { 456 if (std::find(supportedRetryPolicies.begin(), 457 supportedRetryPolicies.end(), 458 *retryPolicy) == supportedRetryPolicies.end()) 459 { 460 messages::propertyValueNotInList(asyncResp->res, 461 *retryPolicy, 462 "DeliveryRetryPolicy"); 463 return; 464 } 465 subValue->retryPolicy = *retryPolicy; 466 } 467 else 468 { 469 // Default "TerminateAfterRetries" 470 subValue->retryPolicy = "TerminateAfterRetries"; 471 } 472 473 if (mrdJsonArray) 474 { 475 for (nlohmann::json& mrdObj : *mrdJsonArray) 476 { 477 std::string mrdUri; 478 if (json_util::getValueFromJsonObject( 479 mrdObj, "@odata.id", mrdUri)) 480 { 481 subValue->metricReportDefinitions.emplace_back( 482 mrdUri); 483 } 484 else 485 { 486 messages::propertyValueFormatError( 487 asyncResp->res, 488 mrdObj.dump( 489 2, ' ', true, 490 nlohmann::json::error_handler_t::replace), 491 "MetricReportDefinitions"); 492 return; 493 } 494 } 495 } 496 497 std::string id = 498 EventServiceManager::getInstance().addSubscription( 499 subValue); 500 if (id.empty()) 501 { 502 messages::internalError(asyncResp->res); 503 return; 504 } 505 506 messages::created(asyncResp->res); 507 asyncResp->res.addHeader( 508 "Location", "/redfish/v1/EventService/Subscriptions/" + id); 509 }); 510 } 511 512 inline void requestRoutesEventDestination(App& app) 513 { 514 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 515 .privileges(redfish::privileges::getEventDestination) 516 .methods(boost::beast::http::verb::get)( 517 [](const crow::Request&, 518 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 519 const std::string& param) { 520 std::shared_ptr<Subscription> subValue = 521 EventServiceManager::getInstance().getSubscription(param); 522 if (subValue == nullptr) 523 { 524 asyncResp->res.result( 525 boost::beast::http::status::not_found); 526 return; 527 } 528 const std::string& id = param; 529 530 asyncResp->res.jsonValue = { 531 {"@odata.type", 532 "#EventDestination.v1_7_0.EventDestination"}, 533 {"Protocol", "Redfish"}}; 534 asyncResp->res.jsonValue["@odata.id"] = 535 "/redfish/v1/EventService/Subscriptions/" + id; 536 asyncResp->res.jsonValue["Id"] = id; 537 asyncResp->res.jsonValue["Name"] = "Event Destination " + id; 538 asyncResp->res.jsonValue["Destination"] = 539 subValue->destinationUrl; 540 asyncResp->res.jsonValue["Context"] = subValue->customText; 541 asyncResp->res.jsonValue["SubscriptionType"] = 542 subValue->subscriptionType; 543 asyncResp->res.jsonValue["HttpHeaders"] = 544 nlohmann::json::array(); 545 asyncResp->res.jsonValue["EventFormatType"] = 546 subValue->eventFormatType; 547 asyncResp->res.jsonValue["RegistryPrefixes"] = 548 subValue->registryPrefixes; 549 asyncResp->res.jsonValue["ResourceTypes"] = 550 subValue->resourceTypes; 551 552 asyncResp->res.jsonValue["MessageIds"] = 553 subValue->registryMsgIds; 554 asyncResp->res.jsonValue["DeliveryRetryPolicy"] = 555 subValue->retryPolicy; 556 557 std::vector<nlohmann::json> mrdJsonArray; 558 for (const auto& mdrUri : subValue->metricReportDefinitions) 559 { 560 mrdJsonArray.push_back({{"@odata.id", mdrUri}}); 561 } 562 asyncResp->res.jsonValue["MetricReportDefinitions"] = 563 mrdJsonArray; 564 }); 565 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 566 // The below privilege is wrong, it should be ConfigureManager OR 567 // ConfigureSelf 568 // https://github.com/openbmc/bmcweb/issues/220 569 //.privileges(redfish::privileges::patchEventDestination) 570 .privileges({{"ConfigureManager"}}) 571 .methods(boost::beast::http::verb::patch)( 572 [](const crow::Request& req, 573 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 574 const std::string& param) { 575 std::shared_ptr<Subscription> subValue = 576 EventServiceManager::getInstance().getSubscription(param); 577 if (subValue == nullptr) 578 { 579 asyncResp->res.result( 580 boost::beast::http::status::not_found); 581 return; 582 } 583 584 std::optional<std::string> context; 585 std::optional<std::string> retryPolicy; 586 std::optional<std::vector<nlohmann::json>> headers; 587 588 if (!json_util::readJson(req, asyncResp->res, "Context", 589 context, "DeliveryRetryPolicy", 590 retryPolicy, "HttpHeaders", headers)) 591 { 592 return; 593 } 594 595 if (context) 596 { 597 subValue->customText = *context; 598 } 599 600 if (headers) 601 { 602 boost::beast::http::fields fields; 603 for (const nlohmann::json& headerChunk : *headers) 604 { 605 for (auto& it : headerChunk.items()) 606 { 607 const std::string* value = 608 it.value().get_ptr<const std::string*>(); 609 if (value == nullptr) 610 { 611 messages::propertyValueFormatError( 612 asyncResp->res, 613 it.value().dump(2, ' ', true), 614 "HttpHeaders/" + it.key()); 615 return; 616 } 617 fields.set(it.key(), *value); 618 } 619 } 620 subValue->httpHeaders = fields; 621 } 622 623 if (retryPolicy) 624 { 625 if (std::find(supportedRetryPolicies.begin(), 626 supportedRetryPolicies.end(), 627 *retryPolicy) == supportedRetryPolicies.end()) 628 { 629 messages::propertyValueNotInList(asyncResp->res, 630 *retryPolicy, 631 "DeliveryRetryPolicy"); 632 return; 633 } 634 subValue->retryPolicy = *retryPolicy; 635 subValue->updateRetryPolicy(); 636 } 637 638 EventServiceManager::getInstance().updateSubscriptionData(); 639 }); 640 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 641 // The below privilege is wrong, it should be ConfigureManager OR 642 // ConfigureSelf 643 // https://github.com/openbmc/bmcweb/issues/220 644 //.privileges(redfish::privileges::deleteEventDestination) 645 .privileges({{"ConfigureManager"}}) 646 .methods(boost::beast::http::verb::delete_)( 647 [](const crow::Request&, 648 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 649 const std::string& param) { 650 if (!EventServiceManager::getInstance().isSubscriptionExist( 651 param)) 652 { 653 asyncResp->res.result( 654 boost::beast::http::status::not_found); 655 return; 656 } 657 EventServiceManager::getInstance().deleteSubscription(param); 658 }); 659 } 660 661 } // namespace redfish 662