Check time when getting UserLockedForFailedAttempt

In pam_tally2, the user is unlocked after unlock_time seconds. But
phosphor-user-manager only checks the login failure attempts and sets
UserLockedForFailedAttempt to true when it exceeds the max attempts
allowed. This patch fixes this issue by adding a comparison of current
time and the last login failure time.

Tested:
Login with wrong password for {MaxLoginAttemptBeforeLockout} times,
verified UserLockedForFailedAttempt is changed to true. Then wait for
{AccountUnlockTimeout} seconds, it becomes to false.

Change-Id: If8427e5644e0cd88758b0266973feb50a9831fe1
Signed-off-by: Jiaqing Zhao <jiaqing.zhao@intel.com>
diff --git a/user_mgr.cpp b/user_mgr.cpp
index d5100af..61ffc05 100644
--- a/user_mgr.cpp
+++ b/user_mgr.cpp
@@ -40,6 +40,7 @@
 #include <xyz/openbmc_project/User/Common/error.hpp>
 
 #include <algorithm>
+#include <ctime>
 #include <fstream>
 #include <numeric>
 #include <regex>
@@ -681,18 +682,25 @@
 /**
  * pam_tally2 app will provide the user failure count and failure status
  * in second line of output with words position [0] - user name,
- * [1] - failure count, [2] - latest timestamp, [3] - failure timestamp
+ * [1] - failure count, [2] - latest failure date, [3] - latest failure time
  * [4] - failure app
  **/
 
 static constexpr size_t t2UserIdx = 0;
 static constexpr size_t t2FailCntIdx = 1;
+static constexpr size_t t2FailDateIdx = 2;
+static constexpr size_t t2FailTimeIdx = 3;
 static constexpr size_t t2OutputIndex = 1;
 
 bool UserMgr::userLockedForFailedAttempt(const std::string& userName)
 {
     // All user management lock has to be based on /etc/shadow
     // TODO  phosphor-user-manager#10 phosphor::user::shadow::Lock lock{};
+    if (AccountPolicyIface::maxLoginAttemptBeforeLockout() == 0)
+    {
+        return false;
+    }
+
     std::vector<std::string> output;
     try
     {
@@ -709,28 +717,58 @@
                             boost::algorithm::is_any_of("\t "),
                             boost::token_compress_on);
 
+    uint16_t failAttempts = 0;
     try
     {
         unsigned long tmp = std::stoul(splitWords[t2FailCntIdx], nullptr);
-        uint16_t value16 = 0;
-        if (tmp > std::numeric_limits<decltype(value16)>::max())
+        if (tmp > std::numeric_limits<decltype(failAttempts)>::max())
         {
             throw std::out_of_range("Out of range");
         }
-        value16 = static_cast<decltype(value16)>(tmp);
-        if (AccountPolicyIface::maxLoginAttemptBeforeLockout() != 0 &&
-            value16 >= AccountPolicyIface::maxLoginAttemptBeforeLockout())
-        {
-            return true; // User account is locked out
-        }
-        return false; // User account is un-locked
+        failAttempts = static_cast<decltype(failAttempts)>(tmp);
     }
     catch (const std::exception& e)
     {
         log<level::ERR>("Exception for userLockedForFailedAttempt",
                         entry("WHAT=%s", e.what()));
-        throw;
+        elog<InternalFailure>();
     }
+
+    if (failAttempts < AccountPolicyIface::maxLoginAttemptBeforeLockout())
+    {
+        return false;
+    }
+
+    // When failedAttempts is not 0, Latest failure date/time should be
+    // available
+    if (splitWords.size() < 4)
+    {
+        log<level::ERR>("Unable to read latest failure date/time");
+        elog<InternalFailure>();
+    }
+
+    const std::string failDateTime =
+        splitWords[t2FailDateIdx] + ' ' + splitWords[t2FailTimeIdx];
+
+    // NOTE: Cannot use std::get_time() here as the implementation of %y in
+    // libstdc++ does not match POSIX strptime() before gcc 12.1.0
+    // https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=a8d3c98746098e2784be7144c1ccc9fcc34a0888
+    std::tm tmStruct = {};
+    if (!strptime(failDateTime.c_str(), "%D %H:%M:%S", &tmStruct))
+    {
+        log<level::ERR>("Failed to parse latest failure date/time");
+        elog<InternalFailure>();
+    }
+
+    time_t failTimestamp = std::mktime(&tmStruct);
+    if (failTimestamp +
+            static_cast<time_t>(AccountPolicyIface::accountUnlockTimeout()) <=
+        std::time(NULL))
+    {
+        return false;
+    }
+
+    return true;
 }
 
 bool UserMgr::userLockedForFailedAttempt(const std::string& userName,