[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 <yunyou@selectdb.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-08-18 16:08:48 +08:00
1 parent 4a49d3434b
commit d715350c96
4 files changed
+109

No files matched your search

+13
View File
@@ -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
@@ -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.
@@ -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;
@@ -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();