You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by ra...@apache.org on 2026/07/24 14:48:47 UTC
(ranger) branch ranger-2.9 updated: RANGER-5690: UnixAuth service lacks rate limiting on authentication attempts (#1106) (#1108)
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 f229b5ecb RANGER-5690: UnixAuth service lacks rate limiting on authentication attempts (#1106) (#1108)
f229b5ecb is described below
commit f229b5ecb17714bb4c8cf3dff6bc6c76a6e8736b
Author: Vyom Mani Tiwari <vy...@gmail.com>
AuthorDate: Fri Jul 24 20:18:43 2026 +0530
RANGER-5690: UnixAuth service lacks rate limiting on authentication attempts (#1106) (#1108)
(cherry picked from commit f133c2138963fbbff22a93965567db0021e3290d)
---
.../ranger/authentication/LoginAttemptTracker.java | 140 ++++++++++++++++--
.../ranger/authentication/PasswordValidator.java | 37 +++--
.../authentication/UnixAuthenticationService.java | 14 +-
.../authentication/LoginAttemptTrackerTest.java | 160 +++++++++++++++++++++
.../authentication/TestPasswordValidator.java | 9 +-
5 files changed, 334 insertions(+), 26 deletions(-)
diff --git a/unixauthservice/src/main/java/org/apache/ranger/authentication/LoginAttemptTracker.java b/unixauthservice/src/main/java/org/apache/ranger/authentication/LoginAttemptTracker.java
index b448a7b19..073bcb137 100644
--- a/unixauthservice/src/main/java/org/apache/ranger/authentication/LoginAttemptTracker.java
+++ b/unixauthservice/src/main/java/org/apache/ranger/authentication/LoginAttemptTracker.java
@@ -21,78 +21,196 @@
import java.util.Iterator;
import java.util.Map;
+import java.util.Set;
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 ConcurrentHashMap<String, IpRecord> attemptsByIp = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, AccountRecord> attemptsByAccount = new ConcurrentHashMap<>();
private final int maxFailures;
private final long windowMs;
private final long lockoutMs;
+
+ private final boolean accountFanoutEnabled;
+ private final int accountDistinctIpThreshold;
+ private final long accountWindowMs;
+ private final long accountBaseDelayMs;
+ private final long accountMaxDelayMs;
+
private volatile long lastCleanupTime;
public LoginAttemptTracker(int maxFailures, long windowMs, long lockoutMs) {
+ this(maxFailures, windowMs, lockoutMs, false, 0, 0, 0, 0);
+ }
+
+ public LoginAttemptTracker(int maxFailures, long windowMs, long lockoutMs,
+ boolean accountFanoutEnabled, int accountDistinctIpThreshold,
+ long accountWindowMs, long accountBaseDelayMs, long accountMaxDelayMs) {
this.maxFailures = maxFailures;
this.windowMs = windowMs;
this.lockoutMs = lockoutMs;
this.lastCleanupTime = System.currentTimeMillis();
+
+ this.accountFanoutEnabled = accountFanoutEnabled;
+ this.accountDistinctIpThreshold = accountDistinctIpThreshold;
+ this.accountWindowMs = accountWindowMs;
+ this.accountBaseDelayMs = accountBaseDelayMs;
+ this.accountMaxDelayMs = accountMaxDelayMs;
}
public boolean isBlocked(String sourceIp) {
- Record record = attemptsByIp.get(sourceIp);
+ IpRecord record = attemptsByIp.get(sourceIp);
+
if (record == null) {
return false;
}
+
return record.lockedUntil > System.currentTimeMillis();
}
+ public long getAccountDelayMs(String account) {
+ if (!accountFanoutEnabled || account == null || account.isEmpty()) {
+ return 0;
+ }
+
+ AccountRecord record = attemptsByAccount.get(account);
+
+ if (record == null) {
+ return 0;
+ }
+
+ int distinctIpCount;
+
+ synchronized (record) {
+ if (System.currentTimeMillis() - record.windowStart > accountWindowMs) {
+ return 0;
+ }
+
+ distinctIpCount = record.failingIps.size();
+ }
+
+ if (distinctIpCount <= accountDistinctIpThreshold) {
+ return 0;
+ }
+
+ long delay = accountBaseDelayMs * (distinctIpCount - accountDistinctIpThreshold);
+
+ return Math.min(delay, accountMaxDelayMs);
+ }
+
public void recordFailure(String sourceIp) {
+ recordFailure(sourceIp, null);
+ }
+
+ public void recordFailure(String sourceIp, String account) {
maybeCleanup();
+ recordIpFailure(sourceIp);
+
+ if (accountFanoutEnabled && account != null && !account.isEmpty()) {
+ recordAccountFailure(account, sourceIp);
+ }
+ }
+
+ private void recordIpFailure(String sourceIp) {
long now = System.currentTimeMillis();
- Record record = attemptsByIp.computeIfAbsent(sourceIp, k -> new Record(now));
+ IpRecord record = attemptsByIp.computeIfAbsent(sourceIp, k -> new IpRecord(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;
}
}
}
+ private void recordAccountFailure(String account, String sourceIp) {
+ long now = System.currentTimeMillis();
+ AccountRecord record = attemptsByAccount.computeIfAbsent(account, k -> new AccountRecord(now));
+
+ synchronized (record) {
+ if (now - record.windowStart > accountWindowMs) {
+ record.windowStart = now;
+ record.failingIps.clear();
+ }
+
+ record.lastActivity = now;
+ record.failingIps.add(sourceIp);
+ }
+ }
+
public void recordSuccess(String sourceIp) {
+ recordSuccess(sourceIp, null);
+ }
+
+ public void recordSuccess(String sourceIp, String account) {
attemptsByIp.remove(sourceIp);
+
+ if (accountFanoutEnabled && account != null && !account.isEmpty()) {
+ attemptsByAccount.remove(account);
+ }
}
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();
+ long ipCutoff = now - windowMs;
+ Iterator<Map.Entry<String, IpRecord>> ipIt = attemptsByIp.entrySet().iterator();
+ while (ipIt.hasNext()) {
+ IpRecord record = ipIt.next().getValue();
+
+ if (record.lastActivity < ipCutoff && record.lockedUntil <= now) {
+ ipIt.remove();
+ }
+ }
+
+ if (accountFanoutEnabled) {
+ long accountCutoff = now - accountWindowMs;
+ Iterator<Map.Entry<String, AccountRecord>> acctIt = attemptsByAccount.entrySet().iterator();
+ while (acctIt.hasNext()) {
+ AccountRecord record = acctIt.next().getValue();
+ if (record.lastActivity < accountCutoff) {
+ acctIt.remove();
+ }
}
}
}
- private static final class Record {
+ private static final class IpRecord {
final AtomicInteger failCount = new AtomicInteger(0);
volatile long windowStart;
volatile long lastActivity;
volatile long lockedUntil;
- Record(long now) {
+ IpRecord(long now) {
+ this.windowStart = now;
+ this.lastActivity = now;
+ }
+ }
+
+ private static final class AccountRecord {
+ final Set<String> failingIps = ConcurrentHashMap.newKeySet();
+ volatile long windowStart;
+ volatile long lastActivity;
+
+ AccountRecord(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 0416ca644..c3925dc72 100644
--- a/unixauthservice/src/main/java/org/apache/ranger/authentication/PasswordValidator.java
+++ b/unixauthservice/src/main/java/org/apache/ranger/authentication/PasswordValidator.java
@@ -83,6 +83,7 @@ public void run() {
reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
writer = new PrintWriter(new OutputStreamWriter(client.getOutputStream()));
String request = reader.readLine();
+ userName = parseUserName(request);
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);
@@ -90,22 +91,26 @@ public void run() {
return;
}
if (request == null) {
- loginAttemptTracker.recordFailure(remoteIp);
+ loginAttemptTracker.recordFailure(remoteIp, userName);
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();
+ long accountDelayMs = loginAttemptTracker.getAccountDelayMs(userName);
+
+ if (accountDelayMs > 0) {
+ LOG.warn("Applying {}ms delay for user [{}] from [{}] - failures against this account recently seen from multiple source IPs.", accountDelayMs, userName, remoteIp);
+
+ try {
+ Thread.sleep(accountDelayMs);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
}
}
if (validatorProgram == null) {
- loginAttemptTracker.recordFailure(remoteIp);
+ loginAttemptTracker.recordFailure(remoteIp, userName);
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();
@@ -126,14 +131,14 @@ public void run() {
String res = pReader.readLine();
if (res != null && res.startsWith("OK")) {
- loginAttemptTracker.recordSuccess(remoteIp);
+ loginAttemptTracker.recordSuccess(remoteIp, userName);
if (adminRoleNames != null && adminUserList != null) {
if (adminUserList.contains(userName)) {
res = res + " " + adminRoleNames;
}
}
} else {
- loginAttemptTracker.recordFailure(remoteIp);
+ loginAttemptTracker.recordFailure(remoteIp, userName);
res = GENERIC_FAILURE_RESPONSE;
}
@@ -148,7 +153,7 @@ public void run() {
}
}
} catch (Throwable t) {
- loginAttemptTracker.recordFailure(remoteIp);
+ loginAttemptTracker.recordFailure(remoteIp, userName);
if (userName != null && writer != null) {
LOG.error("Response [{}] for user: {} from [{}], {}", GENERIC_FAILURE_RESPONSE, userName, remoteIp, t.getMessage());
writer.println(GENERIC_FAILURE_RESPONSE);
@@ -166,4 +171,16 @@ public void run() {
}
}
}
+
+ private static String parseUserName(String request) {
+ if (request == null || !request.startsWith("LOGIN:")) {
+ return null;
+ }
+ String line = request.substring(6).trim();
+ int passwordAt = line.indexOf(' ');
+ if (passwordAt == -1) {
+ return null;
+ }
+ return line.substring(0, passwordAt).trim();
+ }
}
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 8c9f61238..d6e2511bb 100644
--- a/unixauthservice/src/main/java/org/apache/ranger/authentication/UnixAuthenticationService.java
+++ b/unixauthservice/src/main/java/org/apache/ranger/authentication/UnixAuthenticationService.java
@@ -75,6 +75,11 @@ public class UnixAuthenticationService {
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";
+ private static final String UNIXAUTH_ACCOUNT_FANOUT_ENABLED_PARAM = "ranger.usersync.unixauth.account.fanout.enabled";
+ private static final String UNIXAUTH_ACCOUNT_DISTINCT_IP_THRESHOLD_PARAM = "ranger.usersync.unixauth.account.distinct.ip.threshold";
+ private static final String UNIXAUTH_ACCOUNT_WINDOW_MS_PARAM = "ranger.usersync.unixauth.account.window.ms";
+ private static final String UNIXAUTH_ACCOUNT_BASE_DELAY_MS_PARAM = "ranger.usersync.unixauth.account.base.delay.ms";
+ private static final String UNIXAUTH_ACCOUNT_MAX_DELAY_MS_PARAM = "ranger.usersync.unixauth.account.max.delay.ms";
private static boolean enableUnixAuth;
@@ -333,7 +338,14 @@ private void init() {
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);
+ boolean accountFanoutEnabled = Boolean.parseBoolean(prop.getProperty(UNIXAUTH_ACCOUNT_FANOUT_ENABLED_PARAM, "true"));
+ int accountDistinctIpThreshold = Integer.parseInt(prop.getProperty(UNIXAUTH_ACCOUNT_DISTINCT_IP_THRESHOLD_PARAM, "3"));
+ long accountWindowMs = Long.parseLong(prop.getProperty(UNIXAUTH_ACCOUNT_WINDOW_MS_PARAM, "60000"));
+ long accountBaseDelayMs = Long.parseLong(prop.getProperty(UNIXAUTH_ACCOUNT_BASE_DELAY_MS_PARAM, "200"));
+ long accountMaxDelayMs = Long.parseLong(prop.getProperty(UNIXAUTH_ACCOUNT_MAX_DELAY_MS_PARAM, "2000"));
+
+ loginAttemptTracker = new LoginAttemptTracker(maxFailedAttempts, attemptWindowMs, lockoutDurationMs, accountFanoutEnabled,
+ accountDistinctIpThreshold, accountWindowMs, accountBaseDelayMs, accountMaxDelayMs);
String requireClientAuthProp = prop.getProperty(UNIXAUTH_REQUIRE_CLIENT_AUTH_PARAM, "false");
requireClientAuth = requireClientAuthProp.trim().equalsIgnoreCase("true");
diff --git a/unixauthservice/src/test/java/org/apache/ranger/authentication/LoginAttemptTrackerTest.java b/unixauthservice/src/test/java/org/apache/ranger/authentication/LoginAttemptTrackerTest.java
index 3ff5fb9ab..7671d0168 100644
--- a/unixauthservice/src/test/java/org/apache/ranger/authentication/LoginAttemptTrackerTest.java
+++ b/unixauthservice/src/test/java/org/apache/ranger/authentication/LoginAttemptTrackerTest.java
@@ -21,6 +21,7 @@
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -97,4 +98,163 @@ public void independentIpsTrackedSeparately() {
assertTrue(tracker.isBlocked("10.0.0.6"));
assertFalse(tracker.isBlocked("10.0.0.7"), "a different source IP must not be affected");
}
+
+ // threshold=3, base delay=200ms, max delay=2000ms, account window=60s
+ private static LoginAttemptTracker newFanoutTracker() {
+ return new LoginAttemptTracker(100, 60_000, 30_000, true, 3, 60_000, 200, 2000);
+ }
+
+ @Test
+ public void fanout_legitimateSingleIpNeverDelayed() {
+ LoginAttemptTracker tracker = newFanoutTracker();
+ // simulate a real admin mistyping their password 5 times from ONE ip
+ for (int i = 0; i < 5; i++) {
+ tracker.recordFailure("10.0.0.1", "alice");
+ }
+ assertEquals(0, tracker.getAccountDelayMs("alice"), "single-IP retries must never trigger an account delay");
+ }
+
+ @Test
+ public void fanout_distinctIpsAcrossThresholdTriggerDelay() {
+ LoginAttemptTracker tracker = newFanoutTracker();
+ tracker.recordFailure("10.0.0.1", "bob");
+ tracker.recordFailure("10.0.0.2", "bob");
+ assertEquals(0, tracker.getAccountDelayMs("bob"), "2 distinct IPs, threshold 3 - no delay yet");
+ tracker.recordFailure("10.0.0.3", "bob");
+ assertEquals(0, tracker.getAccountDelayMs("bob"), "3 distinct IPs, at threshold - still no delay");
+ tracker.recordFailure("10.0.0.4", "bob");
+ assertTrue(tracker.getAccountDelayMs("bob") > 0, "4th distinct IP crosses the threshold - delay should apply");
+ }
+
+ @Test
+ public void fanout_delayScalesLinearlyBeforeCap() {
+ LoginAttemptTracker tracker = newFanoutTracker();
+ String account = "carol";
+ for (int i = 0; i < 4; i++) {
+ tracker.recordFailure("10.0.1." + i, account); // 4 distinct -> 1 over threshold
+ }
+ assertEquals(200, tracker.getAccountDelayMs(account), "4 distinct IPs -> exactly 1x base delay");
+ tracker.recordFailure("10.0.1.4", account); // 5 distinct -> 2 over threshold
+ assertEquals(400, tracker.getAccountDelayMs(account), "5 distinct IPs -> exactly 2x base delay");
+ tracker.recordFailure("10.0.1.5", account); // 6 distinct -> 3 over threshold
+ assertEquals(600, tracker.getAccountDelayMs(account), "6 distinct IPs -> exactly 3x base delay");
+ tracker.recordFailure("10.0.1.6", account); // 7 distinct -> 4 over threshold
+ assertEquals(800, tracker.getAccountDelayMs(account), "7 distinct IPs -> exactly 4x base delay");
+ }
+
+ @Test
+ public void fanout_delayCapsInsteadOfGrowingUnbounded() {
+ LoginAttemptTracker tracker = newFanoutTracker();
+ String account = "carol_capped";
+ for (int i = 0; i < 30; i++) {
+ tracker.recordFailure("10.0.1." + i, account); // well past the threshold
+ }
+ assertEquals(2000, tracker.getAccountDelayMs(account), "delay must cap at accountMaxDelayMs regardless of distinct-IP count");
+ }
+
+ @Test
+ public void fanout_successClearsAccountStateImmediately() {
+ LoginAttemptTracker tracker = newFanoutTracker();
+ String account = "dave";
+ for (int i = 0; i < 5; i++) {
+ tracker.recordFailure("10.0.2." + i, account);
+ }
+ assertTrue(tracker.getAccountDelayMs(account) > 0, "delay should be present before success");
+ tracker.recordSuccess("10.0.2.99", account);
+ assertEquals(0, tracker.getAccountDelayMs(account), "delay must clear immediately after a successful login");
+ }
+
+ @Test
+ public void fanout_windowExpiryResetsAccountState() throws InterruptedException {
+ // short account window so the test runs fast
+ LoginAttemptTracker tracker = new LoginAttemptTracker(100, 60_000, 30_000, true, 2, 80, 200, 2000);
+ String account = "erin";
+ tracker.recordFailure("10.0.3.1", account);
+ tracker.recordFailure("10.0.3.2", account);
+ tracker.recordFailure("10.0.3.3", account); // 3 distinct, over threshold of 2 -> delay
+ assertTrue(tracker.getAccountDelayMs(account) > 0, "delay should be present within the window");
+ Thread.sleep(150); // let the account window expire
+ assertEquals(0, tracker.getAccountDelayMs(account), "delay must clear once the window has expired, even without a success");
+ }
+
+ @Test
+ public void fanout_disabledMeansAlwaysZero() {
+ // fan-out disabled entirely
+ LoginAttemptTracker tracker = new LoginAttemptTracker(100, 60_000, 30_000, false, 1, 60_000, 200, 2000);
+ String account = "frank";
+ for (int i = 0; i < 10; i++) {
+ tracker.recordFailure("10.0.4." + i, account);
+ }
+ assertEquals(0, tracker.getAccountDelayMs(account), "fan-out disabled must mean the delay is always 0, regardless of distinct IPs");
+ }
+
+ @Test
+ public void fanout_ipLockoutStillIndependentAndUnaffected() {
+ LoginAttemptTracker tracker = new LoginAttemptTracker(3, 60_000, 30_000, true, 2, 60_000, 200, 2000);
+ for (int i = 0; i < 3; i++) {
+ tracker.recordFailure("10.0.5.1", "grace"); // same IP, 3 failures -> IP lockout still fires
+ }
+ assertTrue(tracker.isBlocked("10.0.5.1"), "existing per-IP hard lockout must still work unchanged alongside account fan-out tracking");
+ }
+
+ @Test
+ public void fanout_sameIpRetriedManyTimesCountsOnce() {
+ LoginAttemptTracker tracker = newFanoutTracker();
+ String account = "henry";
+ for (int i = 0; i < 20; i++) {
+ tracker.recordFailure("10.0.6.1", account); // SAME ip, 20 times
+ }
+ assertEquals(0, tracker.getAccountDelayMs(account), "20 retries from one IP must never cross the distinct-IP threshold");
+ }
+
+ @Test
+ public void fanout_independentAccountsDoNotCrossContaminate() {
+ LoginAttemptTracker tracker = newFanoutTracker();
+ for (int i = 0; i < 10; i++) {
+ tracker.recordFailure("10.0.8." + i, "jack"); // fan-out attack on "jack"
+ }
+ assertTrue(tracker.getAccountDelayMs("jack") > 0, "the attacked account should be delayed");
+ assertEquals(0, tracker.getAccountDelayMs("kate"), "an unrelated account must be completely unaffected");
+ }
+
+ @Test
+ public void fanout_nullOrEmptyAccountIsSafeNoOp() {
+ LoginAttemptTracker tracker = newFanoutTracker();
+ // simulates PasswordValidator's request==null / unparsable-request path
+ for (int i = 0; i < 10; i++) {
+ tracker.recordFailure("10.0.9." + i); // 1-arg overload, no account at all
+ }
+ assertEquals(0, tracker.getAccountDelayMs(null), "no-account failures must never populate any account delay");
+ assertEquals(0, tracker.getAccountDelayMs(""), "empty-string account must also always be a no-op");
+ tracker.recordFailure("10.0.9.1", ""); // explicit empty string account
+ tracker.recordFailure("10.0.9.1", null); // explicit null account
+ assertEquals(0, tracker.getAccountDelayMs(""), "explicit empty/null account args must never throw and never trigger a delay");
+ }
+
+ @Test
+ public void fanout_concurrentFailuresFromManyIpsAreThreadSafe() throws InterruptedException {
+ // Mirrors PasswordValidator's real threading model: one thread per
+ // connection, all potentially hitting the same account concurrently.
+ LoginAttemptTracker tracker = newFanoutTracker();
+ String account = "concurrent_victim";
+ int threadCount = 50;
+ Thread[] threads = new Thread[threadCount];
+ for (int i = 0; i < threadCount; i++) {
+ final String ip = "10.1.0." + i; // 50 distinct IPs
+ threads[i] = new Thread(() -> tracker.recordFailure(ip, account));
+ }
+ for (Thread t : threads) {
+ t.start();
+ }
+ for (Thread t : threads) {
+ t.join(5000);
+ }
+ // 50 distinct IPs, threshold 3 -> 47 over threshold -> would be 47*200=9400ms
+ // uncapped, so this must land exactly at the configured cap.
+ assertEquals(2000, tracker.getAccountDelayMs(account), "50 concurrent distinct-IP failures must land exactly at the cap, with no race corruption");
+ // an unrelated account, hit concurrently at the same time, must still
+ // read back as unaffected - proves no cross-account leakage under
+ // concurrent access either.
+ assertEquals(0, tracker.getAccountDelayMs("bystander"), "an untouched account must remain unaffected during concurrent activity");
+ }
}
diff --git a/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java b/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java
index 90262e4f5..af959fd9f 100644
--- a/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java
+++ b/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java
@@ -216,13 +216,14 @@ public void test07_failedValidatorResponseRecordsFailureOnTracker() throws Excep
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());
+ verify(tracker, times(1)).recordFailure(anyString(), anyString());
+ verify(tracker, never()).recordSuccess(anyString(), anyString());
}
@Test
@@ -252,8 +253,8 @@ public void test08_okValidatorResponseRecordsSuccessOnTracker() throws Exception
String response = out.toString(StandardCharsets.UTF_8.name()).trim();
assertEquals("OK", response);
- verify(tracker, times(1)).recordSuccess(anyString());
- verify(tracker, never()).recordFailure(anyString());
+ verify(tracker, times(1)).recordSuccess(anyString(), anyString());
+ verify(tracker, never()).recordFailure(anyString(), anyString());
}
private Socket buildSocket(String requestLine, ByteArrayOutputStream responseOut) throws IOException {