This is an automated email from the ASF dual-hosted git repository.
ramackri pushed a commit to branch ranger-2.9
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/ranger-2.9 by this push:
new 91cd5d972 RANGER-5690: Backport UnixAuth rate limiting to ranger-2.9
(#1098)
91cd5d972 is described below
commit 91cd5d972834bbaf1aa6ce0ebd97ea172b46ce19
Author: Ramachandran Krishnan <[email protected]>
AuthorDate: Tue Jul 21 22:23:26 2026 +0530
RANGER-5690: Backport UnixAuth rate limiting to ranger-2.9 (#1098)
* 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.
* Fix TestPasswordValidator for Java 8 CI compatibility
Use ByteArrayOutputStream.toString(String) instead of toString(Charset),
which is unavailable on Java 8.
---------
Co-authored-by: ramk <[email protected]>
---
.../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..90262e4f5
--- /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.name()).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.name()).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.name()).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.name()).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.name()).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;
+ }
+}