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 EventServiceManager::getInstance().sendTestEventLog(); 165 asyncResp->res.result(boost::beast::http::status::no_content); 166 }); 167 } 168 169 inline void requestRoutesEventDestinationCollection(App& app) 170 { 171 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions") 172 .privileges(redfish::privileges::getEventDestinationCollection) 173 .methods(boost::beast::http::verb::get)( 174 [](const crow::Request&, 175 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 176 asyncResp->res.jsonValue = { 177 {"@odata.type", 178 "#EventDestinationCollection.EventDestinationCollection"}, 179 {"@odata.id", "/redfish/v1/EventService/Subscriptions"}, 180 {"Name", "Event Destination Collections"}}; 181 182 nlohmann::json& memberArray = 183 asyncResp->res.jsonValue["Members"]; 184 185 std::vector<std::string> subscripIds = 186 EventServiceManager::getInstance().getAllIDs(); 187 memberArray = nlohmann::json::array(); 188 asyncResp->res.jsonValue["Members@odata.count"] = 189 subscripIds.size(); 190 191 for (const std::string& id : subscripIds) 192 { 193 memberArray.push_back( 194 {{"@odata.id", 195 "/redfish/v1/EventService/Subscriptions/" + id}}); 196 } 197 }); 198 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/") 199 .privileges(redfish::privileges::postEventDestinationCollection) 200 .methods(boost::beast::http::verb::post)( 201 [](const crow::Request& req, 202 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 203 if (EventServiceManager::getInstance() 204 .getNumberOfSubscriptions() >= maxNoOfSubscriptions) 205 { 206 messages::eventSubscriptionLimitExceeded(asyncResp->res); 207 return; 208 } 209 std::string destUrl; 210 std::string protocol; 211 std::optional<std::string> context; 212 std::optional<std::string> subscriptionType; 213 std::optional<std::string> eventFormatType2; 214 std::optional<std::string> retryPolicy; 215 std::optional<std::vector<std::string>> msgIds; 216 std::optional<std::vector<std::string>> regPrefixes; 217 std::optional<std::vector<std::string>> resTypes; 218 std::optional<std::vector<nlohmann::json>> headers; 219 std::optional<std::vector<nlohmann::json>> mrdJsonArray; 220 221 if (!json_util::readJson( 222 req, asyncResp->res, "Destination", destUrl, "Context", 223 context, "Protocol", protocol, "SubscriptionType", 224 subscriptionType, "EventFormatType", eventFormatType2, 225 "HttpHeaders", headers, "RegistryPrefixes", regPrefixes, 226 "MessageIds", msgIds, "DeliveryRetryPolicy", 227 retryPolicy, "MetricReportDefinitions", mrdJsonArray, 228 "ResourceTypes", resTypes)) 229 { 230 return; 231 } 232 233 if (regPrefixes && msgIds) 234 { 235 if (!regPrefixes->empty() && !msgIds->empty()) 236 { 237 messages::mutualExclusiveProperties( 238 asyncResp->res, "RegistryPrefixes", "MessageIds"); 239 return; 240 } 241 } 242 243 // Validate the URL using regex expression 244 // Format: <protocol>://<host>:<port>/<uri> 245 // protocol: http/https 246 // host: Exclude ' ', ':', '#', '?' 247 // port: Empty or numeric value with ':' separator. 248 // uri: Start with '/' and Exclude '#', ' ' 249 // Can include query params(ex: '/event?test=1') 250 // TODO: Need to validate hostname extensively(as per rfc) 251 const std::regex urlRegex( 252 "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/" 253 "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)"); 254 std::cmatch match; 255 if (!std::regex_match(destUrl.c_str(), match, urlRegex)) 256 { 257 messages::propertyValueFormatError(asyncResp->res, destUrl, 258 "Destination"); 259 return; 260 } 261 262 std::string uriProto = 263 std::string(match[1].first, match[1].second); 264 if (uriProto == "http") 265 { 266 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING 267 messages::propertyValueFormatError(asyncResp->res, destUrl, 268 "Destination"); 269 return; 270 #endif 271 } 272 273 std::string host = std::string(match[2].first, match[2].second); 274 std::string port = std::string(match[3].first, match[3].second); 275 std::string path = std::string(match[4].first, match[4].second); 276 if (port.empty()) 277 { 278 if (uriProto == "http") 279 { 280 port = "80"; 281 } 282 else 283 { 284 port = "443"; 285 } 286 } 287 if (path.empty()) 288 { 289 path = "/"; 290 } 291 292 std::shared_ptr<Subscription> subValue = 293 std::make_shared<Subscription>(host, port, path, uriProto); 294 295 subValue->destinationUrl = destUrl; 296 297 if (subscriptionType) 298 { 299 if (*subscriptionType != "RedfishEvent") 300 { 301 messages::propertyValueNotInList(asyncResp->res, 302 *subscriptionType, 303 "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) == 326 supportedEvtFormatTypes.end()) 327 { 328 messages::propertyValueNotInList(asyncResp->res, 329 *eventFormatType2, 330 "EventFormatType"); 331 return; 332 } 333 subValue->eventFormatType = *eventFormatType2; 334 } 335 else 336 { 337 // If not specified, use default "Event" 338 subValue->eventFormatType = "Event"; 339 } 340 341 if (context) 342 { 343 subValue->customText = *context; 344 } 345 346 if (headers) 347 { 348 for (const nlohmann::json& headerChunk : *headers) 349 { 350 for (const auto& item : headerChunk.items()) 351 { 352 const std::string* value = 353 item.value().get_ptr<const std::string*>(); 354 if (value == nullptr) 355 { 356 messages::propertyValueFormatError( 357 asyncResp->res, item.value().dump(2, true), 358 "HttpHeaders/" + item.key()); 359 return; 360 } 361 subValue->httpHeaders.set(item.key(), *value); 362 } 363 } 364 } 365 366 if (regPrefixes) 367 { 368 for (const std::string& it : *regPrefixes) 369 { 370 if (std::find(supportedRegPrefixes.begin(), 371 supportedRegPrefixes.end(), 372 it) == supportedRegPrefixes.end()) 373 { 374 messages::propertyValueNotInList( 375 asyncResp->res, it, "RegistryPrefixes"); 376 return; 377 } 378 } 379 subValue->registryPrefixes = *regPrefixes; 380 } 381 382 if (resTypes) 383 { 384 for (const std::string& it : *resTypes) 385 { 386 if (std::find(supportedResourceTypes.begin(), 387 supportedResourceTypes.end(), 388 it) == supportedResourceTypes.end()) 389 { 390 messages::propertyValueNotInList(asyncResp->res, it, 391 "ResourceTypes"); 392 return; 393 } 394 } 395 subValue->resourceTypes = *resTypes; 396 } 397 398 if (msgIds) 399 { 400 std::vector<std::string> registryPrefix; 401 402 // If no registry prefixes are mentioned, consider all 403 // supported prefixes 404 if (subValue->registryPrefixes.empty()) 405 { 406 registryPrefix.assign(supportedRegPrefixes.begin(), 407 supportedRegPrefixes.end()); 408 } 409 else 410 { 411 registryPrefix = subValue->registryPrefixes; 412 } 413 414 for (const std::string& id : *msgIds) 415 { 416 bool validId = false; 417 418 // Check for Message ID in each of the selected Registry 419 for (const std::string& it : registryPrefix) 420 { 421 const std::span< 422 const redfish::message_registries::MessageEntry> 423 registry = redfish::message_registries:: 424 getRegistryFromPrefix(it); 425 426 if (std::any_of( 427 registry.begin(), registry.end(), 428 [&id](const redfish::message_registries:: 429 MessageEntry& messageEntry) { 430 return !id.compare(messageEntry.first); 431 })) 432 { 433 validId = true; 434 break; 435 } 436 } 437 438 if (!validId) 439 { 440 messages::propertyValueNotInList(asyncResp->res, id, 441 "MessageIds"); 442 return; 443 } 444 } 445 446 subValue->registryMsgIds = *msgIds; 447 } 448 449 if (retryPolicy) 450 { 451 if (std::find(supportedRetryPolicies.begin(), 452 supportedRetryPolicies.end(), 453 *retryPolicy) == supportedRetryPolicies.end()) 454 { 455 messages::propertyValueNotInList(asyncResp->res, 456 *retryPolicy, 457 "DeliveryRetryPolicy"); 458 return; 459 } 460 subValue->retryPolicy = *retryPolicy; 461 } 462 else 463 { 464 // Default "TerminateAfterRetries" 465 subValue->retryPolicy = "TerminateAfterRetries"; 466 } 467 468 if (mrdJsonArray) 469 { 470 for (nlohmann::json& mrdObj : *mrdJsonArray) 471 { 472 std::string mrdUri; 473 if (json_util::getValueFromJsonObject( 474 mrdObj, "@odata.id", mrdUri)) 475 { 476 subValue->metricReportDefinitions.emplace_back( 477 mrdUri); 478 } 479 else 480 { 481 messages::propertyValueFormatError( 482 asyncResp->res, 483 mrdObj.dump( 484 2, ' ', true, 485 nlohmann::json::error_handler_t::replace), 486 "MetricReportDefinitions"); 487 return; 488 } 489 } 490 } 491 492 std::string id = 493 EventServiceManager::getInstance().addSubscription( 494 subValue); 495 if (id.empty()) 496 { 497 messages::internalError(asyncResp->res); 498 return; 499 } 500 501 messages::created(asyncResp->res); 502 asyncResp->res.addHeader( 503 "Location", "/redfish/v1/EventService/Subscriptions/" + id); 504 }); 505 } 506 507 inline void requestRoutesEventDestination(App& app) 508 { 509 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 510 .privileges(redfish::privileges::getEventDestination) 511 .methods(boost::beast::http::verb::get)( 512 [](const crow::Request&, 513 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 514 const std::string& param) { 515 std::shared_ptr<Subscription> subValue = 516 EventServiceManager::getInstance().getSubscription(param); 517 if (subValue == nullptr) 518 { 519 asyncResp->res.result( 520 boost::beast::http::status::not_found); 521 return; 522 } 523 const std::string& id = param; 524 525 asyncResp->res.jsonValue = { 526 {"@odata.type", 527 "#EventDestination.v1_7_0.EventDestination"}, 528 {"Protocol", "Redfish"}}; 529 asyncResp->res.jsonValue["@odata.id"] = 530 "/redfish/v1/EventService/Subscriptions/" + id; 531 asyncResp->res.jsonValue["Id"] = id; 532 asyncResp->res.jsonValue["Name"] = "Event Destination " + id; 533 asyncResp->res.jsonValue["Destination"] = 534 subValue->destinationUrl; 535 asyncResp->res.jsonValue["Context"] = subValue->customText; 536 asyncResp->res.jsonValue["SubscriptionType"] = 537 subValue->subscriptionType; 538 asyncResp->res.jsonValue["HttpHeaders"] = 539 nlohmann::json::array(); 540 asyncResp->res.jsonValue["EventFormatType"] = 541 subValue->eventFormatType; 542 asyncResp->res.jsonValue["RegistryPrefixes"] = 543 subValue->registryPrefixes; 544 asyncResp->res.jsonValue["ResourceTypes"] = 545 subValue->resourceTypes; 546 547 asyncResp->res.jsonValue["MessageIds"] = 548 subValue->registryMsgIds; 549 asyncResp->res.jsonValue["DeliveryRetryPolicy"] = 550 subValue->retryPolicy; 551 552 std::vector<nlohmann::json> mrdJsonArray; 553 for (const auto& mdrUri : subValue->metricReportDefinitions) 554 { 555 mrdJsonArray.push_back({{"@odata.id", mdrUri}}); 556 } 557 asyncResp->res.jsonValue["MetricReportDefinitions"] = 558 mrdJsonArray; 559 }); 560 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 561 // The below privilege is wrong, it should be ConfigureManager OR 562 // ConfigureSelf 563 // https://github.com/openbmc/bmcweb/issues/220 564 //.privileges(redfish::privileges::patchEventDestination) 565 .privileges({{"ConfigureManager"}}) 566 .methods(boost::beast::http::verb::patch)( 567 [](const crow::Request& req, 568 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 569 const std::string& param) { 570 std::shared_ptr<Subscription> subValue = 571 EventServiceManager::getInstance().getSubscription(param); 572 if (subValue == nullptr) 573 { 574 asyncResp->res.result( 575 boost::beast::http::status::not_found); 576 return; 577 } 578 579 std::optional<std::string> context; 580 std::optional<std::string> retryPolicy; 581 std::optional<std::vector<nlohmann::json>> headers; 582 583 if (!json_util::readJson(req, asyncResp->res, "Context", 584 context, "DeliveryRetryPolicy", 585 retryPolicy, "HttpHeaders", headers)) 586 { 587 return; 588 } 589 590 if (context) 591 { 592 subValue->customText = *context; 593 } 594 595 if (headers) 596 { 597 boost::beast::http::fields fields; 598 for (const nlohmann::json& headerChunk : *headers) 599 { 600 for (auto& it : headerChunk.items()) 601 { 602 const std::string* value = 603 it.value().get_ptr<const std::string*>(); 604 if (value == nullptr) 605 { 606 messages::propertyValueFormatError( 607 asyncResp->res, 608 it.value().dump(2, ' ', true), 609 "HttpHeaders/" + it.key()); 610 return; 611 } 612 fields.set(it.key(), *value); 613 } 614 } 615 subValue->httpHeaders = fields; 616 } 617 618 if (retryPolicy) 619 { 620 if (std::find(supportedRetryPolicies.begin(), 621 supportedRetryPolicies.end(), 622 *retryPolicy) == supportedRetryPolicies.end()) 623 { 624 messages::propertyValueNotInList(asyncResp->res, 625 *retryPolicy, 626 "DeliveryRetryPolicy"); 627 return; 628 } 629 subValue->retryPolicy = *retryPolicy; 630 subValue->updateRetryPolicy(); 631 } 632 633 EventServiceManager::getInstance().updateSubscriptionData(); 634 }); 635 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 636 // The below privilege is wrong, it should be ConfigureManager OR 637 // ConfigureSelf 638 // https://github.com/openbmc/bmcweb/issues/220 639 //.privileges(redfish::privileges::deleteEventDestination) 640 .privileges({{"ConfigureManager"}}) 641 .methods(boost::beast::http::verb::delete_)( 642 [](const crow::Request&, 643 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 644 const std::string& param) { 645 if (!EventServiceManager::getInstance().isSubscriptionExist( 646 param)) 647 { 648 asyncResp->res.result( 649 boost::beast::http::status::not_found); 650 return; 651 } 652 EventServiceManager::getInstance().deleteSubscription(param); 653 }); 654 } 655 656 } // namespace redfish 657