1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation 4 #pragma once 5 6 #include "bmcweb_config.h" 7 8 #include "app.hpp" 9 #include "async_resp.hpp" 10 #include "dbus_utility.hpp" 11 #include "error_messages.hpp" 12 #include "generated/enums/resource.hpp" 13 #include "http_request.hpp" 14 #include "identity.hpp" 15 #include "logging.hpp" 16 #include "privileges.hpp" 17 #include "query.hpp" 18 #include "redfish_util.hpp" 19 #include "registries/privilege_registry.hpp" 20 #include "utils/dbus_utils.hpp" 21 #include "utils/json_utils.hpp" 22 #include "utils/stl_utils.hpp" 23 24 #include <boost/beast/http/field.hpp> 25 #include <boost/beast/http/status.hpp> 26 #include <boost/beast/http/verb.hpp> 27 #include <boost/system/error_code.hpp> 28 #include <boost/url/format.hpp> 29 #include <sdbusplus/message/native_types.hpp> 30 31 #include <array> 32 #include <cstddef> 33 #include <functional> 34 #include <memory> 35 #include <optional> 36 #include <string> 37 #include <string_view> 38 #include <tuple> 39 #include <utility> 40 #include <variant> 41 #include <vector> 42 43 namespace redfish 44 { 45 46 void getNTPProtocolEnabled(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp); 47 48 static constexpr std::string_view sshServiceName = "dropbear"; 49 static constexpr std::string_view httpsServiceName = "bmcweb"; 50 static constexpr std::string_view ipmiServiceName = "phosphor-ipmi-net"; 51 52 // Mapping from Redfish NetworkProtocol key name to backend service that hosts 53 // that protocol. 54 static constexpr std::array<std::pair<std::string_view, std::string_view>, 3> 55 networkProtocolToDbus = {{{"SSH", sshServiceName}, 56 {"HTTPS", httpsServiceName}, 57 {"IPMI", ipmiServiceName}}}; 58 59 inline void extractNTPServersAndDomainNamesData( 60 const dbus::utility::ManagedObjectType& dbusData, 61 std::vector<std::string>& ntpData, std::vector<std::string>& dynamicNtpData, 62 std::vector<std::string>& dnData) 63 { 64 for (const auto& obj : dbusData) 65 { 66 for (const auto& ifacePair : obj.second) 67 { 68 if (ifacePair.first != 69 "xyz.openbmc_project.Network.EthernetInterface") 70 { 71 continue; 72 } 73 74 for (const auto& propertyPair : ifacePair.second) 75 { 76 if (propertyPair.first == "StaticNTPServers") 77 { 78 const std::vector<std::string>* ntpServers = 79 std::get_if<std::vector<std::string>>( 80 &propertyPair.second); 81 if (ntpServers != nullptr) 82 { 83 ntpData.insert(ntpData.end(), ntpServers->begin(), 84 ntpServers->end()); 85 } 86 } 87 else if (propertyPair.first == "NTPServers") 88 { 89 const std::vector<std::string>* dynamicNtpServers = 90 std::get_if<std::vector<std::string>>( 91 &propertyPair.second); 92 if (dynamicNtpServers != nullptr) 93 { 94 dynamicNtpData = *dynamicNtpServers; 95 } 96 } 97 else if (propertyPair.first == "DomainName") 98 { 99 const std::vector<std::string>* domainNames = 100 std::get_if<std::vector<std::string>>( 101 &propertyPair.second); 102 if (domainNames != nullptr) 103 { 104 dnData.insert(dnData.end(), domainNames->begin(), 105 domainNames->end()); 106 } 107 } 108 } 109 } 110 } 111 stl_utils::removeDuplicate(ntpData); 112 stl_utils::removeDuplicate(dnData); 113 } 114 115 template <typename CallbackFunc> 116 void getEthernetIfaceData(CallbackFunc&& callback) 117 { 118 sdbusplus::message::object_path path("/xyz/openbmc_project/network"); 119 dbus::utility::getManagedObjects( 120 "xyz.openbmc_project.Network", path, 121 [callback = std::forward<CallbackFunc>(callback)]( 122 const boost::system::error_code& ec, 123 const dbus::utility::ManagedObjectType& dbusData) { 124 std::vector<std::string> ntpServers; 125 std::vector<std::string> dynamicNtpServers; 126 std::vector<std::string> domainNames; 127 128 if (ec) 129 { 130 callback(false, ntpServers, dynamicNtpServers, domainNames); 131 return; 132 } 133 134 extractNTPServersAndDomainNamesData(dbusData, ntpServers, 135 dynamicNtpServers, domainNames); 136 137 callback(true, ntpServers, dynamicNtpServers, domainNames); 138 }); 139 } 140 141 inline void afterNetworkPortRequest( 142 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 143 const boost::system::error_code& ec, 144 const std::vector<std::tuple<std::string, std::string, bool>>& socketData) 145 { 146 if (ec) 147 { 148 messages::internalError(asyncResp->res); 149 return; 150 } 151 for (const auto& data : socketData) 152 { 153 const std::string& socketPath = get<0>(data); 154 const std::string& protocolName = get<1>(data); 155 bool isProtocolEnabled = get<2>(data); 156 157 asyncResp->res.jsonValue[protocolName]["ProtocolEnabled"] = 158 isProtocolEnabled; 159 asyncResp->res.jsonValue[protocolName]["Port"] = nullptr; 160 getPortNumber(socketPath, [asyncResp, protocolName]( 161 const boost::system::error_code& ec2, 162 int portNumber) { 163 if (ec2) 164 { 165 messages::internalError(asyncResp->res); 166 return; 167 } 168 asyncResp->res.jsonValue[protocolName]["Port"] = portNumber; 169 }); 170 } 171 } 172 173 inline void getNetworkData(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 174 const crow::Request& req) 175 { 176 if (req.session == nullptr) 177 { 178 messages::internalError(asyncResp->res); 179 return; 180 } 181 182 asyncResp->res.addHeader( 183 boost::beast::http::field::link, 184 "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/NetworkProtocol.json>; rel=describedby"); 185 asyncResp->res.jsonValue["@odata.type"] = 186 "#ManagerNetworkProtocol.v1_9_0.ManagerNetworkProtocol"; 187 asyncResp->res.jsonValue["@odata.id"] = 188 boost::urls::format("/redfish/v1/Managers/{}/NetworkProtocol", 189 BMCWEB_REDFISH_MANAGER_URI_NAME); 190 asyncResp->res.jsonValue["Id"] = "NetworkProtocol"; 191 asyncResp->res.jsonValue["Name"] = "Manager Network Protocol"; 192 asyncResp->res.jsonValue["Description"] = "Manager Network Service"; 193 asyncResp->res.jsonValue["Status"]["Health"] = resource::Health::OK; 194 asyncResp->res.jsonValue["Status"]["HealthRollup"] = resource::Health::OK; 195 asyncResp->res.jsonValue["Status"]["State"] = resource::State::Enabled; 196 197 // HTTP is Mandatory attribute as per OCP Baseline Profile - v1.0.0, 198 // but from security perspective it is not recommended to use. 199 // Hence using protocolEnabled as false to make it OCP and security-wise 200 // compliant 201 asyncResp->res.jsonValue["HTTP"]["Port"] = nullptr; 202 asyncResp->res.jsonValue["HTTP"]["ProtocolEnabled"] = false; 203 204 // The ProtocolEnabled of the following protocols is determined by 205 // inspecting the state of associated systemd sockets. If these protocols 206 // have been disabled, then the systemd socket unit files will not be found 207 // and the protocols will not be returned in this Redfish query. Set some 208 // defaults to ensure something is always returned. 209 for (const auto& nwkProtocol : networkProtocolToDbus) 210 { 211 asyncResp->res.jsonValue[nwkProtocol.first]["Port"] = nullptr; 212 asyncResp->res.jsonValue[nwkProtocol.first]["ProtocolEnabled"] = false; 213 } 214 215 std::string hostName = getHostName(); 216 217 asyncResp->res.jsonValue["HostName"] = hostName; 218 219 getNTPProtocolEnabled(asyncResp); 220 221 getEthernetIfaceData([hostName, asyncResp]( 222 const bool& success, 223 const std::vector<std::string>& ntpServers, 224 const std::vector<std::string>& dynamicNtpServers, 225 const std::vector<std::string>& domainNames) { 226 if (!success) 227 { 228 messages::resourceNotFound(asyncResp->res, "ManagerNetworkProtocol", 229 "NetworkProtocol"); 230 return; 231 } 232 asyncResp->res.jsonValue["NTP"]["NTPServers"] = ntpServers; 233 asyncResp->res.jsonValue["NTP"]["NetworkSuppliedServers"] = 234 dynamicNtpServers; 235 if (!hostName.empty()) 236 { 237 std::string fqdn = hostName; 238 if (!domainNames.empty()) 239 { 240 fqdn += "."; 241 fqdn += domainNames[0]; 242 } 243 asyncResp->res.jsonValue["FQDN"] = std::move(fqdn); 244 } 245 }); 246 247 Privileges effectiveUserPrivileges = 248 redfish::getUserPrivileges(*req.session); 249 250 // /redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates is 251 // something only ConfigureManager can access then only display when 252 // the user has permissions ConfigureManager 253 if (isOperationAllowedWithPrivileges({{"ConfigureManager"}}, 254 effectiveUserPrivileges)) 255 { 256 asyncResp->res.jsonValue["HTTPS"]["Certificates"]["@odata.id"] = 257 boost::urls::format( 258 "/redfish/v1/Managers/{}/NetworkProtocol/HTTPS/Certificates", 259 BMCWEB_REDFISH_MANAGER_URI_NAME); 260 } 261 262 getPortStatusAndPath(std::span(networkProtocolToDbus), 263 std::bind_front(afterNetworkPortRequest, asyncResp)); 264 } // namespace redfish 265 266 inline void afterSetNTP(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 267 const boost::system::error_code& ec) 268 { 269 if (ec) 270 { 271 BMCWEB_LOG_DEBUG("Failed to set elapsed time. DBUS response error {}", 272 ec); 273 messages::internalError(asyncResp->res); 274 return; 275 } 276 asyncResp->res.result(boost::beast::http::status::no_content); 277 } 278 279 inline void handleNTPProtocolEnabled( 280 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, bool ntpEnabled) 281 { 282 bool interactive = false; 283 auto callback = [asyncResp](const boost::system::error_code& ec) { 284 afterSetNTP(asyncResp, ec); 285 }; 286 dbus::utility::async_method_call( 287 asyncResp, std::move(callback), "org.freedesktop.timedate1", 288 "/org/freedesktop/timedate1", "org.freedesktop.timedate1", "SetNTP", 289 ntpEnabled, interactive); 290 } 291 292 // Redfish states that ip addresses can be 293 // string, to set a value 294 // null, to delete the value 295 // object_t, empty json object, to ignore the value 296 using IpAddress = 297 std::variant<std::string, nlohmann::json::object_t, std::nullptr_t>; 298 299 inline void handleNTPServersPatch( 300 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 301 const std::vector<IpAddress>& ntpServerObjects, 302 std::vector<std::string> currentNtpServers) 303 { 304 std::vector<std::string>::iterator currentNtpServer = 305 currentNtpServers.begin(); 306 for (size_t index = 0; index < ntpServerObjects.size(); index++) 307 { 308 const IpAddress& ntpServer = ntpServerObjects[index]; 309 if (std::holds_alternative<std::nullptr_t>(ntpServer)) 310 { 311 // Can't delete an item that doesn't exist 312 if (currentNtpServer == currentNtpServers.end()) 313 { 314 messages::propertyValueNotInList( 315 asyncResp->res, "null", 316 "NTP/NTPServers/" + std::to_string(index)); 317 318 return; 319 } 320 currentNtpServer = currentNtpServers.erase(currentNtpServer); 321 continue; 322 } 323 const nlohmann::json::object_t* ntpServerObject = 324 std::get_if<nlohmann::json::object_t>(&ntpServer); 325 if (ntpServerObject != nullptr) 326 { 327 if (!ntpServerObject->empty()) 328 { 329 messages::propertyValueNotInList( 330 asyncResp->res, *ntpServerObject, 331 "NTP/NTPServers/" + std::to_string(index)); 332 return; 333 } 334 // Can't retain an item that doesn't exist 335 if (currentNtpServer == currentNtpServers.end()) 336 { 337 messages::propertyValueOutOfRange( 338 asyncResp->res, *ntpServerObject, 339 "NTP/NTPServers/" + std::to_string(index)); 340 341 return; 342 } 343 // empty objects should leave the NtpServer unmodified 344 currentNtpServer++; 345 continue; 346 } 347 348 const std::string* ntpServerStr = std::get_if<std::string>(&ntpServer); 349 if (ntpServerStr == nullptr) 350 { 351 messages::internalError(asyncResp->res); 352 return; 353 } 354 if (currentNtpServer == currentNtpServers.end()) 355 { 356 // if we're at the end of the list, append to the end 357 currentNtpServers.push_back(*ntpServerStr); 358 currentNtpServer = currentNtpServers.end(); 359 continue; 360 } 361 *currentNtpServer = *ntpServerStr; 362 currentNtpServer++; 363 } 364 365 // Any remaining array elements should be removed 366 currentNtpServers.erase(currentNtpServer, currentNtpServers.end()); 367 368 constexpr std::array<std::string_view, 1> ethInterfaces = { 369 "xyz.openbmc_project.Network.EthernetInterface"}; 370 dbus::utility::getSubTree( 371 "/xyz/openbmc_project", 0, ethInterfaces, 372 [asyncResp, currentNtpServers]( 373 const boost::system::error_code& ec, 374 const dbus::utility::MapperGetSubTreeResponse& subtree) { 375 if (ec) 376 { 377 BMCWEB_LOG_WARNING("D-Bus error: {}, {}", ec, ec.message()); 378 messages::internalError(asyncResp->res); 379 return; 380 } 381 382 for (const auto& [objectPath, serviceMap] : subtree) 383 { 384 for (const auto& [service, interfaces] : serviceMap) 385 { 386 for (const auto& interface : interfaces) 387 { 388 if (interface != 389 "xyz.openbmc_project.Network.EthernetInterface") 390 { 391 continue; 392 } 393 394 setDbusProperty(asyncResp, "NTP/NTPServers/", service, 395 objectPath, interface, 396 "StaticNTPServers", currentNtpServers); 397 } 398 } 399 } 400 }); 401 } 402 403 inline void handleProtocolEnabled( 404 const bool protocolEnabled, 405 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 406 const std::string& netBasePath) 407 { 408 constexpr std::array<std::string_view, 1> interfaces = { 409 "xyz.openbmc_project.Control.Service.Attributes"}; 410 dbus::utility::getSubTree( 411 "/xyz/openbmc_project/control/service", 0, interfaces, 412 [protocolEnabled, asyncResp, 413 netBasePath](const boost::system::error_code& ec, 414 const dbus::utility::MapperGetSubTreeResponse& subtree) { 415 if (ec) 416 { 417 messages::internalError(asyncResp->res); 418 return; 419 } 420 421 for (const auto& entry : subtree) 422 { 423 if (entry.first.starts_with(netBasePath)) 424 { 425 setDbusProperty( 426 asyncResp, "IPMI/ProtocolEnabled", 427 entry.second.begin()->first, entry.first, 428 "xyz.openbmc_project.Control.Service.Attributes", 429 "Running", protocolEnabled); 430 setDbusProperty( 431 asyncResp, "IPMI/ProtocolEnabled", 432 entry.second.begin()->first, entry.first, 433 "xyz.openbmc_project.Control.Service.Attributes", 434 "Enabled", protocolEnabled); 435 } 436 } 437 }); 438 } 439 440 inline void getNTPProtocolEnabled( 441 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 442 { 443 dbus::utility::getProperty<bool>( 444 "org.freedesktop.timedate1", "/org/freedesktop/timedate1", 445 "org.freedesktop.timedate1", "NTP", 446 [asyncResp](const boost::system::error_code& ec, bool enabled) { 447 if (ec) 448 { 449 BMCWEB_LOG_WARNING( 450 "Failed to get NTP status, assuming not supported"); 451 return; 452 } 453 454 asyncResp->res.jsonValue["NTP"]["ProtocolEnabled"] = enabled; 455 }); 456 } 457 458 inline std::string encodeServiceObjectPath(std::string_view serviceName) 459 { 460 sdbusplus::message::object_path objPath( 461 "/xyz/openbmc_project/control/service"); 462 objPath /= serviceName; 463 return objPath.str; 464 } 465 466 inline void handleBmcNetworkProtocolHead( 467 crow::App& app, const crow::Request& req, 468 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 469 { 470 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 471 { 472 return; 473 } 474 asyncResp->res.addHeader( 475 boost::beast::http::field::link, 476 "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby"); 477 } 478 479 inline void handleManagersNetworkProtocolPatch( 480 App& app, const crow::Request& req, 481 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 482 const std::string& managerId) 483 { 484 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 485 { 486 return; 487 } 488 489 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 490 { 491 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 492 return; 493 } 494 495 std::optional<std::string> newHostName; 496 497 std::optional<std::vector<IpAddress>> ntpServerObjects; 498 std::optional<bool> ntpEnabled; 499 std::optional<bool> ipmiEnabled; 500 std::optional<bool> sshEnabled; 501 502 if (!json_util::readJsonPatch( 503 req, asyncResp->res, // 504 "HostName", newHostName, // 505 "NTP/NTPServers", ntpServerObjects, // 506 "NTP/ProtocolEnabled", ntpEnabled, // 507 "IPMI/ProtocolEnabled", ipmiEnabled, // 508 "SSH/ProtocolEnabled", sshEnabled // 509 )) 510 { 511 return; 512 } 513 514 asyncResp->res.result(boost::beast::http::status::no_content); 515 if (newHostName) 516 { 517 messages::propertyNotWritable(asyncResp->res, "HostName"); 518 return; 519 } 520 521 if (ntpEnabled) 522 { 523 handleNTPProtocolEnabled(asyncResp, *ntpEnabled); 524 } 525 if (ntpServerObjects) 526 { 527 getEthernetIfaceData( 528 [asyncResp, ntpServerObjects]( 529 const bool success, std::vector<std::string>& currentNtpServers, 530 const std::vector<std::string>& /*dynamicNtpServers*/, 531 const std::vector<std::string>& /*domainNames*/) { 532 if (!success) 533 { 534 messages::internalError(asyncResp->res); 535 return; 536 } 537 handleNTPServersPatch(asyncResp, *ntpServerObjects, 538 std::move(currentNtpServers)); 539 }); 540 } 541 542 if (ipmiEnabled) 543 { 544 handleProtocolEnabled( 545 *ipmiEnabled, asyncResp, 546 encodeServiceObjectPath(std::string(ipmiServiceName) + '@')); 547 } 548 549 if (sshEnabled) 550 { 551 handleProtocolEnabled(*sshEnabled, asyncResp, 552 encodeServiceObjectPath(sshServiceName)); 553 } 554 } 555 556 inline void handleManagersNetworkProtocolHead( 557 App& app, const crow::Request& req, 558 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 559 const std::string& managerId) 560 { 561 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 562 { 563 return; 564 } 565 asyncResp->res.addHeader( 566 boost::beast::http::field::link, 567 "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby"); 568 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 569 { 570 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 571 return; 572 } 573 } 574 575 inline void handleManagersNetworkProtocolGet( 576 App& app, const crow::Request& req, 577 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 578 const std::string& managerId) 579 { 580 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 581 { 582 return; 583 } 584 asyncResp->res.addHeader( 585 boost::beast::http::field::link, 586 "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby"); 587 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 588 { 589 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 590 return; 591 } 592 593 getNetworkData(asyncResp, req); 594 } 595 596 inline void requestRoutesNetworkProtocol(App& app) 597 { 598 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/NetworkProtocol/") 599 .privileges(redfish::privileges::patchManagerNetworkProtocol) 600 .methods(boost::beast::http::verb::patch)( 601 std::bind_front(handleManagersNetworkProtocolPatch, std::ref(app))); 602 603 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/NetworkProtocol/") 604 .privileges(redfish::privileges::headManagerNetworkProtocol) 605 .methods(boost::beast::http::verb::head)( 606 std::bind_front(handleManagersNetworkProtocolHead, std::ref(app))); 607 608 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/NetworkProtocol/") 609 .privileges(redfish::privileges::getManagerNetworkProtocol) 610 .methods(boost::beast::http::verb::get)( 611 std::bind_front(handleManagersNetworkProtocolGet, std::ref(app))); 612 } 613 614 } // namespace redfish 615