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