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