Add UserPasswordExpired for local users

Adds a new UserPasswordExpired property to local User.Attributes which
represents if the account's password is expired and must be changed.
The value corresponds to the `chage` command.

Note this is distinct from UserLockedForFailedAttempt which represents
a locked account due to unsuccessful authentication atttempts.

Tested: Via busctl
- Checked local and LDAP users.
- Expired password via `passwd --expire USER`.
- Aged password via `chage USER`.
- Changed password via REST API and via the `passwd USER` command.

Signed-off-by: Joseph Reynolds <joseph-reynolds@charter.net>
Change-Id: I44585559509a422bb91c83a2a853c1a033594350
diff --git a/docs/README.md b/docs/README.md
index 0481327..ea3b532 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -14,7 +14,7 @@
 #### Configure LDAP
 
 ```
-curl -c cjar -b cjar -k -H "Content-Type: application/json" -X POST -d '{"data":[false,"ldap://<ldap://<LDAP server ip/hostname>/", "<bindDN>", "<baseDN>","<bindDNPassword>","<searchScope>","<serverType>"]}''  https://$BMC_IP//xyz/openbmc_project/user/ldap/action/CreateConfig
+curl -c cjar -b cjar -k -H "Content-Type: application/json" -X POST -d '{"data":[false,"ldap://<ldap://<LDAP server ip/hostname>/", "<bindDN>", "<baseDN>","<bindDNPassword>","<searchScope>","<serverType>"]}''  https://$BMC_IP/xyz/openbmc_project/user/ldap/action/CreateConfig
 
 ```
 #### NOTE
diff --git a/test/mock_user_mgr.hpp b/test/mock_user_mgr.hpp
index 81f9065..ea7c5f6 100644
--- a/test/mock_user_mgr.hpp
+++ b/test/mock_user_mgr.hpp
@@ -19,6 +19,7 @@
     MOCK_METHOD1(getLdapGroupName, std::string(const std::string& userName));
     MOCK_METHOD0(getPrivilegeMapperObject, DbusUserObj());
     MOCK_METHOD1(userLockedForFailedAttempt, bool(const std::string& userName));
+    MOCK_METHOD1(userPasswordExpired, bool(const std::string& userName));
 
     friend class TestUserMgr;
 };
diff --git a/test/user_mgr_test.cpp b/test/user_mgr_test.cpp
index 943f0a8..83ae651 100644
--- a/test/user_mgr_test.cpp
+++ b/test/user_mgr_test.cpp
@@ -103,6 +103,7 @@
               std::get<std::vector<std::string>>(userInfo["UserGroups"]));
     EXPECT_EQ(true, std::get<bool>(userInfo["UserEnabled"]));
     EXPECT_EQ(false, std::get<bool>(userInfo["UserLockedForFailedAttempt"]));
+    EXPECT_EQ(false, std::get<bool>(userInfo["UserPasswordExpired"]));
     EXPECT_EQ(false, std::get<bool>(userInfo["RemoteUser"]));
 }
 
diff --git a/user_mgr.cpp b/user_mgr.cpp
index 2f22323..9694fd1 100644
--- a/user_mgr.cpp
+++ b/user_mgr.cpp
@@ -18,6 +18,7 @@
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/wait.h>
+#include <time.h>
 #include <fstream>
 #include <grp.h>
 #include <pwd.h>
@@ -726,6 +727,50 @@
     return userLockedForFailedAttempt(userName);
 }
 
+bool UserMgr::userPasswordExpired(const std::string &userName)
+{
+    // All user management lock has to be based on /etc/shadow
+    phosphor::user::shadow::Lock lock();
+
+    struct spwd spwd
+    {
+    };
+    struct spwd *spwdPtr = nullptr;
+    auto buflen = sysconf(_SC_GETPW_R_SIZE_MAX);
+    if (buflen < -1)
+    {
+        // Use a default size if there is no hard limit suggested by sysconf()
+        buflen = 1024;
+    }
+    std::vector<char> buffer(buflen);
+    auto status =
+        getspnam_r(userName.c_str(), &spwd, buffer.data(), buflen, &spwdPtr);
+    // On success, getspnam_r() returns zero, and sets *spwdPtr to spwd.
+    // If no matching password record was found, these functions return 0
+    // and store NULL in *spwdPtr
+    if ((status == 0) && (&spwd == spwdPtr))
+    {
+        // Determine password validity per "chage" docs, where:
+        //   spwd.sp_lstchg == 0 means password is expired, and
+        //   spwd.sp_max == -1 means the password does not expire.
+        constexpr long seconds_per_day = 60 * 60 * 24;
+        long today = static_cast<long>(time(NULL)) / seconds_per_day;
+        if ((spwd.sp_lstchg == 0) ||
+            ((spwd.sp_max != -1) && ((spwd.sp_max + spwd.sp_lstchg) < today)))
+        {
+            return true;
+        }
+    }
+    else
+    {
+        log<level::ERR>("User does not exist",
+                        entry("USER_NAME=%s", userName.c_str()));
+        elog<UserNameDoesNotExist>();
+    }
+
+    return false;
+}
+
 UserSSHLists UserMgr::getUserAndSshGrpList()
 {
     // All user management lock has to be based on /etc/shadow
@@ -952,6 +997,8 @@
         userInfo.emplace("UserEnabled", user.get()->userEnabled());
         userInfo.emplace("UserLockedForFailedAttempt",
                          user.get()->userLockedForFailedAttempt());
+        userInfo.emplace("UserPasswordExpired",
+                         user.get()->userPasswordExpired());
         userInfo.emplace("RemoteUser", false);
     }
     else
diff --git a/user_mgr.hpp b/user_mgr.hpp
index c008cea..e25ca87 100644
--- a/user_mgr.hpp
+++ b/user_mgr.hpp
@@ -168,6 +168,13 @@
     bool userLockedForFailedAttempt(const std::string &userName,
                                     const bool &value);
 
+    /** @brief shows if the user's password is expired
+     *
+     * @param[in]: user name
+     * @return - true / false indicating user password expired
+     **/
+    virtual bool userPasswordExpired(const std::string &userName);
+
     /** @brief returns user info
      * Checks if user is local user, then returns map of properties of user.
      * like user privilege, list of user groups, user enabled state and user
diff --git a/users.cpp b/users.cpp
index dafc2dc..562a0ad 100644
--- a/users.cpp
+++ b/users.cpp
@@ -166,5 +166,13 @@
     }
 }
 
+/** @brief indicates if the user's password is expired
+ *
+ **/
+bool Users::userPasswordExpired(void) const
+{
+    return manager.userPasswordExpired(userName);
+}
+
 } // namespace user
 } // namespace phosphor
diff --git a/users.hpp b/users.hpp
index aa1726a..45d86cd 100644
--- a/users.hpp
+++ b/users.hpp
@@ -111,6 +111,11 @@
      **/
     bool userLockedForFailedAttempt(bool value) override;
 
+    /** @brief indicates if the user's password is expired
+     *
+     **/
+    bool userPasswordExpired(void) const;
+
   private:
     std::string userName;
     UserMgr &manager;