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

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


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

commit d715350c96b1286d21dfde35c9d38a9c633c9e0d
Author: iaorekhov-1980 <[email protected]>
AuthorDate: Tue Aug 18 11:08:48 2026 +0300

    [enhance](auth) introduction of configuration property to prohibit login 
with empty LDAP password (#61440)
    
    ### What problem does this PR solve?
    
    This PR adds new configuration property **ldap_allow_empty_pass** to
    prohibit option for existing user to login into LDAP with empty
    password.
    It doesn't impact new approach from #60407 , because since 4.1.x new
    LDAP plugin explicitly prohibits login with empty pass.
    But in legacy version - 3.1.x and 4.0.x such option is still available.
    If **ldap_allow_empty_pass** in ldap.conf is not specified or specified
    as **false** - user can't login with empty pass (new behavior, aligned
    with newly introduced plugin authentication).
    If **ldap_allow_empty_pass** specified as **true** - login attempt with
    empty password will be possible as legacy behavior.
    
    **Could you please include this PR into 4.x branch, please!**
    
    Issue Number: close #60353
    
    Related PR: #xxx
    
    Problem Summary:
    
    Currently for existing user it is possible to login into LDAP with empty
    password.
    New configuration property disables such option, but user is still able
    to activate legacy behavior and allow login without specified password.
    
    ### Release note
    
    New **ldap_allow_empty_pass** property for legacy authentication
    approach was introduced into ldap.conf to prohibit login with empty LDAP
    password as it is allowed by LDAP protocol by default.
    
    ### 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. <!-- Explain the behavior change -->
    
    1. ldap.conf and LdapConfig.java - new configuration
    **ldap_allow_empty_pass** property with default value **false** to
    disable login with empty LDAP password as in latest plugin
    implementation.
    2. LdapManager.java - added logic to prohibit login with empty LDAP
    password and placed it into separate method, which is invoked from
    already existing checkUserPasswd
    3.1 user has specified empty password
    3.2 property **ldap_allow_empty_pass** is **false** and doesn't allow to
    login with empty password
    If both conditions met - authentication is failed and false is
    returning, as by other check in checkUserPassword
    4. LdapManagerTest.java - introduced set of test methods to validate for
    existing behavior (with **ldap_allow_empty_pass** = true) and new one
    (with **ldap_allow_empty_pass** property is not specified or specified
    to false) to check that login is still successful in first case and
    failed in the second one.
    
    - Does this need documentation?
        - [ ] No.
        - [x] Yes. [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 -->
    
    ---------
    
    Co-authored-by: morningman <[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 2e1e1a26ecb..07a2c9837ed 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
@@ -129,6 +129,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 64fffd2c71d..afe9d034b57 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
@@ -24,6 +24,7 @@ import org.apache.doris.common.jmockit.Deencapsulation;
 import org.apache.doris.mysql.privilege.Auth;
 import org.apache.doris.mysql.privilege.Role;
 
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -49,6 +50,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<>());
     }
@@ -108,6 +114,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 testGetUserInfoWithLdapDefaultRolesWithoutLdapGroups() {
         LdapManager ldapManager = new LdapManager();


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

Reply via email to