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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 35791e3b692 branch-4.1: [enhance](auth) introduction of configuration 
property to prohibit login with empty LDAP password #61440 (#68098)
35791e3b692 is described below

commit 35791e3b692482b0b2974b758e18374cac56503d
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Sun Sep 20 14:24:23 2026 +0800

    branch-4.1: [enhance](auth) introduction of configuration property to 
prohibit login with empty LDAP password #61440 (#68098)
    
    Cherry-picked from #61440 (master commit d715350c96b) to branch-4.1.
    
    ### What problem does this PR solve?
    
    Issue Number: close #60353
    
    Related PR: #61440
    
    Problem Summary:
    
    The legacy LDAP authentication path accepted an empty password: LDAP
    reports a bind with a non-empty DN and an empty password as a successful
    *unauthenticated* bind, so anyone who knew a valid LDAP user name could
    log in to Doris without a password.
    
    This adds the `ldap_allow_empty_pass` FE config (`ldap.conf`, default
    `false`). `LdapManager.checkUserPasswd` now rejects an empty password up
    front, before the cached-password comparison and the LDAP bind, so a
    previously cached empty password cannot short-circuit the check either.
    Setting `ldap_allow_empty_pass = true` restores the legacy behaviour.
    The plugin-based LDAP authentication (fe-authentication LDAP plugin)
    already rejects empty passwords and is not affected.
    
    Conflict resolution: only `LdapManagerTest.java` conflicted. branch-4.1
    carries two extra tests
    (`testCheckUserPasswdCachedPasswdMatchLogsInfoWithoutThreshold`,
    `testGetUserInfoLogsInfoWithoutThreshold`) at the position where master
    inserted the new tests; the new tests are placed right after
    `testCheckUserPasswd()` and the branch tests are left untouched. The
    added lines are identical to the master commit.
    
    ### Release note
    
    New `ldap_allow_empty_pass` property in `ldap.conf` for the legacy LDAP
    authentication path. Login with an empty LDAP password is now rejected
    by default; set it to `true` to restore the previous behaviour.
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. Empty LDAP passwords are rejected by default on the legacy
    LDAP path; `ldap_allow_empty_pass = true` opts back in.
    
    - Does this need documentation?
        - [ ] No.
        - [x] Yes. https://github.com/apache/doris-website/pull/3403
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-authored-by: iaorekhov-1980 <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 conf/ldap.conf                                     | 13 ++++
 .../java/org/apache/doris/common/LdapConfig.java   | 10 +++
 .../doris/mysql/authenticate/ldap/LdapManager.java |  9 +++
 .../mysql/authenticate/ldap/LdapManagerTest.java   | 77 ++++++++++++++++++++++
 4 files changed, 109 insertions(+)

diff --git a/conf/ldap.conf b/conf/ldap.conf
index 00647819273..39a73737164 100644
--- a/conf/ldap.conf
+++ b/conf/ldap.conf
@@ -50,6 +50,19 @@ ldap_group_basedn = ou=group,dc=domain,dc=com
 ## ldap_use_ssl - use secured connection to LDAP server if required (disabled 
by default). Note: When enabling SSL, ensure ldap_port is set appropriately 
(typically 636 for LDAPS instead of 389 for LDAP).
 # ldap_use_ssl = false
 
+## ldap_allow_empty_pass - allow LDAP users to log in with an empty password. 
Disabled by default.
+##
+## LDAP treats a bind with a non-empty DN and an empty password as an 
unauthenticated bind and
+## normally reports it as successful, so with this enabled anyone who knows a 
valid LDAP user
+## name can log in to Doris without a password. Keep it false unless you must 
restore the
+## legacy behaviour; setting it to true reopens that hole.
+##
+## Changing this requires an FE restart (the config is not runtime-mutable).
+##
+## This setting applies to the legacy LDAP authentication path only. The 
plugin-based LDAP
+## authentication (the fe-authentication LDAP plugin) always rejects empty 
passwords.
+# ldap_allow_empty_pass = false
+
 # LDAP pool configuration
 # 
https://docs.spring.io/spring-ldap/docs/2.3.3.RELEASE/reference/#pool-configuration
 # ldap_pool_max_active = 8
diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/LdapConfig.java 
b/fe/fe-common/src/main/java/org/apache/doris/common/LdapConfig.java
index 82966af525b..6756f2ec43e 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/LdapConfig.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/LdapConfig.java
@@ -199,6 +199,16 @@ public class LdapConfig extends ConfigBase {
     @ConfigBase.ConfField
     public static boolean ldap_use_ssl = false;
 
+    /**
+     * Allow LDAP users to log in with an empty password. Disabled by default: 
LDAP reports a bind
+     * with an empty password as a successful unauthenticated bind, so 
enabling this lets anyone
+     * who knows a valid LDAP user name log in without a password. Applies to 
the legacy LDAP
+     * authentication path only; the fe-authentication LDAP plugin always 
rejects empty passwords.
+     * Not runtime-mutable - changing it requires an FE restart.
+     */
+    @ConfigBase.ConfField
+    public static boolean ldap_allow_empty_pass = false;
+
     /**
      * The method constructs the correct URL connection string for the 
specified host and port depending on
      * the value of the {@code ldap_use_ssl} property.
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java
 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java
index bfafb0ac686..6ab0398d315 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java
@@ -130,6 +130,15 @@ public class LdapManager {
                 || Objects.isNull(passwd)) {
             return false;
         }
+
+        // Reject an empty password here, before the cached-password 
comparison and the LDAP bind
+        // below: an empty password would otherwise reach the server as an 
unauthenticated bind,
+        // which LDAP reports as success. Opt out with ldap_allow_empty_pass = 
true.
+        if (passwd.isEmpty() && !LdapConfig.ldap_allow_empty_pass) {
+            LOG.warn("Rejected LDAP login with empty password, user={}, 
ldapAllowEmptyPass=false", fullName);
+            return false;
+        }
+
         LdapUserInfo ldapUserInfo = getUserInfo(fullName);
         if (Objects.isNull(ldapUserInfo) || !ldapUserInfo.isExists()) {
             long elapsed = System.currentTimeMillis() - start;
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java
index 37492c799be..3971d332b4c 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java
@@ -26,6 +26,7 @@ import org.apache.doris.mysql.privilege.Auth;
 import org.apache.doris.mysql.privilege.Role;
 
 import org.apache.logging.log4j.Level;
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -51,6 +52,11 @@ public class LdapManagerTest {
         LdapConfig.ldap_default_roles = new String[0];
     }
 
+    @After
+    public void tearDown() {
+        LdapConfig.ldap_allow_empty_pass = false;
+    }
+
     private void mockClient(boolean userExist, boolean passwd) {
         mockClient(userExist, passwd, new ArrayList<>());
     }
@@ -110,6 +116,77 @@ public class LdapManagerTest {
         Assert.assertFalse(ldapManager.checkUserPasswd(USER2, "123"));
     }
 
+    @Test
+    public void testCheckUserEmptyPasswdAllowed() throws Exception {
+        //test checks - that user with empty ldap password can login with 
ldap_allow_empty_pass = true
+        LdapConfig.ldap_allow_empty_pass = true;
+        LdapManager ldapManager = new LdapManager();
+        Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
+        mockClient(true, true);
+        Assert.assertTrue(ldapManager.checkUserPasswd(USER1, ""));
+        LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1);
+        Assert.assertNotNull(ldapUserInfo);
+        Assert.assertTrue(ldapUserInfo.isSetPasswd());
+        Assert.assertEquals("", ldapUserInfo.getPasswd());
+    }
+
+    @Test
+    public void testCheckUserEmptyPasswdDisabled() throws Exception {
+        //test checks - that login with empty ldap password is prohibited by 
default
+        //corresponding property is set to false - so login with empty 
password is not allowed
+        //if password is not empty - user can login as usual
+        LdapManager ldapManager = new LdapManager();
+        Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
+        mockClient(true, true);
+        Assert.assertFalse(ldapManager.checkUserPasswd(USER1, ""));
+
+        Assert.assertTrue(ldapManager.checkUserPasswd(USER1, "123"));
+        LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1);
+        Assert.assertNotNull(ldapUserInfo);
+        Assert.assertTrue(ldapUserInfo.isSetPasswd());
+        Assert.assertEquals("123", ldapUserInfo.getPasswd());
+    }
+
+    @Test
+    public void testCachedEmptyPasswordIsRejectedAfterFlagDisabled() {
+        LdapConfig.ldap_allow_empty_pass = true;
+        LdapManager ldapManager = new LdapManager();
+        Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
+        mockClient(true, true);
+        //empty password succeeds and gets cached while the flag is still 
enabled.
+        Assert.assertTrue(ldapManager.checkUserPasswd(USER1, ""));
+        Assert.assertEquals("", ldapManager.getUserInfo(USER1).getPasswd());
+
+        //once disabled, the cached entry must not short-circuit the new check
+        LdapConfig.ldap_allow_empty_pass = false;
+        Assert.assertFalse(ldapManager.checkUserPasswd(USER1, ""));
+        //a non-empty password still authenticates against the same cached 
entry
+        Assert.assertTrue(ldapManager.checkUserPasswd(USER1, "123"));
+    }
+
+    @Test
+    public void testEmptyPasswordIsRejectedBeforeCacheLookup() throws 
Exception {
+        //the empty password check must run before getUserInfo(), which is 
what keeps a cached
+        //empty password from short-circuiting the check and letting the login 
through
+        LdapManager ldapManager = new LdapManager();
+        Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
+        mockClient(true, true);
+
+        LdapManager spyManager = Mockito.spy(ldapManager);
+        Assert.assertFalse(spyManager.checkUserPasswd(USER1, ""));
+        Mockito.verify(spyManager, Mockito.times(0)).getUserInfo(USER1);
+    }
+
+    @Test
+    public void testCheckUserNullPasswd() throws Exception {
+        //test check existing feature that user with null ldap password can't 
login in any case
+        //because this is first check in checkUserPasswd() method
+        LdapManager ldapManager = new LdapManager();
+        Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
+        mockClient(true, true);
+        Assert.assertFalse(ldapManager.checkUserPasswd(USER1, null));
+    }
+
     @Test
     public void testCheckUserPasswdCachedPasswdMatchLogsInfoWithoutThreshold() 
{
         LdapManager ldapManager = new LdapManager();


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

Reply via email to