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(std::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(std::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 228 class AccountsCollection : public Node 229 { 230 public: 231 AccountsCollection(CrowApp& app) : 232 Node(app, "/redfish/v1/AccountService/Accounts/") 233 { 234 entityPrivileges = { 235 {boost::beast::http::verb::get, 236 {{"ConfigureUsers"}, {"ConfigureManager"}}}, 237 {boost::beast::http::verb::head, {{"Login"}}}, 238 {boost::beast::http::verb::patch, {{"ConfigureUsers"}}}, 239 {boost::beast::http::verb::put, {{"ConfigureUsers"}}}, 240 {boost::beast::http::verb::delete_, {{"ConfigureUsers"}}}, 241 {boost::beast::http::verb::post, {{"ConfigureUsers"}}}}; 242 } 243 244 private: 245 void doGet(crow::Response& res, const crow::Request& req, 246 const std::vector<std::string>& params) override 247 { 248 auto asyncResp = std::make_shared<AsyncResp>(res); 249 res.jsonValue = {{"@odata.context", 250 "/redfish/v1/" 251 "$metadata#ManagerAccountCollection." 252 "ManagerAccountCollection"}, 253 {"@odata.id", "/redfish/v1/AccountService/Accounts"}, 254 {"@odata.type", "#ManagerAccountCollection." 255 "ManagerAccountCollection"}, 256 {"Name", "Accounts Collection"}, 257 {"Description", "BMC User Accounts"}}; 258 259 crow::connections::systemBus->async_method_call( 260 [asyncResp](const boost::system::error_code ec, 261 const ManagedObjectType& users) { 262 if (ec) 263 { 264 messages::internalError(asyncResp->res); 265 return; 266 } 267 268 nlohmann::json& memberArray = 269 asyncResp->res.jsonValue["Members"]; 270 memberArray = nlohmann::json::array(); 271 272 asyncResp->res.jsonValue["Members@odata.count"] = users.size(); 273 for (auto& user : users) 274 { 275 const std::string& path = 276 static_cast<const std::string&>(user.first); 277 std::size_t lastIndex = path.rfind("/"); 278 if (lastIndex == std::string::npos) 279 { 280 lastIndex = 0; 281 } 282 else 283 { 284 lastIndex += 1; 285 } 286 memberArray.push_back( 287 {{"@odata.id", "/redfish/v1/AccountService/Accounts/" + 288 path.substr(lastIndex)}}); 289 } 290 }, 291 "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user", 292 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); 293 } 294 void doPost(crow::Response& res, const crow::Request& req, 295 const std::vector<std::string>& params) override 296 { 297 auto asyncResp = std::make_shared<AsyncResp>(res); 298 299 std::string username; 300 std::string password; 301 std::optional<std::string> roleId("User"); 302 std::optional<bool> enabled = true; 303 if (!json_util::readJson(req, res, "UserName", username, "Password", 304 password, "RoleId", roleId, "Enabled", 305 enabled)) 306 { 307 return; 308 } 309 310 std::string priv = getRoleIdFromPrivilege(*roleId); 311 if (priv.empty()) 312 { 313 messages::propertyValueNotInList(asyncResp->res, *roleId, "RoleId"); 314 return; 315 } 316 roleId = priv; 317 318 crow::connections::systemBus->async_method_call( 319 [asyncResp, username, password{std::move(password)}]( 320 const boost::system::error_code ec) { 321 if (ec) 322 { 323 messages::resourceAlreadyExists( 324 asyncResp->res, "#ManagerAccount.v1_0_3.ManagerAccount", 325 "UserName", username); 326 return; 327 } 328 329 if (!pamUpdatePassword(username, password)) 330 { 331 // At this point we have a user that's been created, but the 332 // password set failed. Something is wrong, so delete the 333 // user that we've already created 334 crow::connections::systemBus->async_method_call( 335 [asyncResp](const boost::system::error_code ec) { 336 if (ec) 337 { 338 messages::internalError(asyncResp->res); 339 return; 340 } 341 342 messages::invalidObject(asyncResp->res, "Password"); 343 }, 344 "xyz.openbmc_project.User.Manager", 345 "/xyz/openbmc_project/user/" + username, 346 "xyz.openbmc_project.Object.Delete", "Delete"); 347 348 BMCWEB_LOG_ERROR << "pamUpdatePassword Failed"; 349 return; 350 } 351 352 messages::created(asyncResp->res); 353 asyncResp->res.addHeader( 354 "Location", 355 "/redfish/v1/AccountService/Accounts/" + username); 356 }, 357 "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user", 358 "xyz.openbmc_project.User.Manager", "CreateUser", username, 359 std::array<const char*, 4>{"ipmi", "redfish", "ssh", "web"}, 360 *roleId, *enabled); 361 } 362 }; 363 364 template <typename Callback> 365 inline void checkDbusPathExists(const std::string& path, Callback&& callback) 366 { 367 using GetObjectType = 368 std::vector<std::pair<std::string, std::vector<std::string>>>; 369 370 crow::connections::systemBus->async_method_call( 371 [callback{std::move(callback)}](const boost::system::error_code ec, 372 const GetObjectType& object_names) { 373 callback(!ec && object_names.size() != 0); 374 }, 375 "xyz.openbmc_project.ObjectMapper", 376 "/xyz/openbmc_project/object_mapper", 377 "xyz.openbmc_project.ObjectMapper", "GetObject", path, 378 std::array<std::string, 0>()); 379 } 380 381 class ManagerAccount : public Node 382 { 383 public: 384 ManagerAccount(CrowApp& app) : 385 Node(app, "/redfish/v1/AccountService/Accounts/<str>/", std::string()) 386 { 387 entityPrivileges = { 388 {boost::beast::http::verb::get, 389 {{"ConfigureUsers"}, {"ConfigureManager"}, {"ConfigureSelf"}}}, 390 {boost::beast::http::verb::head, {{"Login"}}}, 391 {boost::beast::http::verb::patch, {{"ConfigureUsers"}}}, 392 {boost::beast::http::verb::put, {{"ConfigureUsers"}}}, 393 {boost::beast::http::verb::delete_, {{"ConfigureUsers"}}}, 394 {boost::beast::http::verb::post, {{"ConfigureUsers"}}}}; 395 } 396 397 private: 398 void doGet(crow::Response& res, const crow::Request& req, 399 const std::vector<std::string>& params) override 400 { 401 res.jsonValue = { 402 {"@odata.context", 403 "/redfish/v1/$metadata#ManagerAccount.ManagerAccount"}, 404 {"@odata.type", "#ManagerAccount.v1_0_3.ManagerAccount"}, 405 {"Name", "User Account"}, 406 {"Description", "User Account"}, 407 {"Password", nullptr}, 408 {"RoleId", "Administrator"}}; 409 410 auto asyncResp = std::make_shared<AsyncResp>(res); 411 412 if (params.size() != 1) 413 { 414 messages::internalError(asyncResp->res); 415 return; 416 } 417 418 crow::connections::systemBus->async_method_call( 419 [asyncResp, accountName{std::string(params[0])}]( 420 const boost::system::error_code ec, 421 const ManagedObjectType& users) { 422 if (ec) 423 { 424 messages::internalError(asyncResp->res); 425 return; 426 } 427 auto userIt = users.begin(); 428 429 for (; userIt != users.end(); userIt++) 430 { 431 if (boost::ends_with(userIt->first.str, "/" + accountName)) 432 { 433 break; 434 } 435 } 436 if (userIt == users.end()) 437 { 438 messages::resourceNotFound(asyncResp->res, "ManagerAccount", 439 accountName); 440 return; 441 } 442 for (const auto& interface : userIt->second) 443 { 444 if (interface.first == 445 "xyz.openbmc_project.User.Attributes") 446 { 447 for (const auto& property : interface.second) 448 { 449 if (property.first == "UserEnabled") 450 { 451 const bool* userEnabled = 452 std::get_if<bool>(&property.second); 453 if (userEnabled == nullptr) 454 { 455 BMCWEB_LOG_ERROR 456 << "UserEnabled wasn't a bool"; 457 messages::internalError(asyncResp->res); 458 return; 459 } 460 asyncResp->res.jsonValue["Enabled"] = 461 *userEnabled; 462 } 463 else if (property.first == 464 "UserLockedForFailedAttempt") 465 { 466 const bool* userLocked = 467 std::get_if<bool>(&property.second); 468 if (userLocked == nullptr) 469 { 470 BMCWEB_LOG_ERROR << "UserLockedForF" 471 "ailedAttempt " 472 "wasn't a bool"; 473 messages::internalError(asyncResp->res); 474 return; 475 } 476 asyncResp->res.jsonValue["Locked"] = 477 *userLocked; 478 asyncResp->res.jsonValue 479 ["Locked@Redfish.AllowableValues"] = { 480 "false"}; 481 } 482 else if (property.first == "UserPrivilege") 483 { 484 const std::string* userRolePtr = 485 std::get_if<std::string>(&property.second); 486 if (userRolePtr == nullptr) 487 { 488 BMCWEB_LOG_ERROR 489 << "UserPrivilege wasn't a " 490 "string"; 491 messages::internalError(asyncResp->res); 492 return; 493 } 494 std::string priv = 495 getPrivilegeFromRoleId(*userRolePtr); 496 if (priv.empty()) 497 { 498 BMCWEB_LOG_ERROR << "Invalid user role"; 499 messages::internalError(asyncResp->res); 500 return; 501 } 502 asyncResp->res.jsonValue["RoleId"] = priv; 503 504 asyncResp->res.jsonValue["Links"]["Role"] = { 505 {"@odata.id", "/redfish/v1/AccountService/" 506 "Roles/" + 507 priv}}; 508 } 509 } 510 } 511 } 512 513 asyncResp->res.jsonValue["@odata.id"] = 514 "/redfish/v1/AccountService/Accounts/" + accountName; 515 asyncResp->res.jsonValue["Id"] = accountName; 516 asyncResp->res.jsonValue["UserName"] = accountName; 517 }, 518 "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user", 519 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); 520 } 521 522 void doPatch(crow::Response& res, const crow::Request& req, 523 const std::vector<std::string>& params) override 524 { 525 auto asyncResp = std::make_shared<AsyncResp>(res); 526 if (params.size() != 1) 527 { 528 messages::internalError(asyncResp->res); 529 return; 530 } 531 532 std::optional<std::string> newUserName; 533 std::optional<std::string> password; 534 std::optional<bool> enabled; 535 std::optional<std::string> roleId; 536 std::optional<bool> locked; 537 if (!json_util::readJson(req, res, "UserName", newUserName, "Password", 538 password, "RoleId", roleId, "Enabled", enabled, 539 "Locked", locked)) 540 { 541 return; 542 } 543 544 const std::string& username = params[0]; 545 546 if (!newUserName) 547 { 548 // If the username isn't being updated, we can update the properties 549 // directly 550 updateUserProperties(asyncResp, username, password, enabled, roleId, 551 locked); 552 return; 553 } 554 else 555 { 556 crow::connections::systemBus->async_method_call( 557 [this, asyncResp, username, password(std::move(password)), 558 roleId(std::move(roleId)), enabled(std::move(enabled)), 559 newUser{std::string(*newUserName)}, locked(std::move(locked))]( 560 const boost::system::error_code ec) { 561 if (ec) 562 { 563 BMCWEB_LOG_ERROR << "D-Bus responses error: " << ec; 564 messages::resourceNotFound( 565 asyncResp->res, 566 "#ManagerAccount.v1_0_3.ManagerAccount", username); 567 return; 568 } 569 570 updateUserProperties(asyncResp, newUser, password, enabled, 571 roleId, locked); 572 }, 573 "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user", 574 "xyz.openbmc_project.User.Manager", "RenameUser", username, 575 *newUserName); 576 } 577 } 578 579 void updateUserProperties(std::shared_ptr<AsyncResp> asyncResp, 580 const std::string& username, 581 std::optional<std::string> password, 582 std::optional<bool> enabled, 583 std::optional<std::string> roleId, 584 std::optional<bool> locked) 585 { 586 if (password) 587 { 588 if (!pamUpdatePassword(username, *password)) 589 { 590 BMCWEB_LOG_ERROR << "pamUpdatePassword Failed"; 591 messages::internalError(asyncResp->res); 592 return; 593 } 594 } 595 596 std::string dbusObjectPath = "/xyz/openbmc_project/user/" + username; 597 dbus::utility::escapePathForDbus(dbusObjectPath); 598 599 checkDbusPathExists( 600 dbusObjectPath, 601 [dbusObjectPath(std::move(dbusObjectPath)), username, 602 password(std::move(password)), roleId(std::move(roleId)), 603 enabled(std::move(enabled)), locked(std::move(locked)), 604 asyncResp{std::move(asyncResp)}](int rc) { 605 if (!rc) 606 { 607 messages::invalidObject(asyncResp->res, username.c_str()); 608 return; 609 } 610 if (enabled) 611 { 612 crow::connections::systemBus->async_method_call( 613 [asyncResp](const boost::system::error_code ec) { 614 if (ec) 615 { 616 BMCWEB_LOG_ERROR << "D-Bus responses error: " 617 << ec; 618 messages::internalError(asyncResp->res); 619 return; 620 } 621 messages::success(asyncResp->res); 622 return; 623 }, 624 "xyz.openbmc_project.User.Manager", 625 dbusObjectPath.c_str(), 626 "org.freedesktop.DBus.Properties", "Set", 627 "xyz.openbmc_project.User.Attributes", "UserEnabled", 628 std::variant<bool>{*enabled}); 629 } 630 631 if (roleId) 632 { 633 std::string priv = getRoleIdFromPrivilege(*roleId); 634 if (priv.empty()) 635 { 636 messages::propertyValueNotInList(asyncResp->res, 637 *roleId, "RoleId"); 638 return; 639 } 640 641 crow::connections::systemBus->async_method_call( 642 [asyncResp](const boost::system::error_code ec) { 643 if (ec) 644 { 645 BMCWEB_LOG_ERROR << "D-Bus responses error: " 646 << ec; 647 messages::internalError(asyncResp->res); 648 return; 649 } 650 messages::success(asyncResp->res); 651 }, 652 "xyz.openbmc_project.User.Manager", 653 dbusObjectPath.c_str(), 654 "org.freedesktop.DBus.Properties", "Set", 655 "xyz.openbmc_project.User.Attributes", "UserPrivilege", 656 std::variant<std::string>{priv}); 657 } 658 659 if (locked) 660 { 661 // admin can unlock the account which is locked by 662 // successive authentication failures but admin should not 663 // be allowed to lock an account. 664 if (*locked) 665 { 666 messages::propertyValueNotInList(asyncResp->res, "true", 667 "Locked"); 668 return; 669 } 670 671 crow::connections::systemBus->async_method_call( 672 [asyncResp](const boost::system::error_code ec) { 673 if (ec) 674 { 675 BMCWEB_LOG_ERROR << "D-Bus responses error: " 676 << ec; 677 messages::internalError(asyncResp->res); 678 return; 679 } 680 messages::success(asyncResp->res); 681 return; 682 }, 683 "xyz.openbmc_project.User.Manager", 684 dbusObjectPath.c_str(), 685 "org.freedesktop.DBus.Properties", "Set", 686 "xyz.openbmc_project.User.Attributes", 687 "UserLockedForFailedAttempt", 688 sdbusplus::message::variant<bool>{*locked}); 689 } 690 }); 691 } 692 693 void doDelete(crow::Response& res, const crow::Request& req, 694 const std::vector<std::string>& params) override 695 { 696 auto asyncResp = std::make_shared<AsyncResp>(res); 697 698 if (params.size() != 1) 699 { 700 messages::internalError(asyncResp->res); 701 return; 702 } 703 704 const std::string userPath = "/xyz/openbmc_project/user/" + params[0]; 705 706 crow::connections::systemBus->async_method_call( 707 [asyncResp, username{std::move(params[0])}]( 708 const boost::system::error_code ec) { 709 if (ec) 710 { 711 messages::resourceNotFound( 712 asyncResp->res, "#ManagerAccount.v1_0_3.ManagerAccount", 713 username); 714 return; 715 } 716 717 messages::accountRemoved(asyncResp->res); 718 }, 719 "xyz.openbmc_project.User.Manager", userPath, 720 "xyz.openbmc_project.Object.Delete", "Delete"); 721 } 722 }; 723 724 } // namespace redfish 725