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