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