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