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