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