PATCH userGroups Information  ("AccountTypes")

This commit enhances the redfish API to set and unset userGroups
information for each user account.

Users with ConfigureUsers level privilege can patch (Set and Unset)
AccountTypes of each user role. In addition, a user with
"ConfigureSelf" level privilege can only set or Update their password.

"Redfish" is always enabled in each user role. However,
"ConfigureUsers" can disable other user redfish services. But if
"ConfigureUsers" try to disable its redfish service, that generates an
error.

In this commit, users can enable and disable "redfish", "ssh",
"hostconsole" and "ipmi" services from each user where ssh is a special
case.

The 'web' group does not control access to the web interface, and
doesn't appear to do anything. The 'redfish' in the UserGroups is
mapped to both Redfish and WebUI AccountTypes. To enable redfish
User Group both of these account types should be specified, and none
to disable it.

Tested:
Testing was done using curl command with ConfigureUsers and
ConfigureSelf.

$ curl -k -X PATCH
https://$bmc:18080/redfish/v1/AccountService/Accounts/webuser -d
'{"AccountTypes": ["Redfish", "WebUI", "ManagerConsole",
"HostConsole"]}'
{
  "@Message.ExtendedInfo": [
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The request completed successfully.",
      "MessageArgs": [],
      "MessageId": "Base.1.13.0.Success",
      "MessageSeverity": "OK",
      "Resolution": "None"
    }
  ]
}

Also ran following cases:

$ curl -k -X PATCH
https://${bmc}/redfish/v1/AccountService/Accounts/user99
-d '{"AccountTypes": ["HostConsole"]}'

$ curl -k -X PATCH
https://${bmc}/redfish/v1/AccountService/Accounts/user99
-d '{"AccountTypes": ["IPMI"]}'

$ curl -k -X PATCH
https://${bmc}/redfish/v1/AccountService/Accounts/user99
-d '{"AccountTypes": ["Redfish", "WebUI"]}'

$ curl -k -X PATCH
https://${bmc}/redfish/v1/AccountService/Accounts/user99
-d '{"AccountTypes": ["ManagerConsole"]}'

$ curl -k -X PATCH
https://${bmc}/redfish/v1/AccountService/Accounts/user99
-d '{"AccountTypes": ["Redfish", "IPMI", "HostConsole",
                      "ManagerConsole", "WebUI"]}'
{
  "error": {
    "@Message.ExtendedInfo": [
      {
        "@odata.type": "#Message.v1_1_1.Message",
        "Message": "There are insufficient privileges for the account or
                    credentials associated with the current session to
                    perform the requested operation.",
        "MessageArgs": [],
        "MessageId": "Base.1.13.0.InsufficientPrivilege",
        "MessageSeverity": "Critical",
        "Resolution": "Either abandon the operation or change the
         associated access rights and resubmit the request if the
         operation failed."
      }
    ],
    "code": "Base.1.13.0.InsufficientPrivilege",
    "message": "There are insufficient privileges for the account or
                credentials associated with the current session to
                perform the requested operation."
  }

$  curl -k -H 'X-Auth-Token: IpnCBj1Lozh53Jhzxu7T' -X PATCH
https://${bmc}/redfish/v1/AccountService/Accounts/user999
-d '{"Password":"0penBmc123"}'
{
  "@Message.ExtendedInfo": [
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The request completed successfully.",
      "MessageArgs": [],
      "MessageId": "Base.1.13.0.Success",
      "MessageSeverity": "OK",
      "Resolution": "None"
    }
  ]

Signed-off-by: Ninad Palsule <ninadpalsule@us.ibm.com>
Signed-off-by: Abhishek Patel <Abhishek.Patel@ibm.com>
Change-Id: I1a0344ca45556b820bb77c3dcb459f27eb032501
Signed-off-by: Shantappa Teekappanavar <shantappa.teekappanavar@ibm.com>
diff --git a/Redfish.md b/Redfish.md
index 4e5a19b..9cd7a10 100644
--- a/Redfish.md
+++ b/Redfish.md
@@ -111,6 +111,7 @@
 - Password
 - PasswordChangeRequired
 - RoleId
+- StrictAccountTypes
 - UserName
 
 ### /redfish/v1/AccountService/LDAP/Certificates/
diff --git a/redfish-core/lib/account_service.hpp b/redfish-core/lib/account_service.hpp
index c61fa46..06ef008 100644
--- a/redfish-core/lib/account_service.hpp
+++ b/redfish-core/lib/account_service.hpp
@@ -162,6 +162,120 @@
     return true;
 }
 
+/**
+ * @brief Builds User Groups from the Account Types
+ *
+ * @param[in] asyncResp Async Response
+ * @param[in] accountTypes List of Account Types
+ * @param[out] userGroups List of User Groups mapped from Account Types
+ *
+ * @return true if Account Types mapped to User Groups, false otherwise.
+ */
+inline bool
+    getUserGroupFromAccountType(crow::Response& res,
+                                const std::vector<std::string>& accountTypes,
+                                std::vector<std::string>& userGroups)
+{
+    // Need both Redfish and WebUI Account Types to map to 'redfish' User Group
+    bool redfishType = false;
+    bool webUIType = false;
+
+    for (const auto& accountType : accountTypes)
+    {
+        if (accountType == "Redfish")
+        {
+            redfishType = true;
+        }
+        else if (accountType == "WebUI")
+        {
+            webUIType = true;
+        }
+        else if (accountType == "IPMI")
+        {
+            userGroups.emplace_back("ipmi");
+        }
+        else if (accountType == "HostConsole")
+        {
+            userGroups.emplace_back("hostconsole");
+        }
+        else if (accountType == "ManagerConsole")
+        {
+            userGroups.emplace_back("ssh");
+        }
+        else
+        {
+            // Invalid Account Type
+            messages::propertyValueNotInList(res, "AccountTypes", accountType);
+            return false;
+        }
+    }
+
+    // Both  Redfish and WebUI Account Types are needed to PATCH
+    if (redfishType ^ webUIType)
+    {
+        BMCWEB_LOG_ERROR
+            << "Missing Redfish or WebUI Account Type to set redfish User Group";
+        messages::strictAccountTypes(res, "AccountTypes");
+        return false;
+    }
+
+    if (redfishType && webUIType)
+    {
+        userGroups.emplace_back("redfish");
+    }
+
+    return true;
+}
+
+/**
+ * @brief Sets UserGroups property of the user based on the Account Types
+ *
+ * @param[in] accountTypes List of User Account Types
+ * @param[in] asyncResp Async Response
+ * @param[in] dbusObjectPath D-Bus Object Path
+ * @param[in] userSelf true if User is updating OWN Account Types
+ */
+inline void
+    patchAccountTypes(const std::vector<std::string>& accountTypes,
+                      const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                      const std::string& dbusObjectPath, bool userSelf)
+{
+    // Check if User is disabling own Redfish Account Type
+    if (userSelf &&
+        (accountTypes.cend() ==
+         std::find(accountTypes.cbegin(), accountTypes.cend(), "Redfish")))
+    {
+        BMCWEB_LOG_ERROR
+            << "User disabling OWN Redfish Account Type is not allowed";
+        messages::strictAccountTypes(asyncResp->res, "AccountTypes");
+        return;
+    }
+
+    std::vector<std::string> updatedUserGroups;
+    if (!getUserGroupFromAccountType(asyncResp->res, accountTypes,
+                                     updatedUserGroups))
+    {
+        // Problem in mapping Account Types to User Groups, Error already
+        // logged.
+        return;
+    }
+
+    crow::connections::systemBus->async_method_call(
+        [asyncResp](const boost::system::error_code ec) {
+        if (ec)
+        {
+            BMCWEB_LOG_ERROR << "D-Bus responses error: " << ec;
+            messages::internalError(asyncResp->res);
+            return;
+        }
+        messages::success(asyncResp->res);
+        },
+        "xyz.openbmc_project.User.Manager", dbusObjectPath,
+        "org.freedesktop.DBus.Properties", "Set",
+        "xyz.openbmc_project.User.Attributes", "UserGroups",
+        dbus::utility::DbusVariantType{updatedUserGroups});
+}
+
 inline void userErrorMessageHandler(
     const sd_bus_error* e, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
     const std::string& newUser, const std::string& username)
@@ -1201,12 +1315,12 @@
         });
 }
 
-inline void updateUserProperties(std::shared_ptr<bmcweb::AsyncResp> asyncResp,
-                                 const std::string& username,
-                                 const std::optional<std::string>& password,
-                                 const std::optional<bool>& enabled,
-                                 const std::optional<std::string>& roleId,
-                                 const std::optional<bool>& locked)
+inline void updateUserProperties(
+    std::shared_ptr<bmcweb::AsyncResp> asyncResp, const std::string& username,
+    const std::optional<std::string>& password,
+    const std::optional<bool>& enabled,
+    const std::optional<std::string>& roleId, const std::optional<bool>& locked,
+    std::optional<std::vector<std::string>> accountTypes, bool userSelf)
 {
     sdbusplus::message::object_path tempObjPath(rootUserDbusPath);
     tempObjPath /= username;
@@ -1214,7 +1328,8 @@
 
     dbus::utility::checkDbusPathExists(
         dbusObjectPath, [dbusObjectPath, username, password, roleId, enabled,
-                         locked, asyncResp{std::move(asyncResp)}](int rc) {
+                         locked, accountTypes(std::move(accountTypes)),
+                         userSelf, asyncResp{std::move(asyncResp)}](int rc) {
             if (rc <= 0)
             {
                 messages::resourceNotFound(asyncResp->res, "ManagerAccount",
@@ -1323,6 +1438,12 @@
                     "UserLockedForFailedAttempt",
                     dbus::utility::DbusVariantType{*locked});
             }
+
+            if (accountTypes)
+            {
+                patchAccountTypes(*accountTypes, asyncResp, dbusObjectPath,
+                                  userSelf);
+            }
         });
 }
 
@@ -1871,10 +1992,11 @@
         }
 
         asyncResp->res.jsonValue["@odata.type"] =
-            "#ManagerAccount.v1_4_0.ManagerAccount";
+            "#ManagerAccount.v1_7_0.ManagerAccount";
         asyncResp->res.jsonValue["Name"] = "User Account";
         asyncResp->res.jsonValue["Description"] = "User Account";
         asyncResp->res.jsonValue["Password"] = nullptr;
+        asyncResp->res.jsonValue["StrictAccountTypes"] = true;
 
         for (const auto& interface : userIt->second)
         {
@@ -2037,6 +2159,9 @@
     std::optional<bool> enabled;
     std::optional<std::string> roleId;
     std::optional<bool> locked;
+    std::optional<std::vector<std::string>> accountTypes;
+
+    bool userSelf = (username == req.session->username);
 
     if (req.session == nullptr)
     {
@@ -2052,10 +2177,10 @@
     if (userHasConfigureUsers)
     {
         // Users with ConfigureUsers can modify for all users
-        if (!json_util::readJsonPatch(req, asyncResp->res, "UserName",
-                                      newUserName, "Password", password,
-                                      "RoleId", roleId, "Enabled", enabled,
-                                      "Locked", locked))
+        if (!json_util::readJsonPatch(
+                req, asyncResp->res, "UserName", newUserName, "Password",
+                password, "RoleId", roleId, "Enabled", enabled, "Locked",
+                locked, "AccountTypes", accountTypes))
         {
             return;
         }
@@ -2063,7 +2188,7 @@
     else
     {
         // ConfigureSelf accounts can only modify their own account
-        if (username != req.session->username)
+        if (!userSelf)
         {
             messages::insufficientPrivilege(asyncResp->res);
             return;
@@ -2085,13 +2210,14 @@
     if (!newUserName || (newUserName.value() == username))
     {
         updateUserProperties(asyncResp, username, password, enabled, roleId,
-                             locked);
+                             locked, accountTypes, userSelf);
         return;
     }
     crow::connections::systemBus->async_method_call(
         [asyncResp, username, password(std::move(password)),
          roleId(std::move(roleId)), enabled, newUser{std::string(*newUserName)},
-         locked](const boost::system::error_code& ec, sdbusplus::message_t& m) {
+         locked, userSelf, accountTypes(std::move(accountTypes))](
+            const boost::system::error_code ec, sdbusplus::message_t& m) {
         if (ec)
         {
             userErrorMessageHandler(m.get_error(), asyncResp, newUser,
@@ -2100,7 +2226,7 @@
         }
 
         updateUserProperties(asyncResp, newUser, password, enabled, roleId,
-                             locked);
+                             locked, accountTypes, userSelf);
         },
         "xyz.openbmc_project.User.Manager", "/xyz/openbmc_project/user",
         "xyz.openbmc_project.User.Manager", "RenameUser", username,