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 <registries/privilege_registry.hpp> 21 22 namespace redfish 23 { 24 25 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = { 26 eventFormatType, metricReportFormatType}; 27 static constexpr const std::array<const char*, 3> supportedRegPrefixes = { 28 "Base", "OpenBMC", "TaskEvent"}; 29 static constexpr const std::array<const char*, 3> supportedRetryPolicies = { 30 "TerminateAfterRetries", "SuspendRetries", "RetryForever"}; 31 32 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE 33 static constexpr const std::array<const char*, 2> supportedResourceTypes = { 34 "IBMConfigFile", "Task"}; 35 #else 36 static constexpr const std::array<const char*, 1> supportedResourceTypes = { 37 "Task"}; 38 #endif 39 40 static constexpr const uint8_t maxNoOfSubscriptions = 20; 41 42 inline void requestRoutesEventService(App& app) 43 { 44 BMCWEB_ROUTE(app, "/redfish/v1/EventService/") 45 .privileges(redfish::privileges::getEventService) 46 .methods(boost::beast::http::verb::get)( 47 [](const crow::Request&, 48 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 49 asyncResp->res.jsonValue = { 50 {"@odata.type", "#EventService.v1_5_0.EventService"}, 51 {"Id", "EventService"}, 52 {"Name", "Event Service"}, 53 {"Subscriptions", 54 {{"@odata.id", "/redfish/v1/EventService/Subscriptions"}}}, 55 {"Actions", 56 {{"#EventService.SubmitTestEvent", 57 {{"target", "/redfish/v1/EventService/Actions/" 58 "EventService.SubmitTestEvent"}}}}}, 59 {"@odata.id", "/redfish/v1/EventService"}}; 60 61 const persistent_data::EventServiceConfig eventServiceConfig = 62 persistent_data::EventServiceStore::getInstance() 63 .getEventServiceConfig(); 64 65 asyncResp->res.jsonValue["Status"]["State"] = 66 (eventServiceConfig.enabled ? "Enabled" : "Disabled"); 67 asyncResp->res.jsonValue["ServiceEnabled"] = 68 eventServiceConfig.enabled; 69 asyncResp->res.jsonValue["DeliveryRetryAttempts"] = 70 eventServiceConfig.retryAttempts; 71 asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] = 72 eventServiceConfig.retryTimeoutInterval; 73 asyncResp->res.jsonValue["EventFormatTypes"] = 74 supportedEvtFormatTypes; 75 asyncResp->res.jsonValue["RegistryPrefixes"] = 76 supportedRegPrefixes; 77 asyncResp->res.jsonValue["ResourceTypes"] = 78 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 subValue->httpHeaders = *headers; 347 } 348 349 if (regPrefixes) 350 { 351 for (const std::string& it : *regPrefixes) 352 { 353 if (std::find(supportedRegPrefixes.begin(), 354 supportedRegPrefixes.end(), 355 it) == supportedRegPrefixes.end()) 356 { 357 messages::propertyValueNotInList( 358 asyncResp->res, it, "RegistryPrefixes"); 359 return; 360 } 361 } 362 subValue->registryPrefixes = *regPrefixes; 363 } 364 365 if (resTypes) 366 { 367 for (const std::string& it : *resTypes) 368 { 369 if (std::find(supportedResourceTypes.begin(), 370 supportedResourceTypes.end(), 371 it) == supportedResourceTypes.end()) 372 { 373 messages::propertyValueNotInList(asyncResp->res, it, 374 "ResourceTypes"); 375 return; 376 } 377 } 378 subValue->resourceTypes = *resTypes; 379 } 380 381 if (msgIds) 382 { 383 std::vector<std::string> registryPrefix; 384 385 // If no registry prefixes are mentioned, consider all 386 // supported prefixes 387 if (subValue->registryPrefixes.empty()) 388 { 389 registryPrefix.assign(supportedRegPrefixes.begin(), 390 supportedRegPrefixes.end()); 391 } 392 else 393 { 394 registryPrefix = subValue->registryPrefixes; 395 } 396 397 for (const std::string& id : *msgIds) 398 { 399 bool validId = false; 400 401 // Check for Message ID in each of the selected Registry 402 for (const std::string& it : registryPrefix) 403 { 404 const boost::beast::span< 405 const redfish::message_registries::MessageEntry> 406 registry = redfish::message_registries:: 407 getRegistryFromPrefix(it); 408 409 if (std::any_of( 410 registry.cbegin(), registry.cend(), 411 [&id](const redfish::message_registries:: 412 MessageEntry& messageEntry) { 413 return !id.compare(messageEntry.first); 414 })) 415 { 416 validId = true; 417 break; 418 } 419 } 420 421 if (!validId) 422 { 423 messages::propertyValueNotInList(asyncResp->res, id, 424 "MessageIds"); 425 return; 426 } 427 } 428 429 subValue->registryMsgIds = *msgIds; 430 } 431 432 if (retryPolicy) 433 { 434 if (std::find(supportedRetryPolicies.begin(), 435 supportedRetryPolicies.end(), 436 *retryPolicy) == supportedRetryPolicies.end()) 437 { 438 messages::propertyValueNotInList(asyncResp->res, 439 *retryPolicy, 440 "DeliveryRetryPolicy"); 441 return; 442 } 443 subValue->retryPolicy = *retryPolicy; 444 } 445 else 446 { 447 // Default "TerminateAfterRetries" 448 subValue->retryPolicy = "TerminateAfterRetries"; 449 } 450 451 if (mrdJsonArray) 452 { 453 for (nlohmann::json& mrdObj : *mrdJsonArray) 454 { 455 std::string mrdUri; 456 if (json_util::getValueFromJsonObject( 457 mrdObj, "@odata.id", mrdUri)) 458 { 459 subValue->metricReportDefinitions.emplace_back( 460 mrdUri); 461 } 462 else 463 { 464 messages::propertyValueFormatError( 465 asyncResp->res, 466 mrdObj.dump( 467 2, ' ', true, 468 nlohmann::json::error_handler_t::replace), 469 "MetricReportDefinitions"); 470 return; 471 } 472 } 473 } 474 475 std::string id = 476 EventServiceManager::getInstance().addSubscription( 477 subValue); 478 if (id.empty()) 479 { 480 messages::internalError(asyncResp->res); 481 return; 482 } 483 484 messages::created(asyncResp->res); 485 asyncResp->res.addHeader( 486 "Location", "/redfish/v1/EventService/Subscriptions/" + id); 487 }); 488 } 489 490 inline void requestRoutesEventDestination(App& app) 491 { 492 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 493 .privileges(redfish::privileges::getEventDestination) 494 .methods(boost::beast::http::verb::get)( 495 [](const crow::Request&, 496 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 497 const std::string& param) { 498 std::shared_ptr<Subscription> subValue = 499 EventServiceManager::getInstance().getSubscription(param); 500 if (subValue == nullptr) 501 { 502 asyncResp->res.result( 503 boost::beast::http::status::not_found); 504 return; 505 } 506 const std::string& id = param; 507 508 asyncResp->res.jsonValue = { 509 {"@odata.type", 510 "#EventDestination.v1_7_0.EventDestination"}, 511 {"Protocol", "Redfish"}}; 512 asyncResp->res.jsonValue["@odata.id"] = 513 "/redfish/v1/EventService/Subscriptions/" + id; 514 asyncResp->res.jsonValue["Id"] = id; 515 asyncResp->res.jsonValue["Name"] = "Event Destination " + id; 516 asyncResp->res.jsonValue["Destination"] = 517 subValue->destinationUrl; 518 asyncResp->res.jsonValue["Context"] = subValue->customText; 519 asyncResp->res.jsonValue["SubscriptionType"] = 520 subValue->subscriptionType; 521 asyncResp->res.jsonValue["HttpHeaders"] = 522 nlohmann::json::array(); 523 asyncResp->res.jsonValue["EventFormatType"] = 524 subValue->eventFormatType; 525 asyncResp->res.jsonValue["RegistryPrefixes"] = 526 subValue->registryPrefixes; 527 asyncResp->res.jsonValue["ResourceTypes"] = 528 subValue->resourceTypes; 529 530 asyncResp->res.jsonValue["MessageIds"] = 531 subValue->registryMsgIds; 532 asyncResp->res.jsonValue["DeliveryRetryPolicy"] = 533 subValue->retryPolicy; 534 535 std::vector<nlohmann::json> mrdJsonArray; 536 for (const auto& mdrUri : subValue->metricReportDefinitions) 537 { 538 mrdJsonArray.push_back({{"@odata.id", mdrUri}}); 539 } 540 asyncResp->res.jsonValue["MetricReportDefinitions"] = 541 mrdJsonArray; 542 }); 543 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 544 // The below privilege is wrong, it should be ConfigureManager OR 545 // ConfigureSelf 546 // https://github.com/openbmc/bmcweb/issues/220 547 //.privileges(redfish::privileges::patchEventDestination) 548 .privileges({{"ConfigureManager"}}) 549 .methods(boost::beast::http::verb::patch)( 550 [](const crow::Request& req, 551 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 552 const std::string& param) { 553 std::shared_ptr<Subscription> subValue = 554 EventServiceManager::getInstance().getSubscription(param); 555 if (subValue == nullptr) 556 { 557 asyncResp->res.result( 558 boost::beast::http::status::not_found); 559 return; 560 } 561 562 std::optional<std::string> context; 563 std::optional<std::string> retryPolicy; 564 std::optional<std::vector<nlohmann::json>> headers; 565 566 if (!json_util::readJson(req, asyncResp->res, "Context", 567 context, "DeliveryRetryPolicy", 568 retryPolicy, "HttpHeaders", headers)) 569 { 570 return; 571 } 572 573 if (context) 574 { 575 subValue->customText = *context; 576 } 577 578 if (headers) 579 { 580 subValue->httpHeaders = *headers; 581 } 582 583 if (retryPolicy) 584 { 585 if (std::find(supportedRetryPolicies.begin(), 586 supportedRetryPolicies.end(), 587 *retryPolicy) == supportedRetryPolicies.end()) 588 { 589 messages::propertyValueNotInList(asyncResp->res, 590 *retryPolicy, 591 "DeliveryRetryPolicy"); 592 return; 593 } 594 subValue->retryPolicy = *retryPolicy; 595 subValue->updateRetryPolicy(); 596 } 597 598 EventServiceManager::getInstance().updateSubscriptionData(); 599 }); 600 BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/") 601 // The below privilege is wrong, it should be ConfigureManager OR 602 // ConfigureSelf 603 // https://github.com/openbmc/bmcweb/issues/220 604 //.privileges(redfish::privileges::deleteEventDestination) 605 .privileges({{"ConfigureManager"}}) 606 .methods(boost::beast::http::verb::delete_)( 607 [](const crow::Request&, 608 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 609 const std::string& param) { 610 if (!EventServiceManager::getInstance().isSubscriptionExist( 611 param)) 612 { 613 asyncResp->res.result( 614 boost::beast::http::status::not_found); 615 return; 616 } 617 EventServiceManager::getInstance().deleteSubscription(param); 618 }); 619 } 620 621 } // namespace redfish 622