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