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

markt-asf pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/9.0.x by this push:
     new 6dd75beb55 Add case sensitive attribute to LockOutRealm
6dd75beb55 is described below

commit 6dd75beb55bd42fc5f78e929596b25018cd17717
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Apr 22 14:18:52 2026 +0100

    Add case sensitive attribute to LockOutRealm
    
    # Conflicts:
    #       webapps/docs/changelog.xml
    # Conflicts:
    #       webapps/docs/changelog.xml
---
 java/org/apache/catalina/realm/LockOutRealm.java   |  25 +++++
 .../apache/catalina/realm/TestLockoutRealm.java    | 117 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   7 ++
 webapps/docs/config/realm.xml                      |   6 ++
 4 files changed, 155 insertions(+)

diff --git a/java/org/apache/catalina/realm/LockOutRealm.java 
b/java/org/apache/catalina/realm/LockOutRealm.java
index ef3964a791..cd066401e6 100644
--- a/java/org/apache/catalina/realm/LockOutRealm.java
+++ b/java/org/apache/catalina/realm/LockOutRealm.java
@@ -19,6 +19,7 @@ package org.apache.catalina.realm;
 import java.security.Principal;
 import java.security.cert.X509Certificate;
 import java.util.LinkedHashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -71,6 +72,11 @@ public class LockOutRealm extends CombinedRealm {
      */
     protected Map<String,LockRecord> failedUsers = null;
 
+    /*
+     * Should user names (the keys) in the failedUsers Map be treated in a 
case sensitive manner. The default is false.
+     */
+    private boolean caseSensitive = false;
+
 
     @Override
     protected void startInternal() throws LifecycleException {
@@ -205,6 +211,9 @@ public class LockOutRealm extends CombinedRealm {
      * time will be recorded and any attempt to authenticate a locked user 
will log a warning.
      */
     public boolean isLocked(String username) {
+        if (!getCaseSensitive()) {
+            username = username.toLowerCase(Locale.ROOT);
+        }
         LockRecord lockRecord;
         synchronized (this) {
             lockRecord = failedUsers.get(username);
@@ -227,6 +236,9 @@ public class LockOutRealm extends CombinedRealm {
      * After successful authentication, any record of previous authentication 
failure is removed.
      */
     private synchronized void registerAuthSuccess(String username) {
+        if (!getCaseSensitive()) {
+            username = username.toLowerCase(Locale.ROOT);
+        }
         // Successful authentication means removal from the list of failed 
users
         failedUsers.remove(username);
     }
@@ -236,6 +248,9 @@ public class LockOutRealm extends CombinedRealm {
      * After a failed authentication, add the record of the failed 
authentication.
      */
     private void registerAuthFailure(String username) {
+        if (!getCaseSensitive()) {
+            username = username.toLowerCase(Locale.ROOT);
+        }
         LockRecord lockRecord;
         synchronized (this) {
             if (!failedUsers.containsKey(username)) {
@@ -337,6 +352,16 @@ public class LockOutRealm extends CombinedRealm {
     }
 
 
+    public boolean getCaseSensitive() {
+        return caseSensitive;
+    }
+
+
+    public void setCaseSensitive(boolean caseSensitive) {
+        this.caseSensitive = caseSensitive;
+    }
+
+
     protected static class LockRecord {
         private final AtomicInteger failures = new AtomicInteger(0);
         private long lastFailureTime = 0;
diff --git a/test/org/apache/catalina/realm/TestLockoutRealm.java 
b/test/org/apache/catalina/realm/TestLockoutRealm.java
new file mode 100644
index 0000000000..d859fc85b4
--- /dev/null
+++ b/test/org/apache/catalina/realm/TestLockoutRealm.java
@@ -0,0 +1,117 @@
+/*
+ * 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.catalina.realm;
+
+import java.security.Principal;
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.startup.TesterMapRealm;
+import org.apache.tomcat.unittest.TesterContext;
+
+public class TestLockoutRealm {
+
+    private static final String USER_NAME = "user";
+    private static final String PASSWORD = "password";
+
+    private LockOutRealm realm;
+
+
+    @Before
+    public void init() throws Exception {
+        Context context = new TesterContext();
+        TesterMapRealm tmr = new TesterMapRealm();
+        tmr.setContainer(context);
+        MessageDigestCredentialHandler ch = new 
MessageDigestCredentialHandler();
+        tmr.setCredentialHandler(ch);
+        tmr.addUser(USER_NAME, PASSWORD);
+        tmr.start();
+
+        realm = new LockOutRealm();
+        realm.setContainer(context);
+        realm.addRealm(tmr);
+        realm.setFailureCount(2);
+        realm.start();
+    }
+
+
+    @Test
+    public void testLockoutAfterFailure() {
+        Principal p = realm.authenticate(USER_NAME, PASSWORD);
+        Assert.assertNotNull(p);
+
+        p = realm.authenticate(USER_NAME, "wrong");
+        p = realm.authenticate(USER_NAME, "wrong");
+        // Should be locked now
+        p = realm.authenticate(USER_NAME, PASSWORD);
+        Assert.assertNull(p);
+    }
+
+
+    @Test
+    public void testLockoutAfterFailureCaseSensitiveDefault() {
+        Principal p = realm.authenticate(USER_NAME, PASSWORD);
+        Assert.assertNotNull(p);
+
+        p = realm.authenticate(USER_NAME, "wrong");
+        p = realm.authenticate(USER_NAME.toUpperCase(Locale.ENGLISH), "wrong");
+        // Should be locked now
+        p = realm.authenticate(USER_NAME, PASSWORD);
+        Assert.assertNull(p);
+    }
+
+
+    @Test
+    public void testLockoutAfterFailureCaseSensitiveFalse() {
+        realm.setCaseSensitive(false);
+
+        Principal p = realm.authenticate(USER_NAME, PASSWORD);
+        Assert.assertNotNull(p);
+
+        p = realm.authenticate(USER_NAME, "wrong");
+        p = realm.authenticate(USER_NAME.toUpperCase(Locale.ENGLISH), "wrong");
+        // Should be locked now
+        p = realm.authenticate(USER_NAME, PASSWORD);
+        Assert.assertNull(p);
+    }
+
+
+    @Test
+    public void testLockoutAfterFailureCaseSensitiveTrue() {
+        realm.setCaseSensitive(true);
+
+        Principal p = realm.authenticate(USER_NAME, PASSWORD);
+        Assert.assertNotNull(p);
+
+        p = realm.authenticate(USER_NAME, "wrong");
+        p = realm.authenticate(USER_NAME.toUpperCase(Locale.ENGLISH), "wrong");
+        // Should not be locked yet
+        p = realm.authenticate(USER_NAME, PASSWORD);
+        Assert.assertNotNull(p);
+
+        p = realm.authenticate(USER_NAME, "wrong");
+        p = realm.authenticate(USER_NAME, "wrong");
+
+        // Both should be locked now
+        p = realm.authenticate(USER_NAME, PASSWORD);
+        Assert.assertNull(p);
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index a2f4e7f303..563bf4a0f5 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -162,6 +162,13 @@
         request body for LOCK and PROPFIND. The default value is 4096 bytes.
         (markt)
       </fix>
+      <add>
+        Add a new <code>caseSensitive</code> attribute to the
+        <code>LockOutRealm</code> that controls the manner in which user names
+        are treated when making locking decisions. The default is
+        <code>false</code>, meaning user names are treated in a case 
insensitive
+        manner. (markt)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Coyote">
diff --git a/webapps/docs/config/realm.xml b/webapps/docs/config/realm.xml
index 183c9c5373..d01c687a3d 100644
--- a/webapps/docs/config/realm.xml
+++ b/webapps/docs/config/realm.xml
@@ -1009,6 +1009,12 @@
        1000.</p>
       </attribute>
 
+      <attribute name="caseSensitive" required="false">
+       <p>The manner in which user names will be treated when making locking
+       decisions. Defaults to <code>false</code>, meaning user names will be
+       treated in a case insensitive manner.</p>
+      </attribute>
+
       <attribute name="failureCount" required="false">
        <p>The number of times in a row a user has to fail authentication to be
        locked out. Defaults to 5.</p>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to