1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include "app.hpp" 6 #include "async_resp.hpp" 7 #include "error_messages.hpp" 8 #include "http_request.hpp" 9 #include "http_response.hpp" 10 #include "logging.hpp" 11 #include "ossl_random.hpp" 12 #include "query.hpp" 13 #include "redfish_aggregator.hpp" 14 #include "registries/privilege_registry.hpp" 15 #include "utility.hpp" 16 #include "utils/json_utils.hpp" 17 18 #include <boost/beast/http/field.hpp> 19 #include <boost/beast/http/verb.hpp> 20 #include <boost/system/result.hpp> 21 #include <boost/url/format.hpp> 22 #include <boost/url/parse.hpp> 23 #include <boost/url/url.hpp> 24 #include <nlohmann/json.hpp> 25 26 #include <cstddef> 27 #include <functional> 28 #include <memory> 29 #include <unordered_map> 30 #include <utility> 31 32 namespace redfish 33 { 34 35 inline void handleAggregationServiceHead( 36 App& app, const crow::Request& req, 37 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 38 { 39 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 40 { 41 return; 42 } 43 asyncResp->res.addHeader( 44 boost::beast::http::field::link, 45 "</redfish/v1/JsonSchemas/AggregationService/AggregationService.json>; rel=describedby"); 46 } 47 48 inline void handleAggregationServiceGet( 49 App& app, const crow::Request& req, 50 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 51 { 52 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 53 { 54 return; 55 } 56 asyncResp->res.addHeader( 57 boost::beast::http::field::link, 58 "</redfish/v1/JsonSchemas/AggregationService/AggregationService.json>; rel=describedby"); 59 nlohmann::json& json = asyncResp->res.jsonValue; 60 json["@odata.id"] = "/redfish/v1/AggregationService"; 61 json["@odata.type"] = "#AggregationService.v1_0_1.AggregationService"; 62 json["Id"] = "AggregationService"; 63 json["Name"] = "Aggregation Service"; 64 json["Description"] = "Aggregation Service"; 65 json["ServiceEnabled"] = true; 66 json["AggregationSources"]["@odata.id"] = 67 "/redfish/v1/AggregationService/AggregationSources"; 68 } 69 70 inline void requestRoutesAggregationService(App& app) 71 { 72 BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/") 73 .privileges(redfish::privileges::headAggregationService) 74 .methods(boost::beast::http::verb::head)( 75 std::bind_front(handleAggregationServiceHead, std::ref(app))); 76 BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/") 77 .privileges(redfish::privileges::getAggregationService) 78 .methods(boost::beast::http::verb::get)( 79 std::bind_front(handleAggregationServiceGet, std::ref(app))); 80 } 81 82 inline void populateAggregationSourceCollection( 83 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 84 const boost::system::error_code& ec, 85 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 86 { 87 // Something went wrong while querying dbus 88 if (ec) 89 { 90 messages::internalError(asyncResp->res); 91 return; 92 } 93 nlohmann::json::array_t members; 94 for (const auto& sat : satelliteInfo) 95 { 96 nlohmann::json::object_t member; 97 member["@odata.id"] = boost::urls::format( 98 "/redfish/v1/AggregationService/AggregationSources/{}", sat.first); 99 members.emplace_back(std::move(member)); 100 } 101 asyncResp->res.jsonValue["Members@odata.count"] = members.size(); 102 asyncResp->res.jsonValue["Members"] = std::move(members); 103 } 104 105 inline void handleAggregationSourceCollectionGet( 106 App& app, const crow::Request& req, 107 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 108 { 109 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 110 { 111 return; 112 } 113 asyncResp->res.addHeader( 114 boost::beast::http::field::link, 115 "</redfish/v1/JsonSchemas/AggregationSourceCollection/AggregationSourceCollection.json>; rel=describedby"); 116 nlohmann::json& json = asyncResp->res.jsonValue; 117 json["@odata.id"] = "/redfish/v1/AggregationService/AggregationSources"; 118 json["@odata.type"] = 119 "#AggregationSourceCollection.AggregationSourceCollection"; 120 json["Name"] = "Aggregation Source Collection"; 121 122 // Query D-Bus for satellite configs and add them to the Members array 123 RedfishAggregator::getInstance().getSatelliteConfigs( 124 std::bind_front(populateAggregationSourceCollection, asyncResp)); 125 } 126 127 inline void handleAggregationSourceCollectionHead( 128 App& app, const crow::Request& req, 129 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 130 { 131 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 132 { 133 return; 134 } 135 asyncResp->res.addHeader( 136 boost::beast::http::field::link, 137 "</redfish/v1/JsonSchemas/AggregationService/AggregationSourceCollection.json>; rel=describedby"); 138 } 139 140 inline void requestRoutesAggregationSourceCollection(App& app) 141 { 142 BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/") 143 .privileges(redfish::privileges::getAggregationSourceCollection) 144 .methods(boost::beast::http::verb::get)(std::bind_front( 145 handleAggregationSourceCollectionGet, std::ref(app))); 146 147 BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/") 148 .privileges(redfish::privileges::getAggregationSourceCollection) 149 .methods(boost::beast::http::verb::head)(std::bind_front( 150 handleAggregationSourceCollectionHead, std::ref(app))); 151 } 152 153 inline void populateAggregationSource( 154 const std::string& aggregationSourceId, 155 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 156 const boost::system::error_code& ec, 157 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 158 { 159 asyncResp->res.addHeader( 160 boost::beast::http::field::link, 161 "</redfish/v1/JsonSchemas/AggregationSource/AggregationSource.json>; rel=describedby"); 162 163 // Something went wrong while querying dbus 164 if (ec) 165 { 166 messages::internalError(asyncResp->res); 167 return; 168 } 169 170 const auto& sat = satelliteInfo.find(aggregationSourceId); 171 if (sat == satelliteInfo.end()) 172 { 173 messages::resourceNotFound(asyncResp->res, "AggregationSource", 174 aggregationSourceId); 175 return; 176 } 177 178 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( 179 "/redfish/v1/AggregationService/AggregationSources/{}", 180 aggregationSourceId); 181 asyncResp->res.jsonValue["@odata.type"] = 182 "#AggregationSource.v1_3_1.AggregationSource"; 183 asyncResp->res.jsonValue["Id"] = aggregationSourceId; 184 185 // TODO: We may want to change this whenever we support aggregating multiple 186 // satellite BMCs. Otherwise all AggregationSource resources will have the 187 // same "Name". 188 // TODO: We should use the "Name" from the satellite config whenever we add 189 // support for including it in the data returned in satelliteInfo. 190 asyncResp->res.jsonValue["Name"] = "Aggregation source"; 191 std::string hostName(sat->second.encoded_origin()); 192 asyncResp->res.jsonValue["HostName"] = std::move(hostName); 193 194 // Include UserName property, defaulting to null 195 auto& aggregator = RedfishAggregator::getInstance(); 196 auto it = aggregator.aggregationSources.find(aggregationSourceId); 197 if (it != aggregator.aggregationSources.end() && 198 !it->second.username.empty()) 199 { 200 asyncResp->res.jsonValue["UserName"] = it->second.username; 201 } 202 else 203 { 204 asyncResp->res.jsonValue["UserName"] = nullptr; 205 } 206 207 // The Redfish spec requires Password to be null in responses 208 asyncResp->res.jsonValue["Password"] = nullptr; 209 } 210 211 inline void handleAggregationSourceGet( 212 App& app, const crow::Request& req, 213 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 214 const std::string& aggregationSourceId) 215 { 216 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 217 { 218 return; 219 } 220 221 // Query D-Bus for satellite config corresponding to the specified 222 // AggregationSource 223 RedfishAggregator::getInstance().getSatelliteConfigs(std::bind_front( 224 populateAggregationSource, aggregationSourceId, asyncResp)); 225 } 226 227 inline void handleAggregationSourceHead( 228 App& app, const crow::Request& req, 229 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 230 const std::string& aggregationSourceId) 231 { 232 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 233 { 234 return; 235 } 236 asyncResp->res.addHeader( 237 boost::beast::http::field::link, 238 "</redfish/v1/JsonSchemas/AggregationService/AggregationSource.json>; rel=describedby"); 239 240 // Needed to prevent unused variable error 241 BMCWEB_LOG_DEBUG("Added link header to response from {}", 242 aggregationSourceId); 243 } 244 245 inline bool validateCredentialField(const std::optional<std::string>& field, 246 const std::string& fieldName, 247 crow::Response& res) 248 { 249 if (!field.has_value()) 250 { 251 return true; // Field not provided, that's okay 252 } 253 254 if (field->empty()) 255 { 256 messages::stringValueTooShort(res, fieldName, "1"); 257 return false; 258 } 259 260 if (field->find(':') != std::string::npos) 261 { 262 messages::propertyValueIncorrect(res, *field, fieldName); 263 return false; 264 } 265 266 if (field->length() > 40) 267 { 268 messages::stringValueTooLong(res, fieldName, 40); 269 return false; 270 } 271 272 return true; 273 } 274 275 inline void handleAggregationSourceCollectionPost( 276 App& app, const crow::Request& req, 277 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 278 { 279 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 280 { 281 return; 282 } 283 std::string hostname; 284 std::optional<std::string> username; 285 std::optional<std::string> password; 286 287 if (!json_util::readJsonPatch(req, asyncResp->res, "HostName", hostname, 288 "UserName", username, "Password", password)) 289 { 290 return; 291 } 292 293 boost::system::result<boost::urls::url> url = 294 boost::urls::parse_absolute_uri(hostname); 295 if (!url) 296 { 297 messages::propertyValueIncorrect(asyncResp->res, hostname, "HostName"); 298 return; 299 } 300 url->normalize(); 301 if (url->scheme() != "http" && url->scheme() != "https") 302 { 303 messages::propertyValueIncorrect(asyncResp->res, hostname, "HostName"); 304 return; 305 } 306 crow::utility::setPortDefaults(*url); 307 308 // Check for duplicate hostname 309 auto& aggregator = RedfishAggregator::getInstance(); 310 for (const auto& [existingPrefix, existingSource] : 311 aggregator.aggregationSources) 312 { 313 if (existingSource.url == *url) 314 { 315 messages::resourceAlreadyExists(asyncResp->res, "AggregationSource", 316 "HostName", url->buffer()); 317 return; 318 } 319 } 320 321 // Validate username and password 322 if (!validateCredentialField(username, "UserName", asyncResp->res)) 323 { 324 return; 325 } 326 if (!validateCredentialField(password, "Password", asyncResp->res)) 327 { 328 return; 329 } 330 331 std::string prefix = bmcweb::getRandomIdOfLength(8); 332 aggregator.aggregationSources.emplace( 333 prefix, 334 AggregationSource{*url, username.value_or(""), password.value_or("")}); 335 336 BMCWEB_LOG_DEBUG("Emplaced {} with url {}", prefix, url->buffer()); 337 asyncResp->res.addHeader( 338 boost::beast::http::field::location, 339 boost::urls::format("/redfish/v1/AggregationSources/{}", prefix) 340 .buffer()); 341 messages::created(asyncResp->res); 342 } 343 344 inline void handleAggregationSourcePatch( 345 App& app, const crow::Request& req, 346 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 347 const std::string& aggregationSourceId) 348 { 349 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 350 { 351 return; 352 } 353 354 std::optional<std::string> username; 355 std::optional<std::string> password; 356 357 if (!json_util::readJsonPatch(req, asyncResp->res, "UserName", username, 358 "Password", password)) 359 { 360 return; 361 } 362 363 // Validate username and password 364 if (!validateCredentialField(username, "UserName", asyncResp->res)) 365 { 366 return; 367 } 368 if (!validateCredentialField(password, "Password", asyncResp->res)) 369 { 370 return; 371 } 372 373 // Check if the aggregation source exists in writable sources 374 auto& aggregator = RedfishAggregator::getInstance(); 375 auto it = aggregator.aggregationSources.find(aggregationSourceId); 376 if (it != aggregator.aggregationSources.end()) 377 { 378 // Update only the fields that were provided 379 if (username.has_value()) 380 { 381 it->second.username = *username; 382 } 383 if (password.has_value()) 384 { 385 it->second.password = *password; 386 } 387 388 messages::success(asyncResp->res); 389 return; 390 } 391 392 // Not in writable sources, query D-Bus to check if it exists in 393 // Entity Manager sources 394 RedfishAggregator::getInstance().getSatelliteConfigs( 395 [asyncResp, aggregationSourceId]( 396 const boost::system::error_code& ec, 397 const std::unordered_map<std::string, boost::urls::url>& 398 satelliteInfo) { 399 // Something went wrong while querying dbus 400 if (ec) 401 { 402 messages::internalError(asyncResp->res); 403 return; 404 } 405 406 // Check if it exists in Entity Manager sources 407 if (satelliteInfo.contains(aggregationSourceId)) 408 { 409 // Source exists but is read-only (from Entity Manager) 410 messages::propertyNotWritable(asyncResp->res, "UserName"); 411 return; 412 } 413 414 // Doesn't exist anywhere 415 messages::resourceNotFound(asyncResp->res, "AggregationSource", 416 aggregationSourceId); 417 }); 418 } 419 420 inline void handleAggregationSourceDelete( 421 App& app, const crow::Request& req, 422 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 423 const std::string& aggregationSourceId) 424 { 425 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 426 { 427 return; 428 } 429 asyncResp->res.addHeader( 430 boost::beast::http::field::link, 431 "</redfish/v1/JsonSchemas/AggregationService/AggregationSource.json>; rel=describedby"); 432 433 size_t deleted = RedfishAggregator::getInstance().aggregationSources.erase( 434 aggregationSourceId); 435 if (deleted == 0) 436 { 437 messages::resourceNotFound(asyncResp->res, "AggregationSource", 438 aggregationSourceId); 439 return; 440 } 441 442 messages::success(asyncResp->res); 443 } 444 445 inline void requestRoutesAggregationSource(App& app) 446 { 447 BMCWEB_ROUTE(app, 448 "/redfish/v1/AggregationService/AggregationSources/<str>/") 449 .privileges(redfish::privileges::getAggregationSource) 450 .methods(boost::beast::http::verb::get)( 451 std::bind_front(handleAggregationSourceGet, std::ref(app))); 452 453 BMCWEB_ROUTE(app, 454 "/redfish/v1/AggregationService/AggregationSources/<str>/") 455 .privileges(redfish::privileges::patchAggregationSource) 456 .methods(boost::beast::http::verb::patch)( 457 std::bind_front(handleAggregationSourcePatch, std::ref(app))); 458 459 BMCWEB_ROUTE(app, 460 "/redfish/v1/AggregationService/AggregationSources/<str>/") 461 .privileges(redfish::privileges::deleteAggregationSource) 462 .methods(boost::beast::http::verb::delete_)( 463 std::bind_front(handleAggregationSourceDelete, std::ref(app))); 464 465 BMCWEB_ROUTE(app, 466 "/redfish/v1/AggregationService/AggregationSources/<str>/") 467 .privileges(redfish::privileges::headAggregationSource) 468 .methods(boost::beast::http::verb::head)( 469 std::bind_front(handleAggregationSourceHead, std::ref(app))); 470 471 BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/") 472 .privileges(redfish::privileges::postAggregationSourceCollection) 473 .methods(boost::beast::http::verb::post)(std::bind_front( 474 handleAggregationSourceCollectionPost, std::ref(app))); 475 } 476 477 } // namespace redfish 478