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/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