This is an automated email from the ASF dual-hosted git repository.

ramackri pushed a commit to branch RANGER-5690-backport-ranger-2.9
in repository https://gitbox.apache.org/repos/asf/ranger.git

commit 5b6869b3ec3c577824d5f84b5d093232fab5557a
Author: ramk <[email protected]>
AuthorDate: Tue Jul 21 20:26:32 2026 +0530

    RANGER-5690: UnixAuth service lacks rate limiting on authentication 
attempts (#1080)
    
    Back-port to ranger-2.9. Preserve TLSv1.2 SSL algorithm used on 2.9 branch.
---
 .../conf.dist/ranger-ugsync-default.xml            |  20 +
 .../ranger/authentication/LoginAttemptTracker.java | 100 ++++
 .../ranger/authentication/PasswordValidator.java   | 270 +++++----
 .../authentication/UnixAuthenticationService.java  | 658 ++++++++++-----------
 .../authentication/LoginAttemptTrackerTest.java    | 100 ++++
 .../authentication/TestPasswordValidator.java      | 267 +++++++++
 6 files changed, 940 insertions(+), 475 deletions(-)

diff --git a/unixauthservice/conf.dist/ranger-ugsync-default.xml 
b/unixauthservice/conf.dist/ranger-ugsync-default.xml
index 9cedc99e5..2e48da7e6 100644
--- a/unixauthservice/conf.dist/ranger-ugsync-default.xml
+++ b/unixauthservice/conf.dist/ranger-ugsync-default.xml
@@ -29,6 +29,26 @@
                <name>ranger.usersync.https.ssl.enabled.protocols</name>
                <value>TLSv1.2</value>
        </property>
+       <property>
+               <name>ranger.usersync.unixauth.max.failed.attempts</name>
+               <value>5</value>
+       </property>
+       <property>
+               <name>ranger.usersync.unixauth.attempt.window.ms</name>
+               <value>60000</value>
+       </property>
+       <property>
+               <name>ranger.usersync.unixauth.lockout.duration.ms</name>
+               <value>30000</value>
+       </property>
+       <property>
+               <name>ranger.usersync.unixauth.require.client.auth</name>
+               <value>false</value>
+       </property>
+       <property>
+               <name>ranger.usersync.unixauth.socket.timeout.ms</name>
+               <value>10000</value>
+       </property>
        <property>
                <name>ranger.usersync.passwordvalidator.path</name>
                <value>./native/credValidator.uexe</value>
diff --git 
a/unixauthservice/src/main/java/org/apache/ranger/authentication/LoginAttemptTracker.java
 
b/unixauthservice/src/main/java/org/apache/ranger/authentication/LoginAttemptTracker.java
new file mode 100644
index 000000000..b448a7b19
--- /dev/null
+++ 
b/unixauthservice/src/main/java/org/apache/ranger/authentication/LoginAttemptTracker.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authentication;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class LoginAttemptTracker {
+    private static final long CLEANUP_INTERVAL_MS = 
TimeUnit.MINUTES.toMillis(5);
+    private final ConcurrentHashMap<String, Record> attemptsByIp = new 
ConcurrentHashMap<>();
+
+    private final int maxFailures;
+    private final long windowMs;
+    private final long lockoutMs;
+    private volatile long lastCleanupTime;
+
+    public LoginAttemptTracker(int maxFailures, long windowMs, long lockoutMs) 
{
+        this.maxFailures = maxFailures;
+        this.windowMs = windowMs;
+        this.lockoutMs = lockoutMs;
+        this.lastCleanupTime = System.currentTimeMillis();
+    }
+
+    public boolean isBlocked(String sourceIp) {
+        Record record = attemptsByIp.get(sourceIp);
+        if (record == null) {
+            return false;
+        }
+        return record.lockedUntil > System.currentTimeMillis();
+    }
+
+    public void recordFailure(String sourceIp) {
+        maybeCleanup();
+        long now = System.currentTimeMillis();
+        Record record = attemptsByIp.computeIfAbsent(sourceIp, k -> new 
Record(now));
+        synchronized (record) {
+            if (now - record.windowStart > windowMs) {
+                record.windowStart = now;
+                record.failCount.set(0);
+            }
+            record.lastActivity = now;
+            int count = record.failCount.incrementAndGet();
+            if (count >= maxFailures) {
+                record.lockedUntil = now + lockoutMs;
+            }
+        }
+    }
+
+    public void recordSuccess(String sourceIp) {
+        attemptsByIp.remove(sourceIp);
+    }
+
+    private void maybeCleanup() {
+        long now = System.currentTimeMillis();
+        if (now - lastCleanupTime < CLEANUP_INTERVAL_MS) {
+            return;
+        }
+        lastCleanupTime = now;
+        long cutoff = now - windowMs;
+        Iterator<Map.Entry<String, Record>> it = 
attemptsByIp.entrySet().iterator();
+        while (it.hasNext()) {
+            Record record = it.next().getValue();
+            if (record.lastActivity < cutoff && record.lockedUntil <= now) {
+                it.remove();
+            }
+        }
+    }
+
+    private static final class Record {
+        final AtomicInteger failCount = new AtomicInteger(0);
+        volatile long windowStart;
+        volatile long lastActivity;
+        volatile long lockedUntil;
+
+        Record(long now) {
+            this.windowStart = now;
+            this.lastActivity = now;
+        }
+    }
+}
diff --git 
a/unixauthservice/src/main/java/org/apache/ranger/authentication/PasswordValidator.java
 
b/unixauthservice/src/main/java/org/apache/ranger/authentication/PasswordValidator.java
index c924932ae..0416ca644 100644
--- 
a/unixauthservice/src/main/java/org/apache/ranger/authentication/PasswordValidator.java
+++ 
b/unixauthservice/src/main/java/org/apache/ranger/authentication/PasswordValidator.java
@@ -17,7 +17,10 @@
  * under the License.
  */
 
- package org.apache.ranger.authentication;
+package org.apache.ranger.authentication;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -27,135 +30,140 @@
 import java.net.Socket;
 import java.util.List;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 public class PasswordValidator implements Runnable {
-
-       private static final Logger LOG = 
LoggerFactory.getLogger(PasswordValidator.class);
-       
-       private static String validatorProgram = null;
-
-       private static List<String> adminUserList;
-
-       private static String adminRoleNames;
-
-       private Socket client;
-       
-       public PasswordValidator(Socket client) {
-               this.client = client;
-       }
-
-       @Override
-       public void run() {
-               BufferedReader reader = null;
-               PrintWriter writer = null;
-
-               String userName = null;
-
-               try {
-                       reader = new BufferedReader(new 
InputStreamReader(client.getInputStream()));
-                       writer = new PrintWriter(new 
OutputStreamWriter(client.getOutputStream()));
-                       String request = reader.readLine();
-                       
-                       if (request.startsWith("LOGIN:")) {
-                               String line = request.substring(6).trim();
-                               int passwordAt = line.indexOf(' ');
-                               if (passwordAt != -1) {
-                                       userName = 
line.substring(0,passwordAt).trim();
-                               }
-                       }
-
-                       if (validatorProgram == null) {
-                               String res = "FAILED: Unable to validate 
credentials.";
-                               writer.println(res);
-                               writer.flush();
-                               LOG.error("Response [" + res + "] for user: " + 
userName + " as ValidatorProgram is not defined in configuration.");
-
-                       }
-                       else {
-                               
-                               BufferedReader pReader = null;
-                               PrintWriter pWriter = null;
-                               Process p =  null;
-                               
-                               try {
-                                       p = 
Runtime.getRuntime().exec(validatorProgram);
-                                       
-                                       pReader = new BufferedReader(new 
InputStreamReader(p.getInputStream()));
-                                       
-                                       pWriter = new PrintWriter(new 
OutputStreamWriter(p.getOutputStream()));
-                                       
-                                       pWriter.println(request); 
pWriter.flush();
-       
-                                       String res = pReader.readLine();
-
-
-                                       if (res != null && 
res.startsWith("OK")) {
-                                               if (adminRoleNames != null && 
adminUserList != null) {
-                                                       if 
(adminUserList.contains(userName)) {
-                                                               res = res + " " 
+ adminRoleNames;
-                                                       }
-                                               }
-                                       }
-
-                                       LOG.info("Response [" + res + "] for 
user: " + userName);
-                                       
-                                       writer.println(res); writer.flush();
-                               }
-                               finally {
-                                       if (p != null) {
-                                               p.destroy();
-                                       }
-                               }
-                       }
-                       
-               }
-               catch(Throwable t) {
-                       if (userName != null && writer != null ) {
-                                String res = "FAILED: unable to validate due 
to error " + t.getMessage();
-                               writer.println(res);
-                                LOG.error("Response [" + res + "] for user: " 
+ userName+", "+ t.getMessage());
-                       }
-               }
-               finally {
-                       try {
-                               if (client != null) {
-                                       client.close();
-                               }
-                       }
-                       catch(IOException ioe){
-                               LOG.debug("Close socket failure. Detail: \n", 
ioe);
-                       }
-                       finally {
-                               client = null;
-                       }
-               }
-       }
-       
-       
-       public static String getValidatorProgram() {
-               return validatorProgram;
-       }
-
-       public static void setValidatorProgram(String validatorProgram) {
-               PasswordValidator.validatorProgram = validatorProgram;
-       }
-
-       public static List<String> getAdminUserList() {
-               return adminUserList;
-       }
-
-       public static void setAdminUserList(List<String> adminUserList) {
-               PasswordValidator.adminUserList = adminUserList;
-       }
-
-       public static String getAdminRoleNames() {
-               return adminRoleNames;
-       }
-
-       public static void setAdminRoleNames(String adminRoleNames) {
-               PasswordValidator.adminRoleNames = adminRoleNames;
-       }
-
+    private static final Logger LOG = 
LoggerFactory.getLogger(PasswordValidator.class);
+    private static final String GENERIC_FAILURE_RESPONSE = "FAILED: 
Authentication failed.";
+    private static final String LOCKED_OUT_RESPONSE = "FAILED: Too many 
attempts. Try again later.";
+
+    private static List<String> adminUserList;
+    private static String adminRoleNames;
+    private static String validatorProgram;
+
+    private final LoginAttemptTracker loginAttemptTracker;
+    private Socket client;
+
+    public PasswordValidator(Socket client, LoginAttemptTracker 
loginAttemptTracker) {
+        this.client = client;
+        this.loginAttemptTracker = loginAttemptTracker;
+    }
+
+    public static String getValidatorProgram() {
+        return validatorProgram;
+    }
+
+    public static void setValidatorProgram(String validatorProgram) {
+        PasswordValidator.validatorProgram = validatorProgram;
+    }
+
+    public static List<String> getAdminUserList() {
+        return adminUserList;
+    }
+
+    public static void setAdminUserList(List<String> adminUserList) {
+        PasswordValidator.adminUserList = adminUserList;
+    }
+
+    public static String getAdminRoleNames() {
+        return adminRoleNames;
+    }
+
+    public static void setAdminRoleNames(String adminRoleNames) {
+        PasswordValidator.adminRoleNames = adminRoleNames;
+    }
+
+    @Override
+    public void run() {
+        BufferedReader reader;
+        PrintWriter    writer = null;
+
+        String userName = null;
+
+        String remoteIp = client.getInetAddress() != null ? 
client.getInetAddress().getHostAddress() : "unknown";
+        try {
+            reader = new BufferedReader(new 
InputStreamReader(client.getInputStream()));
+            writer = new PrintWriter(new 
OutputStreamWriter(client.getOutputStream()));
+            String request = reader.readLine();
+            if (loginAttemptTracker.isBlocked(remoteIp)) {
+                LOG.warn("Rejected UnixAuth attempt from [{}] - source IP is 
temporarily locked out due to repeated failures.", remoteIp);
+                writer.println(LOCKED_OUT_RESPONSE);
+                writer.flush();
+                return;
+            }
+            if (request == null) {
+                loginAttemptTracker.recordFailure(remoteIp);
+                LOG.warn("Rejected UnixAuth attempt from [{}] - connection 
closed before sending any data.", remoteIp);
+                writer.println(GENERIC_FAILURE_RESPONSE);
+                writer.flush();
+                return;
+            }
+            if (request.startsWith("LOGIN:")) {
+                String line       = request.substring(6).trim();
+                int    passwordAt = line.indexOf(' ');
+                if (passwordAt != -1) {
+                    userName = line.substring(0, passwordAt).trim();
+                }
+            }
+
+            if (validatorProgram == null) {
+                loginAttemptTracker.recordFailure(remoteIp);
+                LOG.error("Response [{}] for user: {} from [{}] as 
ValidatorProgram is not defined in configuration.", GENERIC_FAILURE_RESPONSE, 
userName, remoteIp);
+                writer.println(GENERIC_FAILURE_RESPONSE);
+                writer.flush();
+            } else {
+                BufferedReader pReader;
+                PrintWriter    pWriter;
+                Process        p       = null;
+
+                try {
+                    p = Runtime.getRuntime().exec(validatorProgram);
+
+                    pReader = new BufferedReader(new 
InputStreamReader(p.getInputStream()));
+                    pWriter = new PrintWriter(new 
OutputStreamWriter(p.getOutputStream()));
+
+                    pWriter.println(request);
+                    pWriter.flush();
+
+                    String res = pReader.readLine();
+
+                    if (res != null && res.startsWith("OK")) {
+                        loginAttemptTracker.recordSuccess(remoteIp);
+                        if (adminRoleNames != null && adminUserList != null) {
+                            if (adminUserList.contains(userName)) {
+                                res = res + " " + adminRoleNames;
+                            }
+                        }
+                    } else {
+                        loginAttemptTracker.recordFailure(remoteIp);
+                        res = GENERIC_FAILURE_RESPONSE;
+                    }
+
+                    LOG.info("Response [{}] for user: {} from [{}]", res, 
userName, remoteIp);
+
+                    writer.println(res);
+                    writer.flush();
+                } finally {
+                    if (p != null) {
+                        p.destroy();
+                    }
+                }
+            }
+        } catch (Throwable t) {
+            loginAttemptTracker.recordFailure(remoteIp);
+            if (userName != null && writer != null) {
+                LOG.error("Response [{}] for user: {} from [{}], {}", 
GENERIC_FAILURE_RESPONSE, userName, remoteIp, t.getMessage());
+                writer.println(GENERIC_FAILURE_RESPONSE);
+                writer.flush();
+            }
+        } finally {
+            try {
+                if (client != null) {
+                    client.close();
+                }
+            } catch (IOException ioe) {
+                LOG.debug("Close socket failure. Detail: ", ioe);
+            } finally {
+                client = null;
+            }
+        }
+    }
 }
diff --git 
a/unixauthservice/src/main/java/org/apache/ranger/authentication/UnixAuthenticationService.java
 
b/unixauthservice/src/main/java/org/apache/ranger/authentication/UnixAuthenticationService.java
index d3d14a175..8c9f61238 100644
--- 
a/unixauthservice/src/main/java/org/apache/ranger/authentication/UnixAuthenticationService.java
+++ 
b/unixauthservice/src/main/java/org/apache/ranger/authentication/UnixAuthenticationService.java
@@ -19,6 +19,23 @@
 
 package org.apache.ranger.authentication;
 
+import org.apache.ranger.credentialapi.CredentialReader;
+import org.apache.ranger.plugin.util.XMLUtils;
+import org.apache.ranger.unixusersync.config.UserGroupSyncConfig;
+import org.apache.ranger.unixusersync.ha.UserSyncHAInitializerImpl;
+import org.apache.ranger.usergroupsync.UserGroupSync;
+import org.apache.ranger.usergroupsync.UserSyncMetricsProducer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -35,354 +52,307 @@
 import java.util.Properties;
 import java.util.Set;
 
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLServerSocket;
-import javax.net.ssl.SSLServerSocketFactory;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
+public class UnixAuthenticationService {
+    private static final Logger LOG = 
LoggerFactory.getLogger(UnixAuthenticationService.class);
 
-import org.apache.ranger.unixusersync.ha.UserSyncHAInitializerImpl;
-import org.apache.ranger.credentialapi.CredentialReader;
-import org.apache.ranger.plugin.util.XMLUtils;
-import org.apache.ranger.unixusersync.config.UserGroupSyncConfig;
-import org.apache.ranger.usergroupsync.UserGroupSync;
-import org.apache.ranger.usergroupsync.UserSyncMetricsProducer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+    private static final String serviceName                          = 
"UnixAuthenticationService";
+    private static final String SSL_ALGORITHM                        = 
"TLSv1.2";
+    private static final String REMOTE_LOGIN_AUTH_SERVICE_PORT_PARAM = 
"ranger.usersync.port";
+    private static final String SSL_KEYSTORE_PATH_PARAM              = 
"ranger.usersync.keystore.file";
+    private static final String SSL_TRUSTSTORE_PATH_PARAM            = 
"ranger.usersync.truststore.file";
+    private static final String SSL_KEYSTORE_FILE_TYPE_PARAM         = 
"ranger.keystore.file.type";
+    private static final String SSL_TRUSTSTORE_FILE_TYPE_PARAM       = 
"ranger.truststore.file.type";
+    private static final String SSL_KEYSTORE_PATH_PASSWORD_ALIAS     = 
"usersync.ssl.key.password";
+    private static final String SSL_TRUSTSTORE_PATH_PASSWORD_ALIAS   = 
"usersync.ssl.truststore.password";
+    private static final String CRED_VALIDATOR_PROG                  = 
"ranger.usersync.passwordvalidator.path";
+    private static final String ADMIN_USER_LIST_PARAM                = 
"admin.users";
+    private static final String ADMIN_ROLE_LIST_PARAM                = 
"admin.roleNames";
+    private static final String SSL_ENABLED_PARAM                    = 
"ranger.usersync.ssl";
+    private static final String CREDSTORE_FILENAME_PARAM             = 
"ranger.usersync.credstore.filename";
+    private static final String[] UGSYNC_CONFIG_XML_FILES            = 
{"ranger-ugsync-default.xml", "ranger-ugsync-site.xml"};
+    private static final String UNIXAUTH_MAX_FAILED_ATTEMPTS_PARAM   = 
"ranger.usersync.unixauth.max.failed.attempts";
+    private static final String UNIXAUTH_ATTEMPT_WINDOW_MS_PARAM     = 
"ranger.usersync.unixauth.attempt.window.ms";
+    private static final String UNIXAUTH_LOCKOUT_DURATION_MS_PARAM   = 
"ranger.usersync.unixauth.lockout.duration.ms";
+    private static final String UNIXAUTH_REQUIRE_CLIENT_AUTH_PARAM   = 
"ranger.usersync.unixauth.require.client.auth";
+    private static final String UNIXAUTH_SOCKET_TIMEOUT_MS_PARAM     = 
"ranger.usersync.unixauth.socket.timeout.ms";
 
-public class UnixAuthenticationService {
+    private static boolean enableUnixAuth;
+
+    private final List<String> adminUserList = new ArrayList<>();
+
+    private String adminRoleNames;
+    private String keyStorePath;
+    private String keyStorePathPassword;
+    private String keyStoreType;
+    private String trustStorePath;
+    private String trustStorePathPassword;
+    private String trustStoreType;
+    private List<String> enabledProtocolsList;
+    private List<String> enabledCipherSuiteList;
+    private UserSyncHAInitializerImpl userSyncHAInitializerImpl;
+    private int portNum;
+    private boolean sslEnabled;
+    private boolean requireClientAuth;
+    private LoginAttemptTracker loginAttemptTracker;
+    private int socketTimeoutMs;
+
+    public UnixAuthenticationService() {
+    }
+
+    public static void main(String[] args) {
+        enableUnixAuth = 
Arrays.stream(args).anyMatch("-enableUnixAuth"::equalsIgnoreCase);
+        UnixAuthenticationService service = new UnixAuthenticationService();
+        service.userSyncHAInitializerImpl = 
UserSyncHAInitializerImpl.getInstance(UserGroupSyncConfig.getInstance().getUserGroupConfig());
+        service.run();
+    }
+
+    public void run() {
+        try {
+            LOG.info("Starting User Sync Service!");
+            startUnixUserGroupSyncProcess();
+            Thread.sleep(5000);
+            if (enableUnixAuth) {
+                LOG.info("Enabling Unix Auth Service!");
+                init();
+                startService();
+            } else {
+                LOG.info("Unix Auth Service Disabled!");
+            }
+        } catch (Throwable t) {
+            LOG.error("ERROR: Service: {}", serviceName, t);
+        } finally {
+            LOG.info("Service: {} - STOPPED", serviceName);
+            if (this.userSyncHAInitializerImpl != null) {
+                LOG.info("Stopping curator leader latch service as main thread 
is closing");
+                this.userSyncHAInitializerImpl.stop();
+            }
+        }
+    }
+
+    public void startService() throws Throwable {
+        SSLContext context = SSLContext.getInstance(SSL_ALGORITHM);
 
-       private static final Logger LOG = 
LoggerFactory.getLogger(UnixAuthenticationService.class);
-
-       private static final String serviceName = "UnixAuthenticationService";
-
-       private static final String SSL_ALGORITHM = "TLSv1.2";
-       private static final String REMOTE_LOGIN_AUTH_SERVICE_PORT_PARAM = 
"ranger.usersync.port";
-
-       private static final String SSL_KEYSTORE_PATH_PARAM = 
"ranger.usersync.keystore.file";
-       private static final String SSL_TRUSTSTORE_PATH_PARAM = 
"ranger.usersync.truststore.file";
-
-       private static final String SSL_KEYSTORE_FILE_TYPE_PARAM = 
"ranger.keystore.file.type";
-       private static final String SSL_TRUSTSTORE_FILE_TYPE_PARAM = 
"ranger.truststore.file.type";
-
-       private static final String SSL_KEYSTORE_PATH_PASSWORD_ALIAS = 
"usersync.ssl.key.password";
-       private static final String SSL_TRUSTSTORE_PATH_PASSWORD_ALIAS = 
"usersync.ssl.truststore.password";
-
-       private static final String CRED_VALIDATOR_PROG = 
"ranger.usersync.passwordvalidator.path";
-       private static final String ADMIN_USER_LIST_PARAM = "admin.users";
-       private static final String ADMIN_ROLE_LIST_PARAM = "admin.roleNames";
-       private static final String SSL_ENABLED_PARAM = "ranger.usersync.ssl";
-       
-       private static final String CREDSTORE_FILENAME_PARAM = 
"ranger.usersync.credstore.filename";
-       
-       private String keyStorePath;
-       private String keyStoreType;
-       private List<String> enabledProtocolsList;
-       private List<String> enabledCipherSuiteList;
-       private String keyStorePathPassword;
-       private String trustStorePath;
-       private String trustStorePathPassword;
-       private String trustStoreType;
-       private List<String>  adminUserList = new ArrayList<String>();
-       private String adminRoleNames;
-       private UserSyncHAInitializerImpl userSyncHAInitializerImpl = null;
-       
-       private int  portNum;
-       
-       private boolean SSLEnabled = false;
-       
-       static private boolean enableUnixAuth = false;
-       
-       private static final String[] UGSYNC_CONFIG_XML_FILES = { 
"ranger-ugsync-default.xml",  "ranger-ugsync-site.xml" };
-
-       public static void main(String[] args) {
-               if (args.length > 0) {
-                       for (String s : args) {
-                               if ("-enableUnixAuth".equalsIgnoreCase(s)) {
-                                       enableUnixAuth = true;
-                                       break;
-                               }
-                       }
-               }
-               UnixAuthenticationService service = new 
UnixAuthenticationService();
-               service.userSyncHAInitializerImpl = 
UserSyncHAInitializerImpl.getInstance(UserGroupSyncConfig.getInstance().getUserGroupConfig());
-               service.run();
-       }
-
-       public UnixAuthenticationService() {
-       }
-
-       
-       public void run() {
-               try {
-                       LOG.info("Starting User Sync Service!");
-                       startUnixUserGroupSyncProcess();
-                       Thread.sleep(5000);
-                       if (enableUnixAuth) {
-                               LOG.info("Enabling Unix Auth Service!");
-                           init();
-                           startService();
-                       } else {
-                               LOG.info("Unix Auth Service Disabled!");
-                       }
-               }
-               catch(Throwable t) {
-                       LOG.error("ERROR: Service: " + serviceName , t);
-               }
-               finally {
-                       LOG.info("Service: " + serviceName + " - STOPPED.");
-                       if(this.userSyncHAInitializerImpl != null) {
-                               LOG.info("Stopping curator leader latch service 
as main thread is closing");
-                               this.userSyncHAInitializerImpl.stop();
-                       }
-               }
-       }
-
-       private void startUnixUserGroupSyncProcess() {
-               //
-               //  Start the synchronization service ...
-               //
-               LOG.info("Start : startUnixUserGroupSyncProcess " );
-               UserGroupSync syncProc = new UserGroupSync();
-               Thread newSyncProcThread = new Thread(syncProc);
-               newSyncProcThread.setName("UnixUserSyncThread");
-               // If this thread is set as daemon, then the entire process 
will terminate if enableUnixAuth is false
-        // Therefore this is marked as non-daemon thread. Don't change the 
following line
-               newSyncProcThread.setDaemon(false);
-               newSyncProcThread.start();
+        KeyManager[] km = null;
+
+        if (keyStorePath != null && !keyStorePath.isEmpty()) {
+            KeyStore ks = KeyStore.getInstance(keyStoreType);
+
+            try (InputStream in = getFileInputStream(keyStorePath)) {
+                if (keyStorePathPassword == null) {
+                    keyStorePathPassword = "";
+                }
+                ks.load(in, keyStorePathPassword.toCharArray());
+            }
+
+            KeyManagerFactory kmf = 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            kmf.init(ks, keyStorePathPassword.toCharArray());
+            km = kmf.getKeyManagers();
+        }
+
+        TrustManagerFactory trustManagerFactory = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+
+        KeyStore trustStoreKeyStore = null;
+
+        if (trustStorePath != null && !trustStorePath.isEmpty()) {
+            trustStoreKeyStore = KeyStore.getInstance(trustStoreType);
+
+            try (InputStream in = getFileInputStream(trustStorePath)) {
+                if (trustStorePathPassword == null) {
+                    trustStorePathPassword = "";
+                }
+                trustStoreKeyStore.load(in, 
trustStorePathPassword.toCharArray());
+            }
+        }
+
+        trustManagerFactory.init(trustStoreKeyStore);
+
+        TrustManager[] tm = trustManagerFactory.getTrustManagers();
+
+        SecureRandom random = new SecureRandom();
+
+        context.init(km, tm, random);
+
+        SSLServerSocketFactory sf = context.getServerSocketFactory();
+
+        try (ServerSocket socket = (sslEnabled ? 
sf.createServerSocket(portNum) : new ServerSocket(portNum))) {
+            if (sslEnabled) {
+                SSLServerSocket secureSocket     = (SSLServerSocket) socket;
+                String[]        protocols        = 
secureSocket.getEnabledProtocols();
+                Set<String>     allowedProtocols = new HashSet<>();
+                for (String ep : protocols) {
+                    if (enabledProtocolsList.contains(ep.toUpperCase())) {
+                        LOG.info("Enabling Protocol: [{}]", ep);
+                        allowedProtocols.add(ep);
+                    } else {
+                        LOG.info("Disabling Protocol: [{}]", ep);
+                    }
+                }
+
+                if (!allowedProtocols.isEmpty()) {
+                    
secureSocket.setEnabledProtocols(allowedProtocols.toArray(new String[0]));
+                }
+                String[]    enabledCipherSuites = 
secureSocket.getEnabledCipherSuites();
+                Set<String> allowedCipherSuites = new HashSet<>();
+                for (String enabledCipherSuite : enabledCipherSuites) {
+                    if (enabledCipherSuiteList.contains(enabledCipherSuite)) {
+                        LOG.debug("Enabling CipherSuite : [{}]", 
enabledCipherSuite);
+                        allowedCipherSuites.add(enabledCipherSuite);
+                    } else {
+                        LOG.debug("Disabling CipherSuite : [{}]", 
enabledCipherSuite);
+                    }
+                }
+                if (requireClientAuth) {
+                    if (trustStoreKeyStore == null) {
+                        throw new RuntimeException("[" + 
UNIXAUTH_REQUIRE_CLIENT_AUTH_PARAM + "] is enabled but [" + 
SSL_TRUSTSTORE_PATH_PARAM + "] is not configured - a truststore is required to 
validate client certificates.");
+                    }
+                    LOG.info("Enabling TLS client certificate authentication 
for UnixAuth listener.");
+                    secureSocket.setNeedClientAuth(true);
+                }
+                if (!allowedCipherSuites.isEmpty()) {
+                    
secureSocket.setEnabledCipherSuites(allowedCipherSuites.toArray(new String[0]));
+                }
+            }
+
+            Socket client;
+
+            try {
+                while ((client = socket.accept()) != null) {
+                    try {
+                        client.setSoTimeout(socketTimeoutMs);
+                    } catch (java.net.SocketException se) {
+                        LOG.warn("Failed to set socket timeout for connection 
from [{}]: {}", client.getInetAddress(), se.getMessage());
+                    }
+                    Thread clientValidatorThread = new Thread(new 
PasswordValidator(client, loginAttemptTracker));
+                    clientValidatorThread.start();
+                }
+            } catch (IOException e) {
+                socket.close();
+                throw (e);
+            }
+        }
+    }
+
+    private void startUnixUserGroupSyncProcess() {
+        //  Start user group synchronization service
+        LOG.info("Start : startUnixUserGroupSyncProcess ");
+        UserGroupSync syncProc          = new UserGroupSync();
+        Thread        newSyncProcThread = new Thread(syncProc);
+        newSyncProcThread.setName("UnixUserSyncThread");
+
+        /* If this thread is set as daemon, then the entire process will 
terminate if enableUnixAuth is false
+           Therefore this is marked as non-daemon thread. Don't change the 
following line
+         */
+        newSyncProcThread.setDaemon(false);
+        newSyncProcThread.start();
         LOG.info("UnixUserSyncThread started");
-        LOG.info("creating UserSyncMetricsProducer thread with default metrics 
location : "+System.getProperty("logdir"));
-               //Start the user sync metrics
-               boolean isUserSyncMetricsEnabled = 
UserGroupSyncConfig.getInstance().isUserSyncMetricsEnabled();
-               if (isUserSyncMetricsEnabled) {
-                       UserSyncMetricsProducer userSyncMetricsProducer = new 
UserSyncMetricsProducer();
-                       Thread userSyncMetricsProducerThread = new 
Thread(userSyncMetricsProducer);
-                       
userSyncMetricsProducerThread.setName("UserSyncMetricsProducerThread");
-                       userSyncMetricsProducerThread.setDaemon(false);
-                       userSyncMetricsProducerThread.start();
-                       LOG.info("UserSyncMetricsProducer started");
-               } else {
-                       LOG.info(" Ranger userSync metrics is not enabled");
-               }
-       }
-
-
-       //TODO: add more validation code
-       private void init() throws Throwable {
-               Properties prop = new Properties();
-
-               for (String fn : UGSYNC_CONFIG_XML_FILES ) {
+        LOG.info("creating UserSyncMetricsProducer thread with default metrics 
location : {}", System.getProperty("logdir"));
+
+        //Start the user sync metrics
+        boolean isUserSyncMetricsEnabled = 
UserGroupSyncConfig.getInstance().isUserSyncMetricsEnabled();
+        if (isUserSyncMetricsEnabled) {
+            UserSyncMetricsProducer userSyncMetricsProducer       = new 
UserSyncMetricsProducer();
+            Thread                  userSyncMetricsProducerThread = new 
Thread(userSyncMetricsProducer);
+            
userSyncMetricsProducerThread.setName("UserSyncMetricsProducerThread");
+            userSyncMetricsProducerThread.setDaemon(false);
+            userSyncMetricsProducerThread.start();
+            LOG.info("UserSyncMetricsProducer started");
+        } else {
+            LOG.info(" Ranger userSync metrics is not enabled");
+        }
+    }
+
+    //TODO: add more validation code
+    private void init() {
+        Properties prop = new Properties();
+
+        for (String fn : UGSYNC_CONFIG_XML_FILES) {
             XMLUtils.loadConfig(fn, prop);
-               }
-               
-               String credStoreFileName = 
prop.getProperty(CREDSTORE_FILENAME_PARAM);
-               
-               keyStorePath = prop.getProperty(SSL_KEYSTORE_PATH_PARAM);
-
-               keyStoreType = prop.getProperty(SSL_KEYSTORE_FILE_TYPE_PARAM, 
KeyStore.getDefaultType());
-               trustStoreType = 
prop.getProperty(SSL_TRUSTSTORE_FILE_TYPE_PARAM, KeyStore.getDefaultType());
-               
-               if (credStoreFileName == null) {
-                       throw new RuntimeException("Credential file is not 
defined. param = [" + CREDSTORE_FILENAME_PARAM + "]");
-               }
-               
-               File credFile = new File(credStoreFileName);
-               
-               if (! credFile.exists()) {
-                       throw new RuntimeException("Credential file [" + 
credStoreFileName + "]: does not exists." );
-               }
-               
-               if ( ! credFile.canRead() ) {
-                       throw new RuntimeException("Credential file [" + 
credStoreFileName + "]: can not be read." );
-               }
-               
-               if ("bcfks".equalsIgnoreCase(keyStoreType)) {
-                       String crendentialProviderPrefixBcfks= "bcfks" + 
"://file";
-                       credStoreFileName = crendentialProviderPrefixBcfks + 
credStoreFileName;
-               }
-               keyStorePathPassword = 
CredentialReader.getDecryptedString(credStoreFileName, 
SSL_KEYSTORE_PATH_PASSWORD_ALIAS, keyStoreType);
-               trustStorePathPassword = 
CredentialReader.getDecryptedString(credStoreFileName,SSL_TRUSTSTORE_PATH_PASSWORD_ALIAS,
 trustStoreType);
-               
-               trustStorePath  = prop.getProperty(SSL_TRUSTSTORE_PATH_PARAM);
-               portNum = 
Integer.parseInt(prop.getProperty(REMOTE_LOGIN_AUTH_SERVICE_PORT_PARAM));
-               String validatorProg = prop.getProperty(CRED_VALIDATOR_PROG);
-               if (validatorProg != null) {
-                       PasswordValidator.setValidatorProgram(validatorProg);
-               }
-               
-               String adminUsers = prop.getProperty(ADMIN_USER_LIST_PARAM);
-               
-               if (adminUsers != null && adminUsers.trim().length() > 0) {
-                       for(String u : adminUsers.split(",")) {
-                               LOG.info("Adding Admin User:"  + u.trim());
-                               adminUserList.add(u.trim());
-                       }
-                       PasswordValidator.setAdminUserList(adminUserList);
-               }
-               
-               
-               adminRoleNames = prop.getProperty(ADMIN_ROLE_LIST_PARAM);
-               
-               if (adminRoleNames != null) {
-                       LOG.info("Adding Admin Group:" + adminRoleNames);
-                       PasswordValidator.setAdminRoleNames(adminRoleNames);
-               }
-               
-               String SSLEnabledProp = prop.getProperty(SSL_ENABLED_PARAM);
-               
-               SSLEnabled = (SSLEnabledProp != null &&  
(SSLEnabledProp.equalsIgnoreCase("true")));
-               String defaultEnabledProtocols = "TLSv1.2";
-               String enabledProtocols = 
prop.getProperty("ranger.usersync.https.ssl.enabled.protocols", 
defaultEnabledProtocols);
-               String enabledCipherSuites = 
prop.getProperty("ranger.usersync.https.ssl.enabled.cipher.suites", "");
-               enabledProtocolsList=new 
ArrayList<String>(Arrays.asList(enabledProtocols.toUpperCase().trim().split("\\s*,\\s*")));
-               enabledCipherSuiteList = new 
ArrayList<String>(Arrays.asList(enabledCipherSuites.toUpperCase().trim().split("\\s*,\\s*")));
-//             LOG.info("Key:" + keyStorePath);
-//             LOG.info("KeyPassword:" + keyStorePathPassword);
-//             LOG.info("TrustStore:" + trustStorePath);
-//             LOG.info("PortNum:" + portNum);
-//             LOG.info("ValidatorProg:" + validatorProg);
-               
-       }
-       
-       
-       public void startService() throws Throwable {
-               
-               SSLContext context =  SSLContext.getInstance(SSL_ALGORITHM);
-               
-               KeyManager[] km = null;
-
-               if (keyStorePath != null && ! keyStorePath.isEmpty()) {
-                       KeyStore ks = KeyStore.getInstance(keyStoreType);
-               
-                       InputStream in = null;
-                       
-                       in = getFileInputStream(keyStorePath);
-                       
-                       try {
-                               if (keyStorePathPassword == null) {
-                                       keyStorePathPassword  = "";
-                               }
-                               ks.load(in, keyStorePathPassword.toCharArray());
-                       }
-                       finally {
-                               if (in != null) {
-                                       in.close();
-                               }
-                       }
-                       
-                       KeyManagerFactory kmf = 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
-                       kmf.init(ks, keyStorePathPassword.toCharArray());
-                       km = kmf.getKeyManagers();
-               }
-               
-               
-               TrustManagerFactory trustManagerFactory = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-               
-               KeyStore trustStoreKeyStore = null;
-               
-               if (trustStorePath != null && ! trustStorePath.isEmpty()) {
-                       trustStoreKeyStore = 
KeyStore.getInstance(trustStoreType);
-                       
-                       InputStream in = null;
-                       
-                       in = getFileInputStream(trustStorePath);
-                       
-                       try {
-                               if (trustStorePathPassword == null) {
-                                       trustStorePathPassword = "";
-                               }
-                               trustStoreKeyStore.load(in, 
trustStorePathPassword.toCharArray());
-                       }
-                       finally {
-                               if (in != null) {
-                                       in.close();
-                               }
-                       }
-               }
-               
-               trustManagerFactory.init(trustStoreKeyStore);
-               
-               TrustManager[] tm = trustManagerFactory.getTrustManagers();
-                               
-               SecureRandom random = new SecureRandom();
-               
-               context.init(km, tm, random);
-               
-               SSLServerSocketFactory sf = context.getServerSocketFactory();
-
-               try(ServerSocket socket = (SSLEnabled ? 
sf.createServerSocket(portNum) :  new ServerSocket(portNum) )) {
-
-                       if (SSLEnabled) {
-                               SSLServerSocket secureSocket     = 
(SSLServerSocket) socket;
-                               String[]        protocols        = 
secureSocket.getEnabledProtocols();
-                               Set<String>     allowedProtocols = new 
HashSet<String>();
-                               for (String ep : protocols) {
-                                       if 
(enabledProtocolsList.contains(ep.toUpperCase())) {
-                                               LOG.info("Enabling Protocol: [" 
+ ep + "]");
-                                               allowedProtocols.add(ep);
-                                       } else {
-                                               LOG.info("Disabling Protocol: 
[" + ep + "]");
-                                       }
-                               }
-
-                               if (!allowedProtocols.isEmpty()) {
-                                       
secureSocket.setEnabledProtocols(allowedProtocols.toArray(new String[0]));
-                               }
-                               String[]    enabledCipherSuites = 
secureSocket.getEnabledCipherSuites();
-                               Set<String> allowedCipherSuites = new 
HashSet<String>();
-                               for (String enabledCipherSuite : 
enabledCipherSuites) {
-                                       if 
(enabledCipherSuiteList.contains(enabledCipherSuite)) {
-                                               if (LOG.isDebugEnabled()) {
-                                                       LOG.debug("Enabling 
CipherSuite : [" + enabledCipherSuite + "]");
-                                               }
-                                               
allowedCipherSuites.add(enabledCipherSuite);
-                                       } else {
-                                               if (LOG.isDebugEnabled()) {
-                                                       LOG.debug("Disabling 
CipherSuite : [" + enabledCipherSuite + "]");
-                                               }
-                                       }
-                               }
-                               if (!allowedCipherSuites.isEmpty()) {
-                                       
secureSocket.setEnabledCipherSuites(allowedCipherSuites.toArray(new String[0]));
-                               }
-                       }
-
-
-                       Socket client = null;
-
-                       try {
-
-                               while ((client = socket.accept()) != null) {
-                                       Thread clientValidatorThread = new 
Thread(new PasswordValidator(client));
-                                       clientValidatorThread.start();
-                               }
-                       } catch (IOException e) {
-                               socket.close();
-                               throw (e);
-                       }
-               }
-
-       }
-       
-       private InputStream getFileInputStream(String path) throws 
FileNotFoundException {
-               
-               InputStream ret = null;
-               
-               File f = new File(path);
-               
-               if (f.exists()) {
-                       ret = new FileInputStream(f);
-               }
-               else {
-                       ret = getClass().getResourceAsStream(path);
-                       if (ret == null) {
-                               ret = 
getClass().getResourceAsStream(File.separator + path);
-                       }
-               }
-               
-               return ret;
-       }
+        }
+
+        String credStoreFileName = prop.getProperty(CREDSTORE_FILENAME_PARAM);
+        keyStorePath   = prop.getProperty(SSL_KEYSTORE_PATH_PARAM);
+        trustStorePath = prop.getProperty(SSL_TRUSTSTORE_PATH_PARAM);
+        keyStoreType   = prop.getProperty(SSL_KEYSTORE_FILE_TYPE_PARAM, 
KeyStore.getDefaultType());
+        trustStoreType = prop.getProperty(SSL_TRUSTSTORE_FILE_TYPE_PARAM, 
KeyStore.getDefaultType());
+
+        if (credStoreFileName == null) {
+            throw new RuntimeException("Credential file is not defined. param 
= [" + CREDSTORE_FILENAME_PARAM + "]");
+        }
+
+        File credFile = new File(credStoreFileName);
+
+        if (!credFile.exists()) {
+            throw new RuntimeException("Credential file [" + credStoreFileName 
+ "]: does not exists.");
+        }
+
+        if (!credFile.canRead()) {
+            throw new RuntimeException("Credential file [" + credStoreFileName 
+ "]: can not be read.");
+        }
+
+        if ("bcfks".equalsIgnoreCase(keyStoreType)) {
+            String crendentialProviderPrefixBcfks = "bcfks://file";
+            credStoreFileName = crendentialProviderPrefixBcfks + 
credStoreFileName;
+        }
+
+        keyStorePathPassword   = 
CredentialReader.getDecryptedString(credStoreFileName, 
SSL_KEYSTORE_PATH_PASSWORD_ALIAS, keyStoreType);
+        trustStorePathPassword = 
CredentialReader.getDecryptedString(credStoreFileName, 
SSL_TRUSTSTORE_PATH_PASSWORD_ALIAS, trustStoreType);
+        portNum                = 
Integer.parseInt(prop.getProperty(REMOTE_LOGIN_AUTH_SERVICE_PORT_PARAM));
+        String validatorProg   = prop.getProperty(CRED_VALIDATOR_PROG);
+
+        if (validatorProg != null) {
+            PasswordValidator.setValidatorProgram(validatorProg);
+        }
+
+        String adminUsers = prop.getProperty(ADMIN_USER_LIST_PARAM);
+
+        if (adminUsers != null && !adminUsers.trim().isEmpty()) {
+            for (String u : adminUsers.split(",")) {
+                LOG.info("Adding Admin User: {}", u.trim());
+                adminUserList.add(u.trim());
+            }
+            PasswordValidator.setAdminUserList(adminUserList);
+        }
+
+        adminRoleNames = prop.getProperty(ADMIN_ROLE_LIST_PARAM);
+
+        if (adminRoleNames != null) {
+            LOG.info("Adding Admin Group: {}", adminRoleNames);
+            PasswordValidator.setAdminRoleNames(adminRoleNames);
+        }
+
+        String sslEnabledProp = prop.getProperty(SSL_ENABLED_PARAM);
+
+        sslEnabled = (sslEnabledProp != null && 
(sslEnabledProp.equalsIgnoreCase("true")));
+        String defaultEnabledProtocols = "TLSv1.2";
+        String enabledProtocols        = 
prop.getProperty("ranger.usersync.https.ssl.enabled.protocols", 
defaultEnabledProtocols);
+        String enabledCipherSuites     = 
prop.getProperty("ranger.usersync.https.ssl.enabled.cipher.suites", "");
+        enabledProtocolsList           = new 
ArrayList<>(Arrays.asList(enabledProtocols.toUpperCase().trim().split("\\s*,\\s*")));
+        enabledCipherSuiteList         = new 
ArrayList<>(Arrays.asList(enabledCipherSuites.toUpperCase().trim().split("\\s*,\\s*")));
+        int  maxFailedAttempts = 
Integer.parseInt(prop.getProperty(UNIXAUTH_MAX_FAILED_ATTEMPTS_PARAM, "5"));
+        long attemptWindowMs   = 
Long.parseLong(prop.getProperty(UNIXAUTH_ATTEMPT_WINDOW_MS_PARAM, "60000"));
+        long lockoutDurationMs = 
Long.parseLong(prop.getProperty(UNIXAUTH_LOCKOUT_DURATION_MS_PARAM, "30000"));
+        socketTimeoutMs = 
Integer.parseInt(prop.getProperty(UNIXAUTH_SOCKET_TIMEOUT_MS_PARAM, "10000"));
+
+        loginAttemptTracker = new LoginAttemptTracker(maxFailedAttempts, 
attemptWindowMs, lockoutDurationMs);
+
+        String requireClientAuthProp = 
prop.getProperty(UNIXAUTH_REQUIRE_CLIENT_AUTH_PARAM, "false");
+        requireClientAuth = 
requireClientAuthProp.trim().equalsIgnoreCase("true");
+    }
+
+    private InputStream getFileInputStream(String path) throws 
FileNotFoundException {
+        InputStream ret;
+
+        File f = new File(path);
+
+        if (f.exists()) {
+            ret = new FileInputStream(f);
+        } else {
+            ret = getClass().getResourceAsStream(path);
+            if (ret == null) {
+                ret = getClass().getResourceAsStream(File.separator + path);
+            }
+        }
 
+        return ret;
+    }
 }
diff --git 
a/unixauthservice/src/test/java/org/apache/ranger/authentication/LoginAttemptTrackerTest.java
 
b/unixauthservice/src/test/java/org/apache/ranger/authentication/LoginAttemptTrackerTest.java
new file mode 100644
index 000000000..3ff5fb9ab
--- /dev/null
+++ 
b/unixauthservice/src/test/java/org/apache/ranger/authentication/LoginAttemptTrackerTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authentication;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class LoginAttemptTrackerTest {
+    @Test
+    public void allowsAttemptsBelowThreshold() {
+        LoginAttemptTracker tracker = new LoginAttemptTracker(5, 60_000, 
30_000);
+        String ip = "10.0.0.1";
+        for (int i = 0; i < 4; i++) {
+            assertFalse(tracker.isBlocked(ip), "should not be blocked before 
threshold");
+            tracker.recordFailure(ip);
+        }
+        assertFalse(tracker.isBlocked(ip), "still not blocked at 4 failures 
with threshold 5");
+    }
+
+    @Test
+    public void locksOutAfterThresholdReached() {
+        LoginAttemptTracker tracker = new LoginAttemptTracker(5, 60_000, 
30_000);
+        String ip = "10.0.0.2";
+        for (int i = 0; i < 5; i++) {
+            tracker.recordFailure(ip);
+        }
+        assertTrue(tracker.isBlocked(ip), "should be locked out at threshold");
+    }
+
+    @Test
+    public void successClearsFailureHistory() {
+        LoginAttemptTracker tracker = new LoginAttemptTracker(5, 60_000, 
30_000);
+        String ip = "10.0.0.3";
+        for (int i = 0; i < 4; i++) {
+            tracker.recordFailure(ip);
+        }
+        tracker.recordSuccess(ip);
+        assertFalse(tracker.isBlocked(ip));
+        // failure count should have reset - one more failure should not lock 
out
+        tracker.recordFailure(ip);
+        assertFalse(tracker.isBlocked(ip));
+    }
+
+    @Test
+    public void windowExpiryResetsFailureCount() throws InterruptedException {
+        // very short window so the test runs fast
+        LoginAttemptTracker tracker = new LoginAttemptTracker(3, 50, 30_000);
+        String ip = "10.0.0.4";
+        tracker.recordFailure(ip);
+        tracker.recordFailure(ip);
+        assertFalse(tracker.isBlocked(ip));
+        Thread.sleep(100); // let the window expire
+        tracker.recordFailure(ip); // should start a fresh window, count = 1
+        assertFalse(tracker.isBlocked(ip), "window should have reset the 
failure count");
+    }
+
+    @Test
+    public void reLocksImmediatelyIfRetriedRightAfterLockoutExpires() throws 
InterruptedException {
+        // short lockout, long window - so lockout expires but window doesn't
+        LoginAttemptTracker tracker = new LoginAttemptTracker(3, 60_000, 50);
+        String ip = "10.0.0.5";
+        tracker.recordFailure(ip);
+        tracker.recordFailure(ip);
+        tracker.recordFailure(ip);
+        assertTrue(tracker.isBlocked(ip));
+        Thread.sleep(100); // let the lockout expire, window is still active
+        assertFalse(tracker.isBlocked(ip), "lockout should have expired");
+        tracker.recordFailure(ip); // one more failure within the same window
+        assertTrue(tracker.isBlocked(ip), "should re-lock immediately since 
window's failure count carried over");
+    }
+
+    @Test
+    public void independentIpsTrackedSeparately() {
+        LoginAttemptTracker tracker = new LoginAttemptTracker(3, 60_000, 
30_000);
+        for (int i = 0; i < 3; i++) {
+            tracker.recordFailure("10.0.0.6");
+        }
+        assertTrue(tracker.isBlocked("10.0.0.6"));
+        assertFalse(tracker.isBlocked("10.0.0.7"), "a different source IP must 
not be affected");
+    }
+}
diff --git 
a/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java
 
b/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java
new file mode 100644
index 000000000..0c19a04fb
--- /dev/null
+++ 
b/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authentication;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for PasswordValidator
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestPasswordValidator {
+    private static LoginAttemptTracker permissiveTracker() {
+        return new LoginAttemptTracker(1000, 60_000, 30_000);
+    }
+
+    @Test
+    public void test00_staticGettersAndSetters() {
+        PasswordValidator.setValidatorProgram("/tmp/prog");
+        PasswordValidator.setAdminUserList(Collections.singletonList("root"));
+        PasswordValidator.setAdminRoleNames("ROLE_SUPER");
+
+        assertEquals("/tmp/prog", PasswordValidator.getValidatorProgram());
+        assertEquals(Collections.singletonList("root"), 
PasswordValidator.getAdminUserList());
+        assertEquals("ROLE_SUPER", PasswordValidator.getAdminRoleNames());
+    }
+
+    @Test
+    public void test01_nullProgramWritesFailedAndClosesSocket() throws 
Exception {
+        PasswordValidator.setValidatorProgram(null);
+        PasswordValidator.setAdminUserList(null);
+        PasswordValidator.setAdminRoleNames(null);
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+
+        new PasswordValidator(socket, permissiveTracker()).run();
+
+        String response = out.toString(StandardCharsets.UTF_8.name()).trim();
+        assertEquals("FAILED: Authentication failed.", response);
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test02_okResponseAppendsAdminRoleAndDestroysProcess() throws 
Exception {
+        PasswordValidator.setValidatorProgram("/bin/validator");
+        PasswordValidator.setAdminUserList(Arrays.asList("alice", "bob"));
+        PasswordValidator.setAdminRoleNames("ROLE_ADMIN");
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+
+        Process              process       = mock(Process.class);
+        ByteArrayInputStream processStdout = new 
ByteArrayInputStream("OK".getBytes(StandardCharsets.UTF_8));
+        when(process.getInputStream()).thenReturn(processStdout);
+        when(process.getOutputStream()).thenReturn(new 
ByteArrayOutputStream());
+
+        try (MockedStatic<Runtime> runtimeStatic = mockStatic(Runtime.class)) {
+            Runtime rt = mock(Runtime.class);
+            runtimeStatic.when(Runtime::getRuntime).thenReturn(rt);
+            when(rt.exec("/bin/validator")).thenReturn(process);
+
+            new PasswordValidator(socket, permissiveTracker()).run();
+        }
+
+        String response = out.toString(StandardCharsets.UTF_8).trim();
+        assertEquals("OK ROLE_ADMIN", response);
+        verify(process, times(1)).destroy();
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test03_okResponseWithoutAdminDoesNotAppendRole() throws 
Exception {
+        PasswordValidator.setValidatorProgram("/bin/validator");
+        PasswordValidator.setAdminUserList(Collections.singletonList("bob"));
+        PasswordValidator.setAdminRoleNames("ROLE_ADMIN");
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+
+        Process              process       = mock(Process.class);
+        ByteArrayInputStream processStdout = new 
ByteArrayInputStream("OK".getBytes(StandardCharsets.UTF_8));
+        when(process.getInputStream()).thenReturn(processStdout);
+        when(process.getOutputStream()).thenReturn(new 
ByteArrayOutputStream());
+
+        try (MockedStatic<Runtime> runtimeStatic = mockStatic(Runtime.class)) {
+            Runtime rt = mock(Runtime.class);
+            runtimeStatic.when(Runtime::getRuntime).thenReturn(rt);
+            when(rt.exec("/bin/validator")).thenReturn(process);
+
+            new PasswordValidator(socket, permissiveTracker()).run();
+        }
+
+        String response = out.toString(StandardCharsets.UTF_8.name()).trim();
+        assertEquals("OK", response);
+        verify(process, times(1)).destroy();
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test04_execThrowsWritesFailed() throws Exception {
+        PasswordValidator.setValidatorProgram("/bin/validator");
+        PasswordValidator.setAdminUserList(Collections.singletonList("alice"));
+        PasswordValidator.setAdminRoleNames("ROLE_ADMIN");
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+
+        try (MockedStatic<Runtime> runtimeStatic = mockStatic(Runtime.class)) {
+            Runtime rt = mock(Runtime.class);
+            runtimeStatic.when(Runtime::getRuntime).thenReturn(rt);
+            when(rt.exec("/bin/validator")).thenThrow(new IOException("boom"));
+
+            new PasswordValidator(socket, permissiveTracker()).run();
+        }
+
+        String response = out.toString(StandardCharsets.UTF_8).trim();
+        assertEquals("FAILED: Authentication failed.", response);
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test05_closeSocketIOExceptionHandled() throws Exception {
+        PasswordValidator.setValidatorProgram(null);
+        PasswordValidator.setAdminUserList(null);
+        PasswordValidator.setAdminRoleNames(null);
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+        doThrow(new IOException("close failure")).when(socket).close();
+
+        new PasswordValidator(socket, permissiveTracker()).run();
+
+        // No exception should be thrown even if close() fails; nothing to 
assert besides method completed
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test06_blockedIpSkipsValidatorAndReturnsLockedOutResponse() 
throws Exception {
+        PasswordValidator.setValidatorProgram("/bin/validator");
+        PasswordValidator.setAdminUserList(null);
+        PasswordValidator.setAdminRoleNames(null);
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        Socket socket = buildSocket("LOGIN: alice secret", out);
+        LoginAttemptTracker tracker = mock(LoginAttemptTracker.class);
+        when(tracker.isBlocked(anyString())).thenReturn(true);
+        new PasswordValidator(socket, tracker).run();
+        String response = out.toString(StandardCharsets.UTF_8).trim();
+        assertEquals("FAILED: Too many attempts. Try again later.", response);
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test07_failedValidatorResponseRecordsFailureOnTracker() throws 
Exception {
+        PasswordValidator.setValidatorProgram("/bin/validator");
+        PasswordValidator.setAdminUserList(null);
+        PasswordValidator.setAdminRoleNames(null);
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        Socket socket = buildSocket("LOGIN: alice secret", out);
+
+        Process process = mock(Process.class);
+        ByteArrayInputStream processStdout = new ByteArrayInputStream("FAILED: 
Password did not match.".getBytes(StandardCharsets.UTF_8));
+        when(process.getInputStream()).thenReturn(processStdout);
+        when(process.getOutputStream()).thenReturn(new 
ByteArrayOutputStream());
+
+        LoginAttemptTracker tracker = mock(LoginAttemptTracker.class);
+        when(tracker.isBlocked(anyString())).thenReturn(false);
+
+        try (MockedStatic<Runtime> runtimeStatic = mockStatic(Runtime.class)) {
+            Runtime rt = mock(Runtime.class);
+            runtimeStatic.when(Runtime::getRuntime).thenReturn(rt);
+            when(rt.exec("/bin/validator")).thenReturn(process);
+            new PasswordValidator(socket, tracker).run();
+        }
+
+        String response = out.toString(StandardCharsets.UTF_8).trim();
+        assertEquals("FAILED: Authentication failed.", response);
+        verify(tracker, times(1)).recordFailure(anyString());
+        verify(tracker, never()).recordSuccess(anyString());
+    }
+
+    @Test
+    public void test08_okValidatorResponseRecordsSuccessOnTracker() throws 
Exception {
+        PasswordValidator.setValidatorProgram("/bin/validator");
+        PasswordValidator.setAdminUserList(null);
+        PasswordValidator.setAdminRoleNames(null);
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        Socket socket = buildSocket("LOGIN: alice secret", out);
+
+        Process process = mock(Process.class);
+        ByteArrayInputStream processStdout = new 
ByteArrayInputStream("OK".getBytes(StandardCharsets.UTF_8));
+        when(process.getInputStream()).thenReturn(processStdout);
+        when(process.getOutputStream()).thenReturn(new 
ByteArrayOutputStream());
+
+        LoginAttemptTracker tracker = mock(LoginAttemptTracker.class);
+        when(tracker.isBlocked(anyString())).thenReturn(false);
+
+        try (MockedStatic<Runtime> runtimeStatic = mockStatic(Runtime.class)) {
+            Runtime rt = mock(Runtime.class);
+            runtimeStatic.when(Runtime::getRuntime).thenReturn(rt);
+            when(rt.exec("/bin/validator")).thenReturn(process);
+
+            new PasswordValidator(socket, tracker).run();
+        }
+
+        String response = out.toString(StandardCharsets.UTF_8).trim();
+        assertEquals("OK", response);
+        verify(tracker, times(1)).recordSuccess(anyString());
+        verify(tracker, never()).recordFailure(anyString());
+    }
+
+    private Socket buildSocket(String requestLine, ByteArrayOutputStream 
responseOut) throws IOException {
+        Socket       socket = mock(Socket.class);
+        InputStream  in     = new ByteArrayInputStream((requestLine + 
"\n").getBytes(StandardCharsets.UTF_8));
+        OutputStream out    = responseOut;
+        when(socket.getInputStream()).thenReturn(in);
+        when(socket.getOutputStream()).thenReturn(out);
+        return socket;
+    }
+}

Reply via email to