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 #include "node.hpp"
18 
19 #include <error_messages.hpp>
20 #include <openbmc_dbus_rest.hpp>
21 #include <utils/json_utils.hpp>
22 
23 namespace redfish
24 {
25 
26 using ManagedObjectType = std::vector<std::pair<
27     sdbusplus::message::object_path,
28     boost::container::flat_map<
29         std::string,
30         boost::container::flat_map<
31             std::string, sdbusplus::message::variant<bool, std::string>>>>>;
32 
33 inline std::string getPrivilegeFromRoleId(boost::beast::string_view role)
34 {
35     if (role == "priv-admin")
36     {
37         return "Administrator";
38     }
39     else if (role == "priv-callback")
40     {
41         return "Callback";
42     }
43     else if (role == "priv-user")
44     {
45         return "User";
46     }
47     else if (role == "priv-operator")
48     {
49         return "Operator";
50     }
51     return "";
52 }
53 inline std::string getRoleIdFromPrivilege(boost::beast::string_view role)
54 {
55     if (role == "Administrator")
56     {
57         return "priv-admin";
58     }
59     else if (role == "Callback")
60     {
61         return "priv-callback";
62     }
63     else if (role == "User")
64     {
65         return "priv-user";
66     }
67     else if (role == "Operator")
68     {
69         return "priv-operator";
70     }
71     return "";
72 }
73 
74 class AccountService : public Node
75 {
76   public:
77     AccountService(CrowApp& app) : Node(app, "/redfish/v1/AccountService/")
78     {
79         entityPrivileges = {
80             {boost::beast::http::verb::get,
81              {{"ConfigureUsers"}, {"ConfigureManager"}}},
82             {boost::beast::http::verb::head, {{"Login"}}},
83             {boost::beast::http::verb::patch, {{"ConfigureUsers"}}},
84             {boost::beast::http::verb::put, {{"ConfigureUsers"}}},
85             {boost::beast::http::verb::delete_, {{"ConfigureUsers"}}},
86             {boost::beast::http::verb::post, {{"ConfigureUsers"}}}};
87     }
88 
89   private:
90     void doGet(crow::Response& res, const crow::Request& req,
91                const std::vector<std::string>& params) override
92     {
93         auto asyncResp = std::make_shared<AsyncResp>(res);
94         res.jsonValue = {
95             {"@odata.context", "/redfish/v1/"
96                                "$metadata#AccountService.AccountService"},
97             {"@odata.id", "/redfish/v1/AccountService"},
98             {"@odata.type", "#AccountService."
99                             "v1_1_0.AccountService"},
100             {"Id", "AccountService"},
101             {"Name", "Account Service"},
102             {"Description", "Account Service"},
103             {"ServiceEnabled", true},
104             {"MaxPasswordLength", 31},
105             {"Accounts",
106              {{"@odata.id", "/redfish/v1/AccountService/Accounts"}}},
107             {"Roles", {{"@odata.id", "/redfish/v1/AccountService/Roles"}}}};
108 
109         crow::connections::systemBus->async_method_call(
110             [asyncResp](
111                 const boost::system::error_code ec,
112                 const std::vector<std::pair<
113                     std::string,
114                     sdbusplus::message::variant<uint32_t, uint16_t, uint8_t>>>&
115                     propertiesList) {
116                 if (ec)
117                 {
118                     messages::internalError(asyncResp->res);
119                     return;
120                 }
121                 BMCWEB_LOG_DEBUG << "Got " << propertiesList.size()
122                                  << "properties for AccountService";
123                 for (const std::pair<std::string,
124                                      sdbusplus::message::variant<
125                                          uint32_t, uint16_t, uint8_t>>&
126                          property : propertiesList)
127                 {
128                     if (property.first == "MinPasswordLength")
129                     {
130                         const uint8_t* value =
131                             sdbusplus::message::variant_ns::get_if<uint8_t>(
132                                 &property.second);
133                         if (value != nullptr)
134                         {
135                             asyncResp->res.jsonValue["MinPasswordLength"] =
136                                 *value;
137                         }
138                     }
139                     if (property.first == "AccountUnlockTimeout")
140                     {
141                         const uint32_t* value =
142                             sdbusplus::message::variant_ns::get_if<uint32_t>(
143                                 &property.second);
144                         if (value != nullptr)
145                         {
146                             asyncResp->res.jsonValue["AccountLockoutDuration"] =
147                                 *value;
148                         }
149                     }
150                     if (property.first == "MaxLoginAttemptBeforeLockout")
151                     {
152                         const uint16_t* value =
153                             sdbusplus::message::variant_ns::get_if<uint16_t>(
154                                 &property.second);
155                         if (value != nullptr)
156                         {
157                             asyncResp->res
158                                 .jsonValue["AccountLockoutThreshold"] = *value;
159                         }
160                     }
161                 }
162             },
163             "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user",
164             "org.freedesktop.DBus.Properties", "GetAll",
165             "xyz.openbmc_project.User.AccountPolicy");
166     }
167     void doPatch(crow::Response& res, const crow::Request& req,
168                  const std::vector<std::string>& params) override
169     {
170         auto asyncResp = std::make_shared<AsyncResp>(res);
171 
172         std::optional<uint32_t> unlockTimeout;
173         std::optional<uint16_t> lockoutThreshold;
174         if (!json_util::readJson(req, res, "AccountLockoutDuration",
175                                  unlockTimeout, "AccountLockoutThreshold",
176                                  lockoutThreshold))
177         {
178             return;
179         }
180         if (unlockTimeout)
181         {
182             crow::connections::systemBus->async_method_call(
183                 [asyncResp](const boost::system::error_code ec) {
184                     if (ec)
185                     {
186                         messages::internalError(asyncResp->res);
187                         return;
188                     }
189                 },
190                 "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user",
191                 "org.freedesktop.DBus.Properties", "Set",
192                 "xyz.openbmc_project.User.AccountPolicy",
193                 "AccountUnlockTimeout",
194                 sdbusplus::message::variant<uint32_t>(*unlockTimeout));
195         }
196         if (lockoutThreshold)
197         {
198             crow::connections::systemBus->async_method_call(
199                 [asyncResp](const boost::system::error_code ec) {
200                     if (ec)
201                     {
202                         messages::internalError(asyncResp->res);
203                         return;
204                     }
205                 },
206                 "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user",
207                 "org.freedesktop.DBus.Properties", "Set",
208                 "xyz.openbmc_project.User.AccountPolicy",
209                 "MaxLoginAttemptBeforeLockout",
210                 sdbusplus::message::variant<uint16_t>(*lockoutThreshold));
211         }
212     }
213 };
214 class AccountsCollection : public Node
215 {
216   public:
217     AccountsCollection(CrowApp& app) :
218         Node(app, "/redfish/v1/AccountService/Accounts/")
219     {
220         entityPrivileges = {
221             {boost::beast::http::verb::get,
222              {{"ConfigureUsers"}, {"ConfigureManager"}}},
223             {boost::beast::http::verb::head, {{"Login"}}},
224             {boost::beast::http::verb::patch, {{"ConfigureUsers"}}},
225             {boost::beast::http::verb::put, {{"ConfigureUsers"}}},
226             {boost::beast::http::verb::delete_, {{"ConfigureUsers"}}},
227             {boost::beast::http::verb::post, {{"ConfigureUsers"}}}};
228     }
229 
230   private:
231     void doGet(crow::Response& res, const crow::Request& req,
232                const std::vector<std::string>& params) override
233     {
234         auto asyncResp = std::make_shared<AsyncResp>(res);
235         res.jsonValue = {{"@odata.context",
236                           "/redfish/v1/"
237                           "$metadata#ManagerAccountCollection."
238                           "ManagerAccountCollection"},
239                          {"@odata.id", "/redfish/v1/AccountService/Accounts"},
240                          {"@odata.type", "#ManagerAccountCollection."
241                                          "ManagerAccountCollection"},
242                          {"Name", "Accounts Collection"},
243                          {"Description", "BMC User Accounts"}};
244 
245         crow::connections::systemBus->async_method_call(
246             [asyncResp](const boost::system::error_code ec,
247                         const ManagedObjectType& users) {
248                 if (ec)
249                 {
250                     messages::internalError(asyncResp->res);
251                     return;
252                 }
253 
254                 nlohmann::json& memberArray =
255                     asyncResp->res.jsonValue["Members"];
256                 memberArray = nlohmann::json::array();
257 
258                 asyncResp->res.jsonValue["Members@odata.count"] = users.size();
259                 for (auto& user : users)
260                 {
261                     const std::string& path =
262                         static_cast<const std::string&>(user.first);
263                     std::size_t lastIndex = path.rfind("/");
264                     if (lastIndex == std::string::npos)
265                     {
266                         lastIndex = 0;
267                     }
268                     else
269                     {
270                         lastIndex += 1;
271                     }
272                     memberArray.push_back(
273                         {{"@odata.id", "/redfish/v1/AccountService/Accounts/" +
274                                            path.substr(lastIndex)}});
275                 }
276             },
277             "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user",
278             "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
279     }
280     void doPost(crow::Response& res, const crow::Request& req,
281                 const std::vector<std::string>& params) override
282     {
283         auto asyncResp = std::make_shared<AsyncResp>(res);
284 
285         std::string username;
286         std::string password;
287         std::optional<std::string> roleId("User");
288         std::optional<bool> enabled = true;
289         if (!json_util::readJson(req, res, "UserName", username, "Password",
290                                  password, "RoleId", roleId, "Enabled",
291                                  enabled))
292         {
293             return;
294         }
295 
296         std::string priv = getRoleIdFromPrivilege(*roleId);
297         if (priv.empty())
298         {
299             messages::propertyValueNotInList(asyncResp->res, *roleId, "RoleId");
300             return;
301         }
302         roleId = priv;
303 
304         crow::connections::systemBus->async_method_call(
305             [asyncResp, username, password{std::move(password)}](
306                 const boost::system::error_code ec) {
307                 if (ec)
308                 {
309                     messages::resourceAlreadyExists(
310                         asyncResp->res, "#ManagerAccount.v1_0_3.ManagerAccount",
311                         "UserName", username);
312                     return;
313                 }
314 
315                 if (!pamUpdatePassword(username, password))
316                 {
317                     // At this point we have a user that's been created, but the
318                     // password set failed.  Something is wrong, so delete the
319                     // user that we've already created
320                     crow::connections::systemBus->async_method_call(
321                         [asyncResp](const boost::system::error_code ec) {
322                             if (ec)
323                             {
324                                 messages::internalError(asyncResp->res);
325                                 return;
326                             }
327 
328                             messages::invalidObject(asyncResp->res, "Password");
329                         },
330                         "xyz.openbmc_project.User.Manager",
331                         "/xyz/openbmc_project/user/" + username,
332                         "xyz.openbmc_project.Object.Delete", "Delete");
333 
334                     BMCWEB_LOG_ERROR << "pamUpdatePassword Failed";
335                     return;
336                 }
337 
338                 messages::created(asyncResp->res);
339                 asyncResp->res.addHeader(
340                     "Location",
341                     "/redfish/v1/AccountService/Accounts/" + username);
342             },
343             "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user",
344             "xyz.openbmc_project.User.Manager", "CreateUser", username,
345             std::array<const char*, 4>{"ipmi", "redfish", "ssh", "web"},
346             *roleId, *enabled);
347     }
348 };
349 
350 template <typename Callback>
351 inline void checkDbusPathExists(const std::string& path, Callback&& callback)
352 {
353     using GetObjectType =
354         std::vector<std::pair<std::string, std::vector<std::string>>>;
355 
356     crow::connections::systemBus->async_method_call(
357         [callback{std::move(callback)}](const boost::system::error_code ec,
358                                         const GetObjectType& object_names) {
359             callback(!ec && object_names.size() != 0);
360         },
361         "xyz.openbmc_project.ObjectMapper",
362         "/xyz/openbmc_project/object_mapper",
363         "xyz.openbmc_project.ObjectMapper", "GetObject", path,
364         std::array<std::string, 0>());
365 }
366 
367 class ManagerAccount : public Node
368 {
369   public:
370     ManagerAccount(CrowApp& app) :
371         Node(app, "/redfish/v1/AccountService/Accounts/<str>/", std::string())
372     {
373         entityPrivileges = {
374             {boost::beast::http::verb::get,
375              {{"ConfigureUsers"}, {"ConfigureManager"}, {"ConfigureSelf"}}},
376             {boost::beast::http::verb::head, {{"Login"}}},
377             {boost::beast::http::verb::patch, {{"ConfigureUsers"}}},
378             {boost::beast::http::verb::put, {{"ConfigureUsers"}}},
379             {boost::beast::http::verb::delete_, {{"ConfigureUsers"}}},
380             {boost::beast::http::verb::post, {{"ConfigureUsers"}}}};
381     }
382 
383   private:
384     void doGet(crow::Response& res, const crow::Request& req,
385                const std::vector<std::string>& params) override
386     {
387         res.jsonValue = {
388             {"@odata.context",
389              "/redfish/v1/$metadata#ManagerAccount.ManagerAccount"},
390             {"@odata.type", "#ManagerAccount.v1_0_3.ManagerAccount"},
391             {"Name", "User Account"},
392             {"Description", "User Account"},
393             {"Password", nullptr},
394             {"RoleId", "Administrator"}};
395 
396         auto asyncResp = std::make_shared<AsyncResp>(res);
397 
398         if (params.size() != 1)
399         {
400             messages::internalError(asyncResp->res);
401             return;
402         }
403 
404         crow::connections::systemBus->async_method_call(
405             [asyncResp, accountName{std::string(params[0])}](
406                 const boost::system::error_code ec,
407                 const ManagedObjectType& users) {
408                 if (ec)
409                 {
410                     messages::internalError(asyncResp->res);
411                     return;
412                 }
413                 auto userIt = users.begin();
414 
415                 for (; userIt != users.end(); userIt++)
416                 {
417                     if (boost::ends_with(userIt->first.str, "/" + accountName))
418                     {
419                         break;
420                     }
421                 }
422                 if (userIt == users.end())
423                 {
424                     messages::resourceNotFound(asyncResp->res, "ManagerAccount",
425                                                accountName);
426                     return;
427                 }
428                 for (const auto& interface : userIt->second)
429                 {
430                     if (interface.first ==
431                         "xyz.openbmc_project.User.Attributes")
432                     {
433                         for (const auto& property : interface.second)
434                         {
435                             if (property.first == "UserEnabled")
436                             {
437                                 const bool* userEnabled =
438                                     sdbusplus::message::variant_ns::get_if<
439                                         bool>(&property.second);
440                                 if (userEnabled == nullptr)
441                                 {
442                                     BMCWEB_LOG_ERROR
443                                         << "UserEnabled wasn't a bool";
444                                     messages::internalError(asyncResp->res);
445                                     return;
446                                 }
447                                 asyncResp->res.jsonValue["Enabled"] =
448                                     *userEnabled;
449                             }
450                             else if (property.first ==
451                                      "UserLockedForFailedAttempt")
452                             {
453                                 const bool* userLocked =
454                                     sdbusplus::message::variant_ns::get_if<
455                                         bool>(&property.second);
456                                 if (userLocked == nullptr)
457                                 {
458                                     BMCWEB_LOG_ERROR << "UserLockedForF"
459                                                         "ailedAttempt "
460                                                         "wasn't a bool";
461                                     messages::internalError(asyncResp->res);
462                                     return;
463                                 }
464                                 asyncResp->res.jsonValue["Locked"] =
465                                     *userLocked;
466                             }
467                             else if (property.first == "UserPrivilege")
468                             {
469                                 const std::string* userRolePtr =
470                                     sdbusplus::message::variant_ns::get_if<
471                                         std::string>(&property.second);
472                                 if (userRolePtr == nullptr)
473                                 {
474                                     BMCWEB_LOG_ERROR
475                                         << "UserPrivilege wasn't a "
476                                            "string";
477                                     messages::internalError(asyncResp->res);
478                                     return;
479                                 }
480                                 std::string priv =
481                                     getPrivilegeFromRoleId(*userRolePtr);
482                                 if (priv.empty())
483                                 {
484                                     BMCWEB_LOG_ERROR << "Invalid user role";
485                                     messages::internalError(asyncResp->res);
486                                     return;
487                                 }
488                                 asyncResp->res.jsonValue["RoleId"] = priv;
489 
490                                 asyncResp->res.jsonValue["Links"]["Role"] = {
491                                     {"@odata.id", "/redfish/v1/AccountService/"
492                                                   "Roles/" +
493                                                       priv}};
494                             }
495                         }
496                     }
497                 }
498 
499                 asyncResp->res.jsonValue["@odata.id"] =
500                     "/redfish/v1/AccountService/Accounts/" + accountName;
501                 asyncResp->res.jsonValue["Id"] = accountName;
502                 asyncResp->res.jsonValue["UserName"] = accountName;
503             },
504             "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user",
505             "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
506     }
507 
508     void doPatch(crow::Response& res, const crow::Request& req,
509                  const std::vector<std::string>& params) override
510     {
511         auto asyncResp = std::make_shared<AsyncResp>(res);
512         if (params.size() != 1)
513         {
514             messages::internalError(asyncResp->res);
515             return;
516         }
517 
518         std::optional<std::string> newUserName;
519         std::optional<std::string> password;
520         std::optional<bool> enabled;
521         std::optional<std::string> roleId;
522         if (!json_util::readJson(req, res, "UserName", newUserName, "Password",
523                                  password, "RoleId", roleId, "Enabled",
524                                  enabled))
525         {
526             return;
527         }
528 
529         const std::string& username = params[0];
530 
531         if (!newUserName)
532         {
533             // If the username isn't being updated, we can update the properties
534             // directly
535             updateUserProperties(asyncResp, username, password, enabled,
536                                  roleId);
537             return;
538         }
539         else
540         {
541             crow::connections::systemBus->async_method_call(
542                 [this, asyncResp, username, password(std::move(password)),
543                  roleId(std::move(roleId)), enabled(std::move(enabled)),
544                  newUser{std::string(*newUserName)}](
545                     const boost::system::error_code ec) {
546                     if (ec)
547                     {
548                         BMCWEB_LOG_ERROR << "D-Bus responses error: " << ec;
549                         messages::resourceNotFound(
550                             asyncResp->res,
551                             "#ManagerAccount.v1_0_3.ManagerAccount", username);
552                         return;
553                     }
554 
555                     updateUserProperties(asyncResp, newUser, password, enabled,
556                                          roleId);
557                 },
558                 "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user",
559                 "xyz.openbmc_project.User.Manager", "RenameUser", username,
560                 *newUserName);
561         }
562     }
563 
564     void updateUserProperties(std::shared_ptr<AsyncResp> asyncResp,
565                               const std::string& username,
566                               std::optional<std::string> password,
567                               std::optional<bool> enabled,
568                               std::optional<std::string> roleId)
569     {
570         if (password)
571         {
572             if (!pamUpdatePassword(username, *password))
573             {
574                 BMCWEB_LOG_ERROR << "pamUpdatePassword Failed";
575                 messages::internalError(asyncResp->res);
576                 return;
577             }
578         }
579 
580         if (enabled)
581         {
582             crow::connections::systemBus->async_method_call(
583                 [asyncResp](const boost::system::error_code ec) {
584                     if (ec)
585                     {
586                         BMCWEB_LOG_ERROR << "D-Bus responses error: " << ec;
587                         messages::internalError(asyncResp->res);
588                         return;
589                     }
590                     messages::success(asyncResp->res);
591                     return;
592                 },
593                 "xyz.openbmc_project.User.Manager",
594                 "/xyz/openbmc_project/user/" + username,
595                 "org.freedesktop.DBus.Properties", "Set",
596                 "xyz.openbmc_project.User.Attributes", "UserEnabled",
597                 sdbusplus::message::variant<bool>{*enabled});
598         }
599 
600         if (roleId)
601         {
602             std::string priv = getRoleIdFromPrivilege(*roleId);
603             if (priv.empty())
604             {
605                 messages::propertyValueNotInList(asyncResp->res, *roleId,
606                                                  "RoleId");
607                 return;
608             }
609 
610             crow::connections::systemBus->async_method_call(
611                 [asyncResp](const boost::system::error_code ec) {
612                     if (ec)
613                     {
614                         BMCWEB_LOG_ERROR << "D-Bus responses error: " << ec;
615                         messages::internalError(asyncResp->res);
616                         return;
617                     }
618                     messages::success(asyncResp->res);
619                 },
620                 "xyz.openbmc_project.User.Manager",
621                 "/xyz/openbmc_project/user/" + username,
622                 "org.freedesktop.DBus.Properties", "Set",
623                 "xyz.openbmc_project.User.Attributes", "UserPrivilege",
624                 sdbusplus::message::variant<std::string>{priv});
625         }
626     }
627 
628     void doDelete(crow::Response& res, const crow::Request& req,
629                   const std::vector<std::string>& params) override
630     {
631         auto asyncResp = std::make_shared<AsyncResp>(res);
632 
633         if (params.size() != 1)
634         {
635             messages::internalError(asyncResp->res);
636             return;
637         }
638 
639         const std::string userPath = "/xyz/openbmc_project/user/" + params[0];
640 
641         crow::connections::systemBus->async_method_call(
642             [asyncResp, username{std::move(params[0])}](
643                 const boost::system::error_code ec) {
644                 if (ec)
645                 {
646                     messages::resourceNotFound(
647                         asyncResp->res, "#ManagerAccount.v1_0_3.ManagerAccount",
648                         username);
649                     return;
650                 }
651 
652                 messages::accountRemoved(asyncResp->res);
653             },
654             "xyz.openbmc_project.User.Manager", userPath,
655             "xyz.openbmc_project.Object.Delete", "Delete");
656     }
657 };
658 
659 } // namespace redfish
660