jt2594838 commented on code in PR #16494:
URL: https://github.com/apache/iotdb/pull/16494#discussion_r2384478821
##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/SessionManager.java:
##########
@@ -248,6 +256,24 @@ public BasicOpenSessionResp login(
return openSessionResp;
}
+ User user = authorityFetcher.get().getUser(username);
+ boolean enableLoginLock = user != null;
Review Comment:
Explain this. Why is a null user associated with the enabling of the login
lock.
##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/auth/LoginLockManager.java:
##########
@@ -0,0 +1,371 @@
+/*
+ * 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.iotdb.db.auth;
+
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Deque;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.ConcurrentMap;
+
+public class LoginLockManager {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(LoginLockManager.class);
+
+ // Configuration parameters
+ private final int failedLoginAttempts;
+ private final int failedLoginAttemptsPerUser;
+ private final int passwordLockTimeMinutes;
+
+ // Lock records storage (in-memory only)
+ private final ConcurrentMap<Long, UserLockInfo> userLocks = new
ConcurrentHashMap<>();
+ private final ConcurrentMap<String, UserLockInfo> userIpLocks = new
ConcurrentHashMap<>();
+
+ // Exempt users who should never be locked (only valid if request is from
local host)
+ private final Set<Long> exemptUsers;
+
+ public LoginLockManager() {
+ this(
+ IoTDBDescriptor.getInstance().getConfig().getFailedLoginAttempts(),
+
IoTDBDescriptor.getInstance().getConfig().getFailedLoginAttemptsPerUser(),
+
IoTDBDescriptor.getInstance().getConfig().getPasswordLockTimeMinutes());
+ }
+
+ public LoginLockManager(
+ int failedLoginAttempts, int failedLoginAttemptsPerUser, int
passwordLockTimeMinutes) {
+ // Initialize exempt users
+ this.exemptUsers = new HashSet<>();
+ this.exemptUsers.add(10000L); // root
+
+ // Set and validate failedLoginAttempts (IP level)
+ if (failedLoginAttempts == -1) {
+ this.failedLoginAttempts = -1; // Completely disable IP-level
restrictions
+ } else {
+ this.failedLoginAttempts = failedLoginAttempts >= 1 ?
failedLoginAttempts : 5;
+ }
+
+ // Set and validate failedLoginAttemptsPerUser (user level)
+ if (failedLoginAttemptsPerUser == -1) {
+ // If IP-level is enabled, user-level cannot be disabled
+ if (this.failedLoginAttempts != -1) {
+ this.failedLoginAttemptsPerUser = 1000; // Default user-level value
+ LOGGER.error(
+ "User-level login attempts cannot be disabled when IP-level is
enabled. "
+ + "Setting user-level attempts to default (1000)");
+ } else {
+ this.failedLoginAttemptsPerUser = -1; // Both are disabled
+ }
+ } else {
+ this.failedLoginAttemptsPerUser =
+ failedLoginAttemptsPerUser >= 1 ? failedLoginAttemptsPerUser : 1000;
+ }
+
+ // Set and validate passwordLockTimeMinutes (default 10, minimum 1)
+ this.passwordLockTimeMinutes = passwordLockTimeMinutes >= 1 ?
passwordLockTimeMinutes : 10;
+
+ // Log final effective configuration
+ LOGGER.info(
+ "Login lock manager initialized with: IP-level attempts={}, User-level
attempts={}, Lock time={} minutes",
+ this.failedLoginAttempts == -1 ? "disabled" : this.failedLoginAttempts,
+ this.failedLoginAttemptsPerUser == -1 ? "disabled" :
this.failedLoginAttemptsPerUser,
+ this.passwordLockTimeMinutes);
+ }
+
+ /** Inner class to store user lock information */
+ static class UserLockInfo {
+ // Deque to store timestamps of failed attempts (milliseconds)
+ private final Deque<Long> failureTimestamps = new
ConcurrentLinkedDeque<>();
+
+ void addFailureTime(long timestamp) {
+ failureTimestamps.addLast(timestamp);
+ }
+
+ void removeOldFailures(long cutoffTime) {
+ // Remove timestamps older than cutoffTime
+ while (!failureTimestamps.isEmpty() && failureTimestamps.peekFirst() <
cutoffTime) {
+ failureTimestamps.pollFirst();
+ }
+ }
+
+ int getFailureCount() {
+ return failureTimestamps.size();
+ }
+ }
+
+ /**
+ * Check if user or user@ip is locked
+ *
+ * @param userId user ID
+ * @param ip IP address
+ * @return true if locked, false otherwise
+ */
+ public boolean checkLock(long userId, String ip) {
+ cleanExpiredLocks(); // Clean expired records (no failures in window)
+
+ // Exempt users are never locked if request is from localhost
+ if (exemptUsers.contains(userId) && isFromLocalhost(ip)) {
+ return false;
+ }
+
+ // Check user@ip lock (failures in window)
+ String userIpKey = buildUserIpKey(userId, ip);
+ UserLockInfo userIpLock = userIpLocks.get(userIpKey);
+ if (userIpLock != null) {
+ long now = System.currentTimeMillis();
+ long cutoffTime = now - (passwordLockTimeMinutes * 60 * 1000L);
+ userIpLock.removeOldFailures(cutoffTime);
+ if (userIpLock.getFailureCount() >= failedLoginAttempts) {
+ return true;
+ }
+ }
+
+ // Check global user lock (failures in window)
+ UserLockInfo userLock = userLocks.get(userId);
+ if (userLock != null) {
+ long now = System.currentTimeMillis();
+ long cutoffTime = now - (passwordLockTimeMinutes * 60 * 1000L);
+ userLock.removeOldFailures(cutoffTime);
+ return userLock.getFailureCount() >= failedLoginAttemptsPerUser;
+ }
+
+ return false;
+ }
+
+ /**
+ * Record a failed login attempt
+ *
+ * @param userId user ID
+ * @param ip IP address
+ */
+ public void recordFailure(long userId, String ip) {
+ // Exempt users from localhost don't get locked
+ if (exemptUsers.contains(userId) && isFromLocalhost(ip)) {
+ return;
+ }
+
+ long now = System.currentTimeMillis();
+ long cutoffTime = now - (passwordLockTimeMinutes * 60 * 1000L);
+
+ // Handle user@ip failures in sliding window
+ String userIpKey = buildUserIpKey(userId, ip);
+ userIpLocks.compute(
+ userIpKey,
+ (key, existing) -> {
+ if (existing == null) {
+ existing = new UserLockInfo();
+ }
+ // Remove failures outside of sliding window
+ existing.removeOldFailures(cutoffTime);
+ // Record this failure
+ existing.addFailureTime(now);
+ // Check if threshold reached (log only when it just reaches)
+ int failCountIp = existing.getFailureCount();
+ if (failCountIp >= failedLoginAttempts && failCountIp ==
failedLoginAttempts) {
+ LOGGER.info("IP '{}' locked for user ID '{}'", ip, userId);
+ }
+ return existing;
+ });
+
+ // Handle global user failures in sliding window
+ userLocks.compute(
+ userId,
+ (key, existing) -> {
+ if (existing == null) {
+ existing = new UserLockInfo();
+ }
+ // Remove failures outside of sliding window
+ existing.removeOldFailures(cutoffTime);
+ // Record this failure
+ existing.addFailureTime(now);
+ // Check if threshold reached (log only when it just reaches)
+ int failCountUser = existing.getFailureCount();
+ if (failCountUser >= failedLoginAttemptsPerUser
+ && failCountUser == failedLoginAttemptsPerUser) {
+ LOGGER.info(
+ "User ID '{}' locked due to {} failed attempts",
+ userId,
+ failedLoginAttemptsPerUser);
+ }
+ return existing;
+ });
+
+ // Check for potential attacks
+ checkForPotentialAttacks(userId, ip);
+ }
+
+ /**
+ * Clear failure records after successful login
+ *
+ * @param userId user ID
+ * @param ip IP address
+ */
+ public void clearFailure(long userId, String ip) {
+ String userIpKey = buildUserIpKey(userId, ip);
+ userIpLocks.remove(userIpKey);
+ userLocks.remove(userId);
+ }
+
+ /**
+ * Unlock user or user@ip
+ *
+ * @param userId user ID (required)
+ * @param ip IP address (optional)
+ */
+ public void unlock(long userId, String ip) {
+ if (ip == null || ip.isEmpty()) {
+ // Unlock global user lock
+ userLocks.remove(userId);
+ // Also remove all IP locks for this user
+ userIpLocks.keySet().removeIf(key -> key.startsWith(userId + "@"));
+ LOGGER.info("User ID '{}' unlocked (manual)", userId);
+ } else {
+ // Unlock specific user@ip lock
+ String userIpKey = buildUserIpKey(userId, ip);
+ userIpLocks.remove(userIpKey);
+ LOGGER.info("IP '{}' for user ID '{}' unlocked (manual)", ip, userId);
+ }
+ }
+
+ /** Clean up expired locks (no failures in the sliding window) */
+ public void cleanExpiredLocks() {
+ long now = System.currentTimeMillis();
+ long cutoffTime = now - (passwordLockTimeMinutes * 60 * 1000L);
+
+ // Clean expired user locks
+ userLocks
+ .entrySet()
+ .removeIf(
+ entry -> {
+ UserLockInfo info = entry.getValue();
+ // Remove outdated failures
+ info.removeOldFailures(cutoffTime);
+ if (info.getFailureCount() == 0) {
+ LOGGER.info("User ID '{}' unlocked (expired)", entry.getKey());
+ return true;
+ }
+ return false;
+ });
+
+ // Clean expired user@ip locks
+ userIpLocks
+ .entrySet()
+ .removeIf(
+ entry -> {
+ UserLockInfo info = entry.getValue();
+ // Remove outdated failures
+ info.removeOldFailures(cutoffTime);
+ if (info.getFailureCount() == 0) {
+ String[] parts = entry.getKey().split("@");
+ LOGGER.info("IP '{}' for user ID '{}' unlocked (expired)",
parts[1], parts[0]);
+ return true;
+ }
+ return false;
+ });
+ }
+
+ // Helper methods
+ private String buildUserIpKey(long userId, String ip) {
+ return userId + "@" + ip;
+ }
+
+ private void checkForPotentialAttacks(long userId, String ip) {
+ // Check if IP is locked by many users
+ Set<Long> usersForIp = new HashSet<>();
+ for (String key : userIpLocks.keySet()) {
+ if (key.endsWith("@" + ip)) {
+ usersForIp.add(Long.parseLong(key.split("@")[0]));
+ }
+ }
+
+ if (usersForIp.size() > 50) {
+ LOGGER.warn("IP '{}' locked by {} different users → potential attack",
ip, usersForIp.size());
+ }
+
+ // Check if user has many IP locks
+ Set<String> ipsForUser = new HashSet<>();
+ for (String key : userIpLocks.keySet()) {
+ if (key.startsWith(userId + "@")) {
+ ipsForUser.add(key.split("@")[1]);
+ }
+ }
+
+ if (ipsForUser.size() > 100) {
+ LOGGER.warn("User ID '{}' has {} IP locks → potential attack", userId,
ipsForUser.size());
+ }
+ }
+
+ public static LoginLockManager getInstance() {
+ return LoginLockManagerHelper.INSTANCE;
+ }
+
+ private static class LoginLockManagerHelper {
+ private static final LoginLockManager INSTANCE = new LoginLockManager();
+
+ private LoginLockManagerHelper() {}
+ }
+
+ /**
+ * Check if an IP address belongs to localhost (loopback or any local
network interface).
+ *
+ * @param ip The IP address as string.
+ * @return true if the IP is local, false otherwise. Note: Network interface
addresses are
+ * reacquired each time to account for possible address changes.
+ */
+ private boolean isFromLocalhost(String ip) {
+ try {
+ if (ip == null || ip.isEmpty()) {
+ return false;
+ }
+ InetAddress remote = InetAddress.getByName(ip);
+
+ // Case 1: Explicit loopback address (127.0.0.1 or ::1)
+ if (remote.isLoopbackAddress()) {
+ return true;
+ }
+
+ // Case 2: Compare against all local network interface addresses
+ Enumeration<NetworkInterface> nics =
NetworkInterface.getNetworkInterfaces();
+ while (nics.hasMoreElements()) {
+ NetworkInterface nic = nics.nextElement();
+ if (!nic.isUp()) {
+ continue; // Skip inactive interfaces
+ }
+ Enumeration<InetAddress> addrs = nic.getInetAddresses();
+ while (addrs.hasMoreElements()) {
+ InetAddress localAddr = addrs.nextElement();
+ if (remote.equals(localAddr)) {
+ return true; // Remote address matches one of the local addresses
+ }
+ }
+ }
+ } catch (Exception e) {
+ return false; // In case of error, assume non-local
Review Comment:
Better to leave a warn log
##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/auth/LoginLockManager.java:
##########
@@ -0,0 +1,371 @@
+/*
+ * 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.iotdb.db.auth;
+
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Deque;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.ConcurrentMap;
+
+public class LoginLockManager {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(LoginLockManager.class);
+
+ // Configuration parameters
+ private final int failedLoginAttempts;
+ private final int failedLoginAttemptsPerUser;
+ private final int passwordLockTimeMinutes;
+
+ // Lock records storage (in-memory only)
+ private final ConcurrentMap<Long, UserLockInfo> userLocks = new
ConcurrentHashMap<>();
+ private final ConcurrentMap<String, UserLockInfo> userIpLocks = new
ConcurrentHashMap<>();
+
+ // Exempt users who should never be locked (only valid if request is from
local host)
+ private final Set<Long> exemptUsers;
+
+ public LoginLockManager() {
+ this(
+ IoTDBDescriptor.getInstance().getConfig().getFailedLoginAttempts(),
+
IoTDBDescriptor.getInstance().getConfig().getFailedLoginAttemptsPerUser(),
+
IoTDBDescriptor.getInstance().getConfig().getPasswordLockTimeMinutes());
+ }
+
+ public LoginLockManager(
+ int failedLoginAttempts, int failedLoginAttemptsPerUser, int
passwordLockTimeMinutes) {
+ // Initialize exempt users
+ this.exemptUsers = new HashSet<>();
+ this.exemptUsers.add(10000L); // root
Review Comment:
Use a constant; if there is none, ask the related person to add one.
##########
iotdb-core/antlr/src/main/antlr4/org/apache/iotdb/db/qp/sql/SqlLexer.g4:
##########
@@ -1226,6 +1234,10 @@ DOUBLE_COLON: '::';
* 5. Literals
*/
+USERNAME_WITH_HOST
+ : [a-zA-Z_] [a-zA-Z_0-9]* '@' '\'' .*? '\''
+ ;
Review Comment:
<img width="633" height="60" alt="image"
src="https://github.com/user-attachments/assets/baf7c950-e24f-4704-aabe-67f8ce697f1c"
/>
This will not do. We support more strange names.
##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/auth/ClusterAuthorityFetcher.java:
##########
@@ -420,13 +422,51 @@ private void onOperatePermissionSuccess(Object plan) {
@Override
public SettableFuture<ConfigTaskResult> operatePermission(AuthorStatement
authorStatement) {
- return operatePermissionInternal(authorStatement, false);
+ return handleAccountUnlock(
+ authorStatement,
+ authorStatement.getUserName(),
+ false,
+ () -> onOperatePermissionSuccess(authorStatement));
}
@Override
public SettableFuture<ConfigTaskResult> operatePermission(
RelationalAuthorStatement authorStatement) {
- return operatePermissionInternal(authorStatement, true);
+ return handleAccountUnlock(
+ authorStatement,
+ authorStatement.getUserName(),
+ true,
+ () -> onOperatePermissionSuccess(authorStatement));
+ }
+
+ private SettableFuture<ConfigTaskResult> handleAccountUnlock(
+ Object authorStatement, String username, boolean isRelational, Runnable
successCallback) {
+
+ if (isUnlockStatement(authorStatement, isRelational)) {
+ SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+ User user = getUser(username);
+ if (user == null) {
+ future.setException(new IoTDBException(String.format("User %s Not
Found", username), 701));
Review Comment:
Use a constant for the status code.
And better to define an explicit exception.
##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/auth/LoginLockManager.java:
##########
@@ -0,0 +1,371 @@
+/*
+ * 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.iotdb.db.auth;
+
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Deque;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.ConcurrentMap;
+
+public class LoginLockManager {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(LoginLockManager.class);
+
+ // Configuration parameters
+ private final int failedLoginAttempts;
+ private final int failedLoginAttemptsPerUser;
+ private final int passwordLockTimeMinutes;
+
+ // Lock records storage (in-memory only)
+ private final ConcurrentMap<Long, UserLockInfo> userLocks = new
ConcurrentHashMap<>();
+ private final ConcurrentMap<String, UserLockInfo> userIpLocks = new
ConcurrentHashMap<>();
+
+ // Exempt users who should never be locked (only valid if request is from
local host)
+ private final Set<Long> exemptUsers;
+
+ public LoginLockManager() {
+ this(
+ IoTDBDescriptor.getInstance().getConfig().getFailedLoginAttempts(),
+
IoTDBDescriptor.getInstance().getConfig().getFailedLoginAttemptsPerUser(),
+
IoTDBDescriptor.getInstance().getConfig().getPasswordLockTimeMinutes());
+ }
+
+ public LoginLockManager(
+ int failedLoginAttempts, int failedLoginAttemptsPerUser, int
passwordLockTimeMinutes) {
+ // Initialize exempt users
+ this.exemptUsers = new HashSet<>();
+ this.exemptUsers.add(10000L); // root
+
+ // Set and validate failedLoginAttempts (IP level)
+ if (failedLoginAttempts == -1) {
+ this.failedLoginAttempts = -1; // Completely disable IP-level
restrictions
+ } else {
+ this.failedLoginAttempts = failedLoginAttempts >= 1 ?
failedLoginAttempts : 5;
+ }
+
+ // Set and validate failedLoginAttemptsPerUser (user level)
+ if (failedLoginAttemptsPerUser == -1) {
+ // If IP-level is enabled, user-level cannot be disabled
+ if (this.failedLoginAttempts != -1) {
+ this.failedLoginAttemptsPerUser = 1000; // Default user-level value
+ LOGGER.error(
+ "User-level login attempts cannot be disabled when IP-level is
enabled. "
+ + "Setting user-level attempts to default (1000)");
+ } else {
+ this.failedLoginAttemptsPerUser = -1; // Both are disabled
+ }
+ } else {
+ this.failedLoginAttemptsPerUser =
+ failedLoginAttemptsPerUser >= 1 ? failedLoginAttemptsPerUser : 1000;
+ }
+
+ // Set and validate passwordLockTimeMinutes (default 10, minimum 1)
+ this.passwordLockTimeMinutes = passwordLockTimeMinutes >= 1 ?
passwordLockTimeMinutes : 10;
+
+ // Log final effective configuration
+ LOGGER.info(
+ "Login lock manager initialized with: IP-level attempts={}, User-level
attempts={}, Lock time={} minutes",
+ this.failedLoginAttempts == -1 ? "disabled" : this.failedLoginAttempts,
+ this.failedLoginAttemptsPerUser == -1 ? "disabled" :
this.failedLoginAttemptsPerUser,
+ this.passwordLockTimeMinutes);
+ }
+
+ /** Inner class to store user lock information */
+ static class UserLockInfo {
+ // Deque to store timestamps of failed attempts (milliseconds)
+ private final Deque<Long> failureTimestamps = new
ConcurrentLinkedDeque<>();
+
+ void addFailureTime(long timestamp) {
+ failureTimestamps.addLast(timestamp);
+ }
+
+ void removeOldFailures(long cutoffTime) {
+ // Remove timestamps older than cutoffTime
+ while (!failureTimestamps.isEmpty() && failureTimestamps.peekFirst() <
cutoffTime) {
+ failureTimestamps.pollFirst();
+ }
+ }
+
+ int getFailureCount() {
+ return failureTimestamps.size();
+ }
+ }
+
+ /**
+ * Check if user or user@ip is locked
+ *
+ * @param userId user ID
+ * @param ip IP address
+ * @return true if locked, false otherwise
+ */
+ public boolean checkLock(long userId, String ip) {
+ cleanExpiredLocks(); // Clean expired records (no failures in window)
Review Comment:
Where is it checked that the functionality is disabled? (i.e., the
confiuration == -1)
The same for recordFailure
##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/auth/LoginLockManager.java:
##########
@@ -0,0 +1,371 @@
+/*
+ * 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.iotdb.db.auth;
+
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Deque;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.ConcurrentMap;
+
+public class LoginLockManager {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(LoginLockManager.class);
+
+ // Configuration parameters
+ private final int failedLoginAttempts;
+ private final int failedLoginAttemptsPerUser;
+ private final int passwordLockTimeMinutes;
+
+ // Lock records storage (in-memory only)
+ private final ConcurrentMap<Long, UserLockInfo> userLocks = new
ConcurrentHashMap<>();
+ private final ConcurrentMap<String, UserLockInfo> userIpLocks = new
ConcurrentHashMap<>();
+
+ // Exempt users who should never be locked (only valid if request is from
local host)
+ private final Set<Long> exemptUsers;
+
+ public LoginLockManager() {
+ this(
+ IoTDBDescriptor.getInstance().getConfig().getFailedLoginAttempts(),
+
IoTDBDescriptor.getInstance().getConfig().getFailedLoginAttemptsPerUser(),
+
IoTDBDescriptor.getInstance().getConfig().getPasswordLockTimeMinutes());
+ }
+
+ public LoginLockManager(
+ int failedLoginAttempts, int failedLoginAttemptsPerUser, int
passwordLockTimeMinutes) {
+ // Initialize exempt users
+ this.exemptUsers = new HashSet<>();
+ this.exemptUsers.add(10000L); // root
+
+ // Set and validate failedLoginAttempts (IP level)
+ if (failedLoginAttempts == -1) {
+ this.failedLoginAttempts = -1; // Completely disable IP-level
restrictions
+ } else {
+ this.failedLoginAttempts = failedLoginAttempts >= 1 ?
failedLoginAttempts : 5;
+ }
+
+ // Set and validate failedLoginAttemptsPerUser (user level)
+ if (failedLoginAttemptsPerUser == -1) {
+ // If IP-level is enabled, user-level cannot be disabled
+ if (this.failedLoginAttempts != -1) {
+ this.failedLoginAttemptsPerUser = 1000; // Default user-level value
+ LOGGER.error(
+ "User-level login attempts cannot be disabled when IP-level is
enabled. "
+ + "Setting user-level attempts to default (1000)");
+ } else {
+ this.failedLoginAttemptsPerUser = -1; // Both are disabled
+ }
+ } else {
+ this.failedLoginAttemptsPerUser =
+ failedLoginAttemptsPerUser >= 1 ? failedLoginAttemptsPerUser : 1000;
+ }
+
+ // Set and validate passwordLockTimeMinutes (default 10, minimum 1)
+ this.passwordLockTimeMinutes = passwordLockTimeMinutes >= 1 ?
passwordLockTimeMinutes : 10;
+
+ // Log final effective configuration
+ LOGGER.info(
+ "Login lock manager initialized with: IP-level attempts={}, User-level
attempts={}, Lock time={} minutes",
+ this.failedLoginAttempts == -1 ? "disabled" : this.failedLoginAttempts,
+ this.failedLoginAttemptsPerUser == -1 ? "disabled" :
this.failedLoginAttemptsPerUser,
+ this.passwordLockTimeMinutes);
+ }
+
+ /** Inner class to store user lock information */
+ static class UserLockInfo {
+ // Deque to store timestamps of failed attempts (milliseconds)
+ private final Deque<Long> failureTimestamps = new
ConcurrentLinkedDeque<>();
Review Comment:
I do not think it is necessary to record every failure timestamp.
You only need to record the last failure timestamp and the number of failed
attempts.
##########
iotdb-core/datanode/src/test/java/org/apache/iotdb/db/auth/LoginLockManagerTest.java:
##########
@@ -0,0 +1,543 @@
+/*
+ * 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.iotdb.db.auth;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.net.InetAddress;
+import java.util.Deque;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class LoginLockManagerTest {
+
+ private LoginLockManager lockManager;
+ private static final long TEST_USER_ID = 1001L;
+ private static final long OTHER_USER_ID = 2002L;
+ private static final String TEST_IP = "192.168.1.1";
+ private static final String ANOTHER_IP = "10.0.0.1";
+ private static final long EXEMPT_USER_ID = 10000L; // root
+
+ private final int failedLoginAttempts = 3;
+ private final int failedLoginAttemptsPerUser = 5;
+ private final int passwordLockTimeMinutes = 1;
+
+ @Before
+ public void setUp() {
+ lockManager =
+ new LoginLockManager(
+ failedLoginAttempts, failedLoginAttemptsPerUser,
passwordLockTimeMinutes);
+ }
+
+ // -------------------- Configuration --------------------
+ @Test
+ public void testAllConfigScenarios() {
+ // 1. Test normal configuration with recommended defaults
+ LoginLockManager normal = new LoginLockManager(5, 1000, 10);
+ assertEquals(5, getField(normal, "failedLoginAttempts"));
+ assertEquals(1000, getField(normal, "failedLoginAttemptsPerUser"));
+ assertEquals(10, getField(normal, "passwordLockTimeMinutes"));
+
+ // 2. Test disabling both IP-level and user-level restrictions
+ LoginLockManager disableBoth = new LoginLockManager(-1, -1, 10);
+ assertEquals(-1, getField(disableBoth, "failedLoginAttempts"));
+ assertEquals(-1, getField(disableBoth, "failedLoginAttemptsPerUser"));
+
+ // 3. Test enabling only user-level restriction
+ LoginLockManager userOnly = new LoginLockManager(-1, 5, 10);
+ assertEquals(-1, getField(userOnly, "failedLoginAttempts"));
+ assertEquals(5, getField(userOnly, "failedLoginAttemptsPerUser"));
+
+ // 4. Test that enabling IP-level automatically enables user-level with
default value
+ LoginLockManager ipEnable = new LoginLockManager(3, -1, 10);
+ assertEquals(3, getField(ipEnable, "failedLoginAttempts"));
+ assertEquals(1000, getField(ipEnable, "failedLoginAttemptsPerUser"));
+
+ // 5. Test invalid IP attempts value falls back to default (5)
+ LoginLockManager invalidIp = new LoginLockManager(0, -1, 10);
+ assertEquals(5, getField(invalidIp, "failedLoginAttempts"));
+
+ // 6. Test invalid user attempts value falls back to default (1000)
+ LoginLockManager invalidUser = new LoginLockManager(-1, 0, 10);
+ assertEquals(1000, getField(invalidUser, "failedLoginAttemptsPerUser"));
+
+ // 7. Test invalid lock time value falls back to default (10 minutes)
+ LoginLockManager invalidTime = new LoginLockManager(5, 1000, 0);
+ assertEquals(10, getField(invalidTime, "passwordLockTimeMinutes"));
+ }
+
+ private int getField(LoginLockManager manager, String fieldName) {
+ try {
+ java.lang.reflect.Field field =
LoginLockManager.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return (int) field.get(manager);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // ---------------- Basic Functionality ----------------
+
+ @Test
+ public void testInitialStateNotLocked() {
+ assertFalse("New user should not be locked",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testSingleFailureNotLocked() {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ assertFalse("Single failure should not lock",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testIpLockAfterMaxAttempts() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(
+ "User@IP should be locked after max attempts",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testGlobalUserLockAfterMaxAttempts() {
+ for (int i = 0; i < failedLoginAttemptsPerUser; i++) {
+ lockManager.recordFailure(TEST_USER_ID, "ip" + i);
+ }
+ assertTrue(
+ "User should be locked globally after max attempts",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testExemptUsersLocalIpNeverLocked() throws Exception {
+ // Use loopback as local IP
+ String localIp = InetAddress.getLoopbackAddress().getHostAddress();
+
+ for (int i = 0; i < 20; i++) {
+ lockManager.recordFailure(EXEMPT_USER_ID, localIp);
+ }
+ assertFalse(
+ "Exempt user with local IP should never be locked",
+ lockManager.checkLock(EXEMPT_USER_ID, localIp));
+ }
+
+ @Test
+ public void testExemptUsersRemoteIpCanBeLocked() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(EXEMPT_USER_ID, TEST_IP);
+ }
+ assertTrue(
+ "Exempt user with non-local IP should still be locked",
+ lockManager.checkLock(EXEMPT_USER_ID, TEST_IP));
+ }
+
+ // -------------------- User-Level Locking --------------------
+ @Test
+ public void testUserLevelOLocking() {
+ LoginLockManager userOnlyManager = new LoginLockManager(-1, 10, 1);
+
+ for (int i = 0; i < 10; i++) {
+ userOnlyManager.recordFailure(TEST_USER_ID, "ip" + i); // Different IPs
each time
+ }
+ // Should be locked
+ assertTrue(userOnlyManager.checkLock(TEST_USER_ID, null));
+ for (int i = 0; i < 10; i++) {
+ assertTrue(userOnlyManager.checkLock(TEST_USER_ID, "ip" + i)); //
Different IPs each time
+ }
+ }
+
+ // ---------------- Unlock Logic ----------------
+
+ @Test
+ public void testManualUnlock() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(lockManager.checkLock(TEST_USER_ID, TEST_IP));
+
+ lockManager.unlock(TEST_USER_ID, TEST_IP);
+ assertFalse(
+ "User should be unlocked after manual unlock",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testGlobalManualUnlock() {
+ for (int i = 0; i < failedLoginAttemptsPerUser; i++) {
+ lockManager.recordFailure(TEST_USER_ID, "ip" + i);
+ }
+ assertTrue(lockManager.checkLock(TEST_USER_ID, TEST_IP));
+
+ lockManager.unlock(TEST_USER_ID, null);
+ assertFalse(
+ "User should be unlocked after manual global unlock",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testClearFailureResetsCounters() {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+
+ lockManager.clearFailure(TEST_USER_ID, TEST_IP);
+
+ for (int i = 0; i < failedLoginAttempts - 1; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertFalse(
+ "Counters should be reset after clear",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testEmptyIpInUnlock() {
+ lockManager.unlock(TEST_USER_ID, "");
+ // no exception expected
+ }
+
+ // ---------------- Multi-user Multi-IP ----------------
+
+ @Test
+ public void testDifferentUsersAreIndependent() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ assertFalse("Other user should not be locked",
lockManager.checkLock(OTHER_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testDifferentIpsCountSeparately() {
+ for (int i = 0; i < failedLoginAttempts - 1; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ for (int i = 0; i < failedLoginAttempts - 1; i++) {
+ lockManager.recordFailure(TEST_USER_ID, ANOTHER_IP);
+ }
+ assertFalse(
+ "Neither IP should lock user individually",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ // ---------------- Cleaning Logic ----------------
+
+ @Test
+ public void testCleanExpiredLocks() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(lockManager.checkLock(TEST_USER_ID, TEST_IP));
+
+ try {
+ Field field = LoginLockManager.class.getDeclaredField("userIpLocks");
+ field.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ ConcurrentMap<String, LoginLockManager.UserLockInfo> userIpLocks =
+ (ConcurrentMap<String, LoginLockManager.UserLockInfo>)
field.get(lockManager);
+
+ LoginLockManager.UserLockInfo lockInfo = userIpLocks.get(TEST_USER_ID +
"@" + TEST_IP);
+
+ Field tsField =
LoginLockManager.UserLockInfo.class.getDeclaredField("failureTimestamps");
+ tsField.setAccessible(true);
+
+ @SuppressWarnings("unchecked")
+ Deque<Long> timestamps = (Deque<Long>) tsField.get(lockInfo);
+
+ timestamps.clear();
+ timestamps.add(System.currentTimeMillis() - (passwordLockTimeMinutes *
60 * 1000L) - 5000);
+
+ } catch (Exception e) {
+ fail("Failed to modify failure timestamps via reflection: " +
e.getMessage());
+ }
+
+ lockManager.cleanExpiredLocks();
+ assertFalse(
+ "User should be unlocked after cleaning",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testNotCleanIfRecentFailureExists() {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ lockManager.cleanExpiredLocks();
+ assertFalse(
+ "Should still keep recent failure record",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ // ---------------- Sliding Window Logic ----------------
+
+ @Test
+ public void testLockWithinTimeWindow() throws InterruptedException {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ Thread.sleep(200); // small delay but still within window
+ }
+ assertTrue(
+ "User should be locked within time window",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testFailuresOutsideWindowNotCounted() throws Exception {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+
+ // hack timestamps to simulate old failure
+ try {
+ Field field = LoginLockManager.class.getDeclaredField("userIpLocks");
+ field.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ ConcurrentMap<String, LoginLockManager.UserLockInfo> userIpLocks =
+ (ConcurrentMap<String, LoginLockManager.UserLockInfo>)
field.get(lockManager);
+
+ LoginLockManager.UserLockInfo lockInfo = userIpLocks.get(TEST_USER_ID +
"@" + TEST_IP);
+
+ Field tsField =
LoginLockManager.UserLockInfo.class.getDeclaredField("failureTimestamps");
+ tsField.setAccessible(true);
+
+ @SuppressWarnings("unchecked")
+ Deque<Long> timestamps = (Deque<Long>) tsField.get(lockInfo);
+
+ timestamps.clear();
+ timestamps.add(System.currentTimeMillis() - (passwordLockTimeMinutes *
60 * 1000L) - 5000);
+
+ } catch (Exception e) {
+ fail("Reflection modification failed: " + e.getMessage());
+ }
+
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ assertFalse(
+ "Old failures should not count toward new lock",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ // ---------------- Configuration and Invalid Input ----------------
+
+ @Test
+ public void testInvalidConfigurationDefaults() {
+ LoginLockManager invalidConfigManager = new LoginLockManager(-1, -1, -1);
+ for (int i = 0; i < 5; i++) {
+ invalidConfigManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(
+ "Should use default config when invalid values provided",
+ invalidConfigManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testNullIpHandling() {
+ lockManager.recordFailure(TEST_USER_ID, null);
+ assertFalse("Null IP should not cause exception",
lockManager.checkLock(TEST_USER_ID, null));
+ }
+
+ @Test
+ public void testNegativeUserId() {
+ long negativeUserId = -123L;
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(negativeUserId, TEST_IP);
+ }
+ assertTrue(
+ "Negative user ID should still lock",
lockManager.checkLock(negativeUserId, TEST_IP));
+ }
+
+ // ---------------- Concurrency Stress Tests ----------------
+
+ @Test
+ public void testConcurrentFailuresCauseLock() throws InterruptedException {
+ // Test configuration: 10 threads each making 2 failure attempts
+ int threadCount = 10;
+ int attemptsPerThread = 2;
+ int totalAttempts = threadCount * attemptsPerThread; // Should exceed lock
threshold
+
+ // Task definition: each thread records multiple failures
+ Runnable recordTask =
+ () -> {
+ for (int i = 0; i < attemptsPerThread; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ };
+
+ // Thread management: create and start all threads
+ Thread[] threads = new Thread[threadCount];
+ for (int i = 0; i < threadCount; i++) {
+ threads[i] = new Thread(recordTask);
+ threads[i].start();
+ }
+
+ // Synchronization: wait for all threads to complete
+ for (Thread t : threads) {
+ t.join();
+ }
+
+ // Verification: should be locked after concurrent attempts
+ assertTrue(
+ "System should be locked after " + totalAttempts + " concurrent
failure attempts",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testConcurrentDifferentUsersIndependent() throws
InterruptedException {
+ // Test configuration: 500 test users with threshold attempts each
+ int userCount = 500; // Large number to stress test isolation
+ int attemptsPerUser = failedLoginAttempts; // Exactly reaching threshold
+
+ // Task definition: simulate failures across multiple users
+ Runnable userFailureTask =
+ () -> {
+ for (int u = 0; u < userCount; u++) {
+ for (int i = 0; i < attemptsPerUser; i++) {
+ lockManager.recordFailure(TEST_USER_ID + u, TEST_IP);
+ }
+ }
+ };
+
+ // Concurrent execution: two threads working on the same user set
+ Thread t1 = new Thread(userFailureTask);
+ Thread t2 = new Thread(userFailureTask);
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+
+ // Verification: each user should be locked independently
+ for (int u = 0; u < userCount; u++) {
+ assertTrue(
+ "User " + u + " should be locked independently without affecting
others",
+ lockManager.checkLock(TEST_USER_ID + u, TEST_IP));
+ }
+ }
+
+ @Test
+ public void testConcurrentClearAndFailure() throws InterruptedException {
+ // Setup: create locked state first
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(
+ "Precondition: user should be locked before concurrency test",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+
+ // Task definitions:
+ // Clear task: resets the failure count
+ Runnable clearTask =
+ () -> {
+ lockManager.clearFailure(TEST_USER_ID, TEST_IP);
+ // Artificial delay to increase race condition probability
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ }
+ };
+
+ // Failure task: records additional failures
+ Runnable failTask =
+ () -> {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ }
+ };
+
+ // Concurrent execution: run clear and record operations simultaneously
+ Thread clearThread = new Thread(clearTask);
+ Thread failThread = new Thread(failTask);
+ clearThread.start();
+ failThread.start();
+ clearThread.join();
+ failThread.join();
+
+ // Validation: system should be in consistent state
+ boolean finalState = lockManager.checkLock(TEST_USER_ID, TEST_IP);
+ assertTrue(
+ "Final state should be either consistently locked or unlocked",
finalState || !finalState);
Review Comment:
I think "finalState || !finalState" is always true.
##########
iotdb-core/datanode/src/test/java/org/apache/iotdb/db/auth/LoginLockManagerTest.java:
##########
@@ -0,0 +1,543 @@
+/*
+ * 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.iotdb.db.auth;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.net.InetAddress;
+import java.util.Deque;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class LoginLockManagerTest {
+
+ private LoginLockManager lockManager;
+ private static final long TEST_USER_ID = 1001L;
+ private static final long OTHER_USER_ID = 2002L;
+ private static final String TEST_IP = "192.168.1.1";
+ private static final String ANOTHER_IP = "10.0.0.1";
+ private static final long EXEMPT_USER_ID = 10000L; // root
+
+ private final int failedLoginAttempts = 3;
+ private final int failedLoginAttemptsPerUser = 5;
+ private final int passwordLockTimeMinutes = 1;
+
+ @Before
+ public void setUp() {
+ lockManager =
+ new LoginLockManager(
+ failedLoginAttempts, failedLoginAttemptsPerUser,
passwordLockTimeMinutes);
+ }
+
+ // -------------------- Configuration --------------------
+ @Test
+ public void testAllConfigScenarios() {
+ // 1. Test normal configuration with recommended defaults
+ LoginLockManager normal = new LoginLockManager(5, 1000, 10);
+ assertEquals(5, getField(normal, "failedLoginAttempts"));
+ assertEquals(1000, getField(normal, "failedLoginAttemptsPerUser"));
+ assertEquals(10, getField(normal, "passwordLockTimeMinutes"));
+
+ // 2. Test disabling both IP-level and user-level restrictions
+ LoginLockManager disableBoth = new LoginLockManager(-1, -1, 10);
+ assertEquals(-1, getField(disableBoth, "failedLoginAttempts"));
+ assertEquals(-1, getField(disableBoth, "failedLoginAttemptsPerUser"));
+
+ // 3. Test enabling only user-level restriction
+ LoginLockManager userOnly = new LoginLockManager(-1, 5, 10);
+ assertEquals(-1, getField(userOnly, "failedLoginAttempts"));
+ assertEquals(5, getField(userOnly, "failedLoginAttemptsPerUser"));
+
+ // 4. Test that enabling IP-level automatically enables user-level with
default value
+ LoginLockManager ipEnable = new LoginLockManager(3, -1, 10);
+ assertEquals(3, getField(ipEnable, "failedLoginAttempts"));
+ assertEquals(1000, getField(ipEnable, "failedLoginAttemptsPerUser"));
+
+ // 5. Test invalid IP attempts value falls back to default (5)
+ LoginLockManager invalidIp = new LoginLockManager(0, -1, 10);
+ assertEquals(5, getField(invalidIp, "failedLoginAttempts"));
+
+ // 6. Test invalid user attempts value falls back to default (1000)
+ LoginLockManager invalidUser = new LoginLockManager(-1, 0, 10);
+ assertEquals(1000, getField(invalidUser, "failedLoginAttemptsPerUser"));
+
+ // 7. Test invalid lock time value falls back to default (10 minutes)
+ LoginLockManager invalidTime = new LoginLockManager(5, 1000, 0);
+ assertEquals(10, getField(invalidTime, "passwordLockTimeMinutes"));
+ }
+
+ private int getField(LoginLockManager manager, String fieldName) {
+ try {
+ java.lang.reflect.Field field =
LoginLockManager.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return (int) field.get(manager);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // ---------------- Basic Functionality ----------------
+
+ @Test
+ public void testInitialStateNotLocked() {
+ assertFalse("New user should not be locked",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testSingleFailureNotLocked() {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ assertFalse("Single failure should not lock",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testIpLockAfterMaxAttempts() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(
+ "User@IP should be locked after max attempts",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testGlobalUserLockAfterMaxAttempts() {
+ for (int i = 0; i < failedLoginAttemptsPerUser; i++) {
+ lockManager.recordFailure(TEST_USER_ID, "ip" + i);
+ }
+ assertTrue(
+ "User should be locked globally after max attempts",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testExemptUsersLocalIpNeverLocked() throws Exception {
+ // Use loopback as local IP
+ String localIp = InetAddress.getLoopbackAddress().getHostAddress();
+
+ for (int i = 0; i < 20; i++) {
+ lockManager.recordFailure(EXEMPT_USER_ID, localIp);
+ }
+ assertFalse(
+ "Exempt user with local IP should never be locked",
+ lockManager.checkLock(EXEMPT_USER_ID, localIp));
+ }
+
+ @Test
+ public void testExemptUsersRemoteIpCanBeLocked() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(EXEMPT_USER_ID, TEST_IP);
+ }
+ assertTrue(
+ "Exempt user with non-local IP should still be locked",
+ lockManager.checkLock(EXEMPT_USER_ID, TEST_IP));
+ }
+
+ // -------------------- User-Level Locking --------------------
+ @Test
+ public void testUserLevelOLocking() {
+ LoginLockManager userOnlyManager = new LoginLockManager(-1, 10, 1);
+
+ for (int i = 0; i < 10; i++) {
+ userOnlyManager.recordFailure(TEST_USER_ID, "ip" + i); // Different IPs
each time
+ }
+ // Should be locked
+ assertTrue(userOnlyManager.checkLock(TEST_USER_ID, null));
+ for (int i = 0; i < 10; i++) {
+ assertTrue(userOnlyManager.checkLock(TEST_USER_ID, "ip" + i)); //
Different IPs each time
+ }
+ }
+
+ // ---------------- Unlock Logic ----------------
+
+ @Test
+ public void testManualUnlock() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(lockManager.checkLock(TEST_USER_ID, TEST_IP));
+
+ lockManager.unlock(TEST_USER_ID, TEST_IP);
+ assertFalse(
+ "User should be unlocked after manual unlock",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testGlobalManualUnlock() {
+ for (int i = 0; i < failedLoginAttemptsPerUser; i++) {
+ lockManager.recordFailure(TEST_USER_ID, "ip" + i);
+ }
+ assertTrue(lockManager.checkLock(TEST_USER_ID, TEST_IP));
+
+ lockManager.unlock(TEST_USER_ID, null);
+ assertFalse(
+ "User should be unlocked after manual global unlock",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testClearFailureResetsCounters() {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+
+ lockManager.clearFailure(TEST_USER_ID, TEST_IP);
+
+ for (int i = 0; i < failedLoginAttempts - 1; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertFalse(
+ "Counters should be reset after clear",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testEmptyIpInUnlock() {
+ lockManager.unlock(TEST_USER_ID, "");
+ // no exception expected
+ }
+
+ // ---------------- Multi-user Multi-IP ----------------
+
+ @Test
+ public void testDifferentUsersAreIndependent() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ assertFalse("Other user should not be locked",
lockManager.checkLock(OTHER_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testDifferentIpsCountSeparately() {
+ for (int i = 0; i < failedLoginAttempts - 1; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ for (int i = 0; i < failedLoginAttempts - 1; i++) {
+ lockManager.recordFailure(TEST_USER_ID, ANOTHER_IP);
+ }
+ assertFalse(
+ "Neither IP should lock user individually",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ // ---------------- Cleaning Logic ----------------
+
+ @Test
+ public void testCleanExpiredLocks() {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(lockManager.checkLock(TEST_USER_ID, TEST_IP));
+
+ try {
+ Field field = LoginLockManager.class.getDeclaredField("userIpLocks");
+ field.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ ConcurrentMap<String, LoginLockManager.UserLockInfo> userIpLocks =
+ (ConcurrentMap<String, LoginLockManager.UserLockInfo>)
field.get(lockManager);
+
+ LoginLockManager.UserLockInfo lockInfo = userIpLocks.get(TEST_USER_ID +
"@" + TEST_IP);
+
+ Field tsField =
LoginLockManager.UserLockInfo.class.getDeclaredField("failureTimestamps");
+ tsField.setAccessible(true);
+
+ @SuppressWarnings("unchecked")
+ Deque<Long> timestamps = (Deque<Long>) tsField.get(lockInfo);
+
+ timestamps.clear();
+ timestamps.add(System.currentTimeMillis() - (passwordLockTimeMinutes *
60 * 1000L) - 5000);
+
+ } catch (Exception e) {
+ fail("Failed to modify failure timestamps via reflection: " +
e.getMessage());
+ }
+
+ lockManager.cleanExpiredLocks();
+ assertFalse(
+ "User should be unlocked after cleaning",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testNotCleanIfRecentFailureExists() {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ lockManager.cleanExpiredLocks();
+ assertFalse(
+ "Should still keep recent failure record",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ // ---------------- Sliding Window Logic ----------------
+
+ @Test
+ public void testLockWithinTimeWindow() throws InterruptedException {
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ Thread.sleep(200); // small delay but still within window
+ }
+ assertTrue(
+ "User should be locked within time window",
lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testFailuresOutsideWindowNotCounted() throws Exception {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+
+ // hack timestamps to simulate old failure
+ try {
+ Field field = LoginLockManager.class.getDeclaredField("userIpLocks");
+ field.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ ConcurrentMap<String, LoginLockManager.UserLockInfo> userIpLocks =
+ (ConcurrentMap<String, LoginLockManager.UserLockInfo>)
field.get(lockManager);
+
+ LoginLockManager.UserLockInfo lockInfo = userIpLocks.get(TEST_USER_ID +
"@" + TEST_IP);
+
+ Field tsField =
LoginLockManager.UserLockInfo.class.getDeclaredField("failureTimestamps");
+ tsField.setAccessible(true);
+
+ @SuppressWarnings("unchecked")
+ Deque<Long> timestamps = (Deque<Long>) tsField.get(lockInfo);
+
+ timestamps.clear();
+ timestamps.add(System.currentTimeMillis() - (passwordLockTimeMinutes *
60 * 1000L) - 5000);
+
+ } catch (Exception e) {
+ fail("Reflection modification failed: " + e.getMessage());
+ }
+
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ assertFalse(
+ "Old failures should not count toward new lock",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ // ---------------- Configuration and Invalid Input ----------------
+
+ @Test
+ public void testInvalidConfigurationDefaults() {
+ LoginLockManager invalidConfigManager = new LoginLockManager(-1, -1, -1);
+ for (int i = 0; i < 5; i++) {
+ invalidConfigManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(
+ "Should use default config when invalid values provided",
+ invalidConfigManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testNullIpHandling() {
+ lockManager.recordFailure(TEST_USER_ID, null);
+ assertFalse("Null IP should not cause exception",
lockManager.checkLock(TEST_USER_ID, null));
+ }
+
+ @Test
+ public void testNegativeUserId() {
+ long negativeUserId = -123L;
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(negativeUserId, TEST_IP);
+ }
+ assertTrue(
+ "Negative user ID should still lock",
lockManager.checkLock(negativeUserId, TEST_IP));
+ }
+
+ // ---------------- Concurrency Stress Tests ----------------
+
+ @Test
+ public void testConcurrentFailuresCauseLock() throws InterruptedException {
+ // Test configuration: 10 threads each making 2 failure attempts
+ int threadCount = 10;
+ int attemptsPerThread = 2;
+ int totalAttempts = threadCount * attemptsPerThread; // Should exceed lock
threshold
+
+ // Task definition: each thread records multiple failures
+ Runnable recordTask =
+ () -> {
+ for (int i = 0; i < attemptsPerThread; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ };
+
+ // Thread management: create and start all threads
+ Thread[] threads = new Thread[threadCount];
+ for (int i = 0; i < threadCount; i++) {
+ threads[i] = new Thread(recordTask);
+ threads[i].start();
+ }
+
+ // Synchronization: wait for all threads to complete
+ for (Thread t : threads) {
+ t.join();
+ }
+
+ // Verification: should be locked after concurrent attempts
+ assertTrue(
+ "System should be locked after " + totalAttempts + " concurrent
failure attempts",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+ }
+
+ @Test
+ public void testConcurrentDifferentUsersIndependent() throws
InterruptedException {
+ // Test configuration: 500 test users with threshold attempts each
+ int userCount = 500; // Large number to stress test isolation
+ int attemptsPerUser = failedLoginAttempts; // Exactly reaching threshold
+
+ // Task definition: simulate failures across multiple users
+ Runnable userFailureTask =
+ () -> {
+ for (int u = 0; u < userCount; u++) {
+ for (int i = 0; i < attemptsPerUser; i++) {
+ lockManager.recordFailure(TEST_USER_ID + u, TEST_IP);
+ }
+ }
+ };
+
+ // Concurrent execution: two threads working on the same user set
+ Thread t1 = new Thread(userFailureTask);
+ Thread t2 = new Thread(userFailureTask);
+ t1.start();
+ t2.start();
+ t1.join();
+ t2.join();
+
+ // Verification: each user should be locked independently
+ for (int u = 0; u < userCount; u++) {
+ assertTrue(
+ "User " + u + " should be locked independently without affecting
others",
+ lockManager.checkLock(TEST_USER_ID + u, TEST_IP));
+ }
+ }
+
+ @Test
+ public void testConcurrentClearAndFailure() throws InterruptedException {
+ // Setup: create locked state first
+ for (int i = 0; i < failedLoginAttempts; i++) {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ }
+ assertTrue(
+ "Precondition: user should be locked before concurrency test",
+ lockManager.checkLock(TEST_USER_ID, TEST_IP));
+
+ // Task definitions:
+ // Clear task: resets the failure count
+ Runnable clearTask =
+ () -> {
+ lockManager.clearFailure(TEST_USER_ID, TEST_IP);
+ // Artificial delay to increase race condition probability
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ }
+ };
+
+ // Failure task: records additional failures
+ Runnable failTask =
+ () -> {
+ lockManager.recordFailure(TEST_USER_ID, TEST_IP);
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ }
+ };
+
+ // Concurrent execution: run clear and record operations simultaneously
+ Thread clearThread = new Thread(clearTask);
+ Thread failThread = new Thread(failTask);
+ clearThread.start();
+ failThread.start();
+ clearThread.join();
+ failThread.join();
+
+ // Validation: system should be in consistent state
+ boolean finalState = lockManager.checkLock(TEST_USER_ID, TEST_IP);
+ assertTrue(
+ "Final state should be either consistently locked or unlocked",
finalState || !finalState);
+
+ // Additional verification: check internal counter state
+ try {
+ Field counterField =
LoginLockManager.class.getDeclaredField("userIpLocks");
+ counterField.setAccessible(true);
+ ConcurrentMap<?, ?> locks = (ConcurrentMap<?, ?>)
counterField.get(lockManager);
+ assertTrue(
+ "Internal counter should reflect concurrent operations",
+ locks.isEmpty() || !locks.isEmpty());
Review Comment:
Should depend on finalState.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]