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