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