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