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