xref: /openbmc/bmcweb/features/redfish/lib/network_protocol.hpp (revision 55dabd74e4d96b78aa1804003062bec66cb5baa7)
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 }
277 
278 inline void handleNTPProtocolEnabled(
279     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, bool ntpEnabled)
280 {
281     bool interactive = false;
282     auto callback = [asyncResp](const boost::system::error_code& ec) {
283         afterSetNTP(asyncResp, ec);
284     };
285     dbus::utility::async_method_call(
286         asyncResp, std::move(callback), "org.freedesktop.timedate1",
287         "/org/freedesktop/timedate1", "org.freedesktop.timedate1", "SetNTP",
288         ntpEnabled, interactive);
289 }
290 
291 // Redfish states that ip addresses can be
292 // string, to set a value
293 // null, to delete the value
294 // object_t, empty json object, to ignore the value
295 using IpAddress =
296     std::variant<std::string, nlohmann::json::object_t, std::nullptr_t>;
297 
298 inline void handleNTPServersPatch(
299     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
300     const std::vector<IpAddress>& ntpServerObjects,
301     std::vector<std::string> currentNtpServers)
302 {
303     std::vector<std::string>::iterator currentNtpServer =
304         currentNtpServers.begin();
305     for (size_t index = 0; index < ntpServerObjects.size(); index++)
306     {
307         const IpAddress& ntpServer = ntpServerObjects[index];
308         if (std::holds_alternative<std::nullptr_t>(ntpServer))
309         {
310             // Can't delete an item that doesn't exist
311             if (currentNtpServer == currentNtpServers.end())
312             {
313                 messages::propertyValueNotInList(
314                     asyncResp->res, "null",
315                     "NTP/NTPServers/" + std::to_string(index));
316 
317                 return;
318             }
319             currentNtpServer = currentNtpServers.erase(currentNtpServer);
320             continue;
321         }
322         const nlohmann::json::object_t* ntpServerObject =
323             std::get_if<nlohmann::json::object_t>(&ntpServer);
324         if (ntpServerObject != nullptr)
325         {
326             if (!ntpServerObject->empty())
327             {
328                 messages::propertyValueNotInList(
329                     asyncResp->res, *ntpServerObject,
330                     "NTP/NTPServers/" + std::to_string(index));
331                 return;
332             }
333             // Can't retain an item that doesn't exist
334             if (currentNtpServer == currentNtpServers.end())
335             {
336                 messages::propertyValueOutOfRange(
337                     asyncResp->res, *ntpServerObject,
338                     "NTP/NTPServers/" + std::to_string(index));
339 
340                 return;
341             }
342             // empty objects should leave the NtpServer unmodified
343             currentNtpServer++;
344             continue;
345         }
346 
347         const std::string* ntpServerStr = std::get_if<std::string>(&ntpServer);
348         if (ntpServerStr == nullptr)
349         {
350             messages::internalError(asyncResp->res);
351             return;
352         }
353         if (currentNtpServer == currentNtpServers.end())
354         {
355             // if we're at the end of the list, append to the end
356             currentNtpServers.push_back(*ntpServerStr);
357             currentNtpServer = currentNtpServers.end();
358             continue;
359         }
360         *currentNtpServer = *ntpServerStr;
361         currentNtpServer++;
362     }
363 
364     // Any remaining array elements should be removed
365     currentNtpServers.erase(currentNtpServer, currentNtpServers.end());
366 
367     constexpr std::array<std::string_view, 1> ethInterfaces = {
368         "xyz.openbmc_project.Network.EthernetInterface"};
369     dbus::utility::getSubTree(
370         "/xyz/openbmc_project", 0, ethInterfaces,
371         [asyncResp, currentNtpServers](
372             const boost::system::error_code& ec,
373             const dbus::utility::MapperGetSubTreeResponse& subtree) {
374             if (ec)
375             {
376                 BMCWEB_LOG_WARNING("D-Bus error: {}, {}", ec, ec.message());
377                 messages::internalError(asyncResp->res);
378                 return;
379             }
380 
381             for (const auto& [objectPath, serviceMap] : subtree)
382             {
383                 for (const auto& [service, interfaces] : serviceMap)
384                 {
385                     for (const auto& interface : interfaces)
386                     {
387                         if (interface !=
388                             "xyz.openbmc_project.Network.EthernetInterface")
389                         {
390                             continue;
391                         }
392 
393                         setDbusProperty(asyncResp, "NTP/NTPServers/", service,
394                                         objectPath, interface,
395                                         "StaticNTPServers", currentNtpServers);
396                     }
397                 }
398             }
399         });
400 }
401 
402 inline void handleProtocolEnabled(
403     const bool protocolEnabled,
404     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
405     const std::string& netBasePath)
406 {
407     constexpr std::array<std::string_view, 1> interfaces = {
408         "xyz.openbmc_project.Control.Service.Attributes"};
409     dbus::utility::getSubTree(
410         "/xyz/openbmc_project/control/service", 0, interfaces,
411         [protocolEnabled, asyncResp,
412          netBasePath](const boost::system::error_code& ec,
413                       const dbus::utility::MapperGetSubTreeResponse& subtree) {
414             if (ec)
415             {
416                 messages::internalError(asyncResp->res);
417                 return;
418             }
419 
420             for (const auto& entry : subtree)
421             {
422                 if (entry.first.starts_with(netBasePath))
423                 {
424                     setDbusProperty(
425                         asyncResp, "IPMI/ProtocolEnabled",
426                         entry.second.begin()->first, entry.first,
427                         "xyz.openbmc_project.Control.Service.Attributes",
428                         "Running", protocolEnabled);
429                     setDbusProperty(
430                         asyncResp, "IPMI/ProtocolEnabled",
431                         entry.second.begin()->first, entry.first,
432                         "xyz.openbmc_project.Control.Service.Attributes",
433                         "Enabled", protocolEnabled);
434                 }
435             }
436         });
437 }
438 
439 inline void getNTPProtocolEnabled(
440     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
441 {
442     dbus::utility::getProperty<bool>(
443         "org.freedesktop.timedate1", "/org/freedesktop/timedate1",
444         "org.freedesktop.timedate1", "NTP",
445         [asyncResp](const boost::system::error_code& ec, bool enabled) {
446             if (ec)
447             {
448                 BMCWEB_LOG_WARNING(
449                     "Failed to get NTP status, assuming not supported");
450                 return;
451             }
452 
453             asyncResp->res.jsonValue["NTP"]["ProtocolEnabled"] = enabled;
454         });
455 }
456 
457 inline std::string encodeServiceObjectPath(std::string_view serviceName)
458 {
459     sdbusplus::message::object_path objPath(
460         "/xyz/openbmc_project/control/service");
461     objPath /= serviceName;
462     return objPath.str;
463 }
464 
465 inline void handleBmcNetworkProtocolHead(
466     crow::App& app, const crow::Request& req,
467     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
468 {
469     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
470     {
471         return;
472     }
473     asyncResp->res.addHeader(
474         boost::beast::http::field::link,
475         "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby");
476 }
477 
478 inline void handleManagersNetworkProtocolPatch(
479     App& app, const crow::Request& req,
480     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
481     const std::string& managerId)
482 {
483     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
484     {
485         return;
486     }
487 
488     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
489     {
490         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
491         return;
492     }
493 
494     std::optional<std::string> newHostName;
495 
496     std::optional<std::vector<IpAddress>> ntpServerObjects;
497     std::optional<bool> ntpEnabled;
498     std::optional<bool> ipmiEnabled;
499     std::optional<bool> sshEnabled;
500 
501     if (!json_util::readJsonPatch(
502             req, asyncResp->res,                 //
503             "HostName", newHostName,             //
504             "NTP/NTPServers", ntpServerObjects,  //
505             "NTP/ProtocolEnabled", ntpEnabled,   //
506             "IPMI/ProtocolEnabled", ipmiEnabled, //
507             "SSH/ProtocolEnabled", sshEnabled    //
508             ))
509     {
510         return;
511     }
512 
513     asyncResp->res.result(boost::beast::http::status::no_content);
514     if (newHostName)
515     {
516         messages::propertyNotWritable(asyncResp->res, "HostName");
517         return;
518     }
519 
520     if (ntpEnabled)
521     {
522         handleNTPProtocolEnabled(asyncResp, *ntpEnabled);
523     }
524     if (ntpServerObjects)
525     {
526         getEthernetIfaceData(
527             [asyncResp, ntpServerObjects](
528                 const bool success, std::vector<std::string>& currentNtpServers,
529                 const std::vector<std::string>& /*dynamicNtpServers*/,
530                 const std::vector<std::string>& /*domainNames*/) {
531                 if (!success)
532                 {
533                     messages::internalError(asyncResp->res);
534                     return;
535                 }
536                 handleNTPServersPatch(asyncResp, *ntpServerObjects,
537                                       std::move(currentNtpServers));
538             });
539     }
540 
541     if (ipmiEnabled)
542     {
543         handleProtocolEnabled(
544             *ipmiEnabled, asyncResp,
545             encodeServiceObjectPath(std::string(ipmiServiceName) + '@'));
546     }
547 
548     if (sshEnabled)
549     {
550         handleProtocolEnabled(*sshEnabled, asyncResp,
551                               encodeServiceObjectPath(sshServiceName));
552     }
553 }
554 
555 inline void handleManagersNetworkProtocolHead(
556     App& app, const crow::Request& req,
557     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
558     const std::string& managerId)
559 {
560     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
561     {
562         return;
563     }
564     asyncResp->res.addHeader(
565         boost::beast::http::field::link,
566         "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby");
567     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
568     {
569         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
570         return;
571     }
572 }
573 
574 inline void handleManagersNetworkProtocolGet(
575     App& app, const crow::Request& req,
576     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
577     const std::string& managerId)
578 {
579     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
580     {
581         return;
582     }
583     asyncResp->res.addHeader(
584         boost::beast::http::field::link,
585         "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby");
586     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
587     {
588         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
589         return;
590     }
591 
592     getNetworkData(asyncResp, req);
593 }
594 
595 inline void requestRoutesNetworkProtocol(App& app)
596 {
597     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/NetworkProtocol/")
598         .privileges(redfish::privileges::patchManagerNetworkProtocol)
599         .methods(boost::beast::http::verb::patch)(
600             std::bind_front(handleManagersNetworkProtocolPatch, std::ref(app)));
601 
602     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/NetworkProtocol/")
603         .privileges(redfish::privileges::headManagerNetworkProtocol)
604         .methods(boost::beast::http::verb::head)(
605             std::bind_front(handleManagersNetworkProtocolHead, std::ref(app)));
606 
607     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/NetworkProtocol/")
608         .privileges(redfish::privileges::getManagerNetworkProtocol)
609         .methods(boost::beast::http::verb::get)(
610             std::bind_front(handleManagersNetworkProtocolGet, std::ref(app)));
611 }
612 
613 } // namespace redfish
614