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 "openbmc_dbus_rest.hpp"
22 #include "query.hpp"
23 #include "redfish_util.hpp"
24 #include "registries/privilege_registry.hpp"
25 #include "utils/json_utils.hpp"
26 #include "utils/stl_utils.hpp"
27 
28 #include <boost/system/error_code.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     crow::connections::systemBus->async_method_call(
102         [callback{std::forward<CallbackFunc>(callback)}](
103             const boost::system::error_code& errorCode,
104             const dbus::utility::ManagedObjectType& dbusData) {
105         std::vector<std::string> ntpServers;
106         std::vector<std::string> domainNames;
107 
108         if (errorCode)
109         {
110             callback(false, ntpServers, domainNames);
111             return;
112         }
113 
114         extractNTPServersAndDomainNamesData(dbusData, ntpServers, domainNames);
115 
116         callback(true, ntpServers, domainNames);
117         },
118         "xyz.openbmc_project.Network", "/xyz/openbmc_project/network",
119         "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
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         "/redfish/v1/Managers/bmc/NetworkProtocol";
170     asyncResp->res.jsonValue["Id"] = "NetworkProtocol";
171     asyncResp->res.jsonValue["Name"] = "Manager Network Protocol";
172     asyncResp->res.jsonValue["Description"] = "Manager Network Service";
173     asyncResp->res.jsonValue["Status"]["Health"] = "OK";
174     asyncResp->res.jsonValue["Status"]["HealthRollup"] = "OK";
175     asyncResp->res.jsonValue["Status"]["State"] = "Enabled";
176 
177     // HTTP is Mandatory attribute as per OCP Baseline Profile - v1.0.0,
178     // but from security perspective it is not recommended to use.
179     // Hence using protocolEnabled as false to make it OCP and security-wise
180     // compliant
181     asyncResp->res.jsonValue["HTTP"]["Port"] = nullptr;
182     asyncResp->res.jsonValue["HTTP"]["ProtocolEnabled"] = false;
183 
184     // The ProtocolEnabled of the following protocols is determined by
185     // inspecting the state of associated systemd sockets. If these protocols
186     // have been disabled, then the systemd socket unit files will not be found
187     // and the protocols will not be returned in this Redfish query. Set some
188     // defaults to ensure something is always returned.
189     for (const auto& nwkProtocol : networkProtocolToDbus)
190     {
191         asyncResp->res.jsonValue[nwkProtocol.first]["Port"] = nullptr;
192         asyncResp->res.jsonValue[nwkProtocol.first]["ProtocolEnabled"] = false;
193     }
194 
195     std::string hostName = getHostName();
196 
197     asyncResp->res.jsonValue["HostName"] = hostName;
198 
199     getNTPProtocolEnabled(asyncResp);
200 
201     getEthernetIfaceData(
202         [hostName, asyncResp](const bool& success,
203                               const std::vector<std::string>& ntpServers,
204                               const std::vector<std::string>& domainNames) {
205         if (!success)
206         {
207             messages::resourceNotFound(asyncResp->res, "ManagerNetworkProtocol",
208                                        "NetworkProtocol");
209             return;
210         }
211         asyncResp->res.jsonValue["NTP"]["NTPServers"] = ntpServers;
212         if (!hostName.empty())
213         {
214             std::string fqdn = hostName;
215             if (!domainNames.empty())
216             {
217                 fqdn += ".";
218                 fqdn += domainNames[0];
219             }
220             asyncResp->res.jsonValue["FQDN"] = std::move(fqdn);
221         }
222     });
223 
224     Privileges effectiveUserPrivileges =
225         redfish::getUserPrivileges(*req.session);
226 
227     // /redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates is
228     // something only ConfigureManager can access then only display when
229     // the user has permissions ConfigureManager
230     if (isOperationAllowedWithPrivileges({{"ConfigureManager"}},
231                                          effectiveUserPrivileges))
232     {
233         asyncResp->res.jsonValue["HTTPS"]["Certificates"]["@odata.id"] =
234             "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates";
235     }
236 
237     getPortStatusAndPath(std::span(networkProtocolToDbus),
238                          std::bind_front(afterNetworkPortRequest, asyncResp));
239 } // namespace redfish
240 
241 inline void handleNTPProtocolEnabled(
242     const bool& ntpEnabled, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
243 {
244     std::string timeSyncMethod;
245     if (ntpEnabled)
246     {
247         timeSyncMethod = "xyz.openbmc_project.Time.Synchronization.Method.NTP";
248     }
249     else
250     {
251         timeSyncMethod =
252             "xyz.openbmc_project.Time.Synchronization.Method.Manual";
253     }
254 
255     crow::connections::systemBus->async_method_call(
256         [asyncResp](const boost::system::error_code& errorCode) {
257         if (errorCode)
258         {
259             messages::internalError(asyncResp->res);
260         }
261         },
262         "xyz.openbmc_project.Settings", "/xyz/openbmc_project/time/sync_method",
263         "org.freedesktop.DBus.Properties", "Set",
264         "xyz.openbmc_project.Time.Synchronization", "TimeSyncMethod",
265         dbus::utility::DbusVariantType{timeSyncMethod});
266 }
267 
268 inline void
269     handleNTPServersPatch(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
270                           const std::vector<nlohmann::json>& ntpServerObjects,
271                           std::vector<std::string> currentNtpServers)
272 {
273     std::vector<std::string>::iterator currentNtpServer =
274         currentNtpServers.begin();
275     for (size_t index = 0; index < ntpServerObjects.size(); index++)
276     {
277         const nlohmann::json& ntpServer = ntpServerObjects[index];
278         if (ntpServer.is_null())
279         {
280             // Can't delete an item that doesn't exist
281             if (currentNtpServer == currentNtpServers.end())
282             {
283                 messages::propertyValueNotInList(asyncResp->res, "null",
284                                                  "NTP/NTPServers/" +
285                                                      std::to_string(index));
286 
287                 return;
288             }
289             currentNtpServer = currentNtpServers.erase(currentNtpServer);
290             continue;
291         }
292         const nlohmann::json::object_t* ntpServerObject =
293             ntpServer.get_ptr<const nlohmann::json::object_t*>();
294         if (ntpServerObject != nullptr)
295         {
296             if (!ntpServerObject->empty())
297             {
298                 messages::propertyValueNotInList(
299                     asyncResp->res,
300                     ntpServer.dump(2, ' ', true,
301                                    nlohmann::json::error_handler_t::replace),
302                     "NTP/NTPServers/" + std::to_string(index));
303                 return;
304             }
305             // Can't retain an item that doesn't exist
306             if (currentNtpServer == currentNtpServers.end())
307             {
308                 messages::propertyValueOutOfRange(
309                     asyncResp->res,
310                     ntpServer.dump(2, ' ', true,
311                                    nlohmann::json::error_handler_t::replace),
312                     "NTP/NTPServers/" + std::to_string(index));
313 
314                 return;
315             }
316             // empty objects should leave the NtpServer unmodified
317             currentNtpServer++;
318             continue;
319         }
320 
321         const std::string* ntpServerStr =
322             ntpServer.get_ptr<const std::string*>();
323         if (ntpServerStr == nullptr)
324         {
325             messages::propertyValueTypeError(
326                 asyncResp->res,
327                 ntpServer.dump(2, ' ', true,
328                                nlohmann::json::error_handler_t::replace),
329                 "NTP/NTPServers/" + std::to_string(index));
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                     crow::connections::systemBus->async_method_call(
373                         [asyncResp](const boost::system::error_code& ec2) {
374                         if (ec2)
375                         {
376                             messages::internalError(asyncResp->res);
377                             return;
378                         }
379                         },
380                         service, objectPath, "org.freedesktop.DBus.Properties",
381                         "Set", interface, "StaticNTPServers",
382                         dbus::utility::DbusVariantType{currentNtpServers});
383                 }
384             }
385         }
386         });
387 }
388 
389 inline void
390     handleProtocolEnabled(const bool protocolEnabled,
391                           const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
392                           const std::string& netBasePath)
393 {
394     constexpr std::array<std::string_view, 1> interfaces = {
395         "xyz.openbmc_project.Control.Service.Attributes"};
396     dbus::utility::getSubTree(
397         "/xyz/openbmc_project/control/service", 0, interfaces,
398         [protocolEnabled, asyncResp,
399          netBasePath](const boost::system::error_code& ec,
400                       const dbus::utility::MapperGetSubTreeResponse& subtree) {
401         if (ec)
402         {
403             messages::internalError(asyncResp->res);
404             return;
405         }
406 
407         for (const auto& entry : subtree)
408         {
409             if (boost::algorithm::starts_with(entry.first, netBasePath))
410             {
411                 crow::connections::systemBus->async_method_call(
412                     [asyncResp](const boost::system::error_code& ec2) {
413                     if (ec2)
414                     {
415                         messages::internalError(asyncResp->res);
416                         return;
417                     }
418                     },
419                     entry.second.begin()->first, entry.first,
420                     "org.freedesktop.DBus.Properties", "Set",
421                     "xyz.openbmc_project.Control.Service.Attributes", "Running",
422                     dbus::utility::DbusVariantType{protocolEnabled});
423 
424                 crow::connections::systemBus->async_method_call(
425                     [asyncResp](const boost::system::error_code& ec2) {
426                     if (ec2)
427                     {
428                         messages::internalError(asyncResp->res);
429                         return;
430                     }
431                     },
432                     entry.second.begin()->first, entry.first,
433                     "org.freedesktop.DBus.Properties", "Set",
434                     "xyz.openbmc_project.Control.Service.Attributes", "Enabled",
435                     dbus::utility::DbusVariantType{protocolEnabled});
436             }
437         }
438         });
439 }
440 
441 inline std::string getHostName()
442 {
443     std::string hostName;
444 
445     std::array<char, HOST_NAME_MAX> hostNameCStr{};
446     if (gethostname(hostNameCStr.data(), hostNameCStr.size()) == 0)
447     {
448         hostName = hostNameCStr.data();
449     }
450     return hostName;
451 }
452 
453 inline void
454     getNTPProtocolEnabled(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
455 {
456     sdbusplus::asio::getProperty<std::string>(
457         *crow::connections::systemBus, "xyz.openbmc_project.Settings",
458         "/xyz/openbmc_project/time/sync_method",
459         "xyz.openbmc_project.Time.Synchronization", "TimeSyncMethod",
460         [asyncResp](const boost::system::error_code& errorCode,
461                     const std::string& timeSyncMethod) {
462         if (errorCode)
463         {
464             return;
465         }
466 
467         if (timeSyncMethod ==
468             "xyz.openbmc_project.Time.Synchronization.Method.NTP")
469         {
470             asyncResp->res.jsonValue["NTP"]["ProtocolEnabled"] = true;
471         }
472         else if (timeSyncMethod == "xyz.openbmc_project.Time.Synchronization."
473                                    "Method.Manual")
474         {
475             asyncResp->res.jsonValue["NTP"]["ProtocolEnabled"] = false;
476         }
477         });
478 }
479 
480 inline std::string encodeServiceObjectPath(std::string_view serviceName)
481 {
482     sdbusplus::message::object_path objPath(
483         "/xyz/openbmc_project/control/service");
484     objPath /= serviceName;
485     return objPath.str;
486 }
487 
488 inline void handleBmcNetworkProtocolHead(
489     crow::App& app, const crow::Request& req,
490     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
491 {
492     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
493     {
494         return;
495     }
496     asyncResp->res.addHeader(
497         boost::beast::http::field::link,
498         "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby");
499 }
500 
501 inline void handleManagersNetworkProtocolPatch(
502     App& app, const crow::Request& req,
503     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
504 {
505     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
506     {
507         return;
508     }
509     std::optional<std::string> newHostName;
510     std::optional<std::vector<nlohmann::json>> ntpServerObjects;
511     std::optional<bool> ntpEnabled;
512     std::optional<bool> ipmiEnabled;
513     std::optional<bool> sshEnabled;
514 
515     // clang-format off
516         if (!json_util::readJsonPatch(
517                 req, asyncResp->res,
518                 "HostName", newHostName,
519                 "NTP/NTPServers", ntpServerObjects,
520                 "NTP/ProtocolEnabled", ntpEnabled,
521                 "IPMI/ProtocolEnabled", ipmiEnabled,
522                 "SSH/ProtocolEnabled", sshEnabled))
523         {
524             return;
525         }
526     // clang-format on
527 
528     asyncResp->res.result(boost::beast::http::status::no_content);
529     if (newHostName)
530     {
531         messages::propertyNotWritable(asyncResp->res, "HostName");
532         return;
533     }
534 
535     if (ntpEnabled)
536     {
537         handleNTPProtocolEnabled(*ntpEnabled, asyncResp);
538     }
539     if (ntpServerObjects)
540     {
541         getEthernetIfaceData(
542             [asyncResp, ntpServerObjects](
543                 const bool success, std::vector<std::string>& currentNtpServers,
544                 const std::vector<std::string>& /*domainNames*/) {
545             if (!success)
546             {
547                 messages::internalError(asyncResp->res);
548                 return;
549             }
550             handleNTPServersPatch(asyncResp, *ntpServerObjects,
551                                   std::move(currentNtpServers));
552         });
553     }
554 
555     if (ipmiEnabled)
556     {
557         handleProtocolEnabled(
558             *ipmiEnabled, asyncResp,
559             encodeServiceObjectPath(std::string(ipmiServiceName) + '@'));
560     }
561 
562     if (sshEnabled)
563     {
564         handleProtocolEnabled(*sshEnabled, asyncResp,
565                               encodeServiceObjectPath(sshServiceName));
566     }
567 }
568 
569 inline void handleManagersNetworkProtocolHead(
570     App& app, const crow::Request& req,
571     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
572 {
573     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
574     {
575         return;
576     }
577     asyncResp->res.addHeader(
578         boost::beast::http::field::link,
579         "</redfish/v1/JsonSchemas/ManagerNetworkProtocol/ManagerNetworkProtocol.json>; rel=describedby");
580 }
581 
582 inline void handleManagersNetworkProtocolGet(
583     App& app, const crow::Request& req,
584     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
585 {
586     handleManagersNetworkProtocolHead(app, req, asyncResp);
587     getNetworkData(asyncResp, req);
588 }
589 
590 inline void requestRoutesNetworkProtocol(App& app)
591 {
592     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/NetworkProtocol/")
593         .privileges(redfish::privileges::patchManagerNetworkProtocol)
594         .methods(boost::beast::http::verb::patch)(
595             std::bind_front(handleManagersNetworkProtocolPatch, std::ref(app)));
596 
597     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/NetworkProtocol/")
598         .privileges(redfish::privileges::headManagerNetworkProtocol)
599         .methods(boost::beast::http::verb::head)(
600             std::bind_front(handleManagersNetworkProtocolHead, std::ref(app)));
601 
602     BMCWEB_ROUTE(app, "/redfish/v1/Managers/bmc/NetworkProtocol/")
603         .privileges(redfish::privileges::getManagerNetworkProtocol)
604         .methods(boost::beast::http::verb::get)(
605             std::bind_front(handleManagersNetworkProtocolGet, std::ref(app)));
606 }
607 
608 } // namespace redfish
609