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