diff --git a/conf/ldap.conf b/conf/ldap.conf
index 00647819273ce1..8ef56d85e8a806 100644
--- a/conf/ldap.conf
+++ b/conf/ldap.conf
@@ -50,6 +50,9 @@ 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 to connect to ldap with empty pass (enabled by default)
+# ldap_allow_empty_pass = true
+
# 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-authentication/fe-authentication-plugins/fe-authentication-plugin-ldap/pom.xml b/fe/fe-authentication/fe-authentication-plugins/fe-authentication-plugin-ldap/pom.xml
index 5f3522fde66b42..b89797f68c868a 100644
--- a/fe/fe-authentication/fe-authentication-plugins/fe-authentication-plugin-ldap/pom.xml
+++ b/fe/fe-authentication/fe-authentication-plugins/fe-authentication-plugin-ldap/pom.xml
@@ -71,6 +71,14 @@ under the License.
6.0.7
test
+
+
+
+ org.apache.doris
+ fe-common
+ ${project.version}
+ test
+
diff --git a/fe/fe-authentication/fe-authentication-plugins/fe-authentication-plugin-ldap/src/test/java/org/apache/doris/authentication/plugin/ldap/LdapAuthenticationPluginIntegrationTest.java b/fe/fe-authentication/fe-authentication-plugins/fe-authentication-plugin-ldap/src/test/java/org/apache/doris/authentication/plugin/ldap/LdapAuthenticationPluginIntegrationTest.java
index c014836a48a6b5..58642595e9ae44 100644
--- a/fe/fe-authentication/fe-authentication-plugins/fe-authentication-plugin-ldap/src/test/java/org/apache/doris/authentication/plugin/ldap/LdapAuthenticationPluginIntegrationTest.java
+++ b/fe/fe-authentication/fe-authentication-plugins/fe-authentication-plugin-ldap/src/test/java/org/apache/doris/authentication/plugin/ldap/LdapAuthenticationPluginIntegrationTest.java
@@ -22,6 +22,7 @@
import org.apache.doris.authentication.AuthenticationResult;
import org.apache.doris.authentication.CredentialType;
import org.apache.doris.authentication.Principal;
+import org.apache.doris.common.LdapConfig;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
@@ -30,6 +31,7 @@
import com.unboundid.ldif.LDIFException;
import com.unboundid.ldif.LDIFReader;
import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
@@ -111,8 +113,17 @@ static void stopLdapServer() {
}
}
+ @AfterEach
+ void tearDown() {
+ //running test with specified value - ldap_allow_empty_pass is be true
+ LdapConfig.ldap_allow_empty_pass = true;
+ }
+
@BeforeEach
void setUp() {
+ //running test with specified value - ldap_allow_empty_pass is be true
+ LdapConfig.ldap_allow_empty_pass = true;
+
plugin = new LdapAuthenticationPlugin();
// Create integration configuration pointing to embedded LDAP
@@ -220,8 +231,43 @@ void testAuthenticateNonExistentUser() throws Exception {
}
@Test
- @DisplayName("Should reject empty password")
- void testAuthenticateEmptyPassword() throws Exception {
+ @DisplayName("Should reject empty password with default value of ldap_allow_empty_pass")
+ void testAuthenticateEmptyPasswordAndAllowEmptyPassDefault() throws Exception {
+ //running test with default value - ldap_allow_empty_pass should be true
+ LdapConfig.ldap_allow_empty_pass = true;
+ AuthenticationRequest request = AuthenticationRequest.builder()
+ .username("alice")
+ .credentialType(CredentialType.CLEAR_TEXT_PASSWORD)
+ .credential("".getBytes(StandardCharsets.UTF_8))
+ .build();
+
+ AuthenticationResult result = plugin.authenticate(request, integration);
+
+ Assertions.assertTrue(result.isFailure());
+ }
+
+ @Test
+ @DisplayName("Should reject empty password with true value of ldap_allow_empty_pass")
+ void testAuthenticateEmptyPasswordAndAllowEmptyPassTrue() throws Exception {
+ //running test with obvious value - ldap_allow_empty_pass is true
+ LdapConfig.ldap_allow_empty_pass = true;
+ AuthenticationRequest request = AuthenticationRequest.builder()
+ .username("alice")
+ .credentialType(CredentialType.CLEAR_TEXT_PASSWORD)
+ .credential("".getBytes(StandardCharsets.UTF_8))
+ .build();
+
+ AuthenticationResult result = plugin.authenticate(request, integration);
+
+ Assertions.assertTrue(result.isFailure());
+ }
+
+ @Test
+ @DisplayName("Should reject empty password with false value of ldap_allow_empty_pass")
+ void testAuthenticateEmptyPasswordAndAllowEmptyPassFalse() throws Exception {
+ //running test with obvious value - ldap_allow_empty_pass is false
+ LdapConfig.ldap_allow_empty_pass = false;
+
AuthenticationRequest request = AuthenticationRequest.builder()
.username("alice")
.credentialType(CredentialType.CLEAR_TEXT_PASSWORD)
diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/ErrorCode.java b/fe/fe-common/src/main/java/org/apache/doris/common/ErrorCode.java
index 8f5fe32bb302b2..c5048532a43374 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/ErrorCode.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/ErrorCode.java
@@ -1235,7 +1235,10 @@ public enum ErrorCode {
ERR_NO_CLUSTER_ERROR(5099, new byte[]{'4', '2', '0', '0', '0'}, "No compute group (cloud cluster) selected"),
ERR_NOT_CLOUD_MODE(6000, new byte[]{'4', '2', '0', '0', '0'},
- "Command only support in cloud mode.");
+ "Command only support in cloud mode."),
+
+ ERR_EMPTY_PASSWORD(6001, new byte[]{'H', 'Y', '0', '0', '0'},
+ "Access with empty password is prohibited for LDAP user '%s'. Set ldap_allow_empty_pass=true to allow.");
// This is error code
private final int code;
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 82966af525b0d4..5730fc791393cf 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
@@ -210,4 +210,10 @@ public class LdapConfig extends ConfigBase {
public static String getConnectionURL(String hostPortInAccessibleFormat) {
return ((LdapConfig.ldap_use_ssl ? "ldaps" : "ldap") + "://" + hostPortInAccessibleFormat);
}
+
+ /**
+ * Flag to enable login with empty pass.
+ */
+ @ConfigBase.ConfField
+ public static boolean ldap_allow_empty_pass = true;
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticator.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticator.java
index b5502f7bcaffac..ee939bc2c185e4 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticator.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticator.java
@@ -21,6 +21,7 @@
import org.apache.doris.catalog.Env;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
+import org.apache.doris.common.LdapConfig;
import org.apache.doris.mysql.authenticate.AuthenticateRequest;
import org.apache.doris.mysql.authenticate.AuthenticateResponse;
import org.apache.doris.mysql.authenticate.Authenticator;
@@ -94,7 +95,7 @@ public boolean canDeal(String qualifiedUser) {
/**
* The LDAP authentication process is as follows:
- * step1: Check the LDAP password.
+ * step1: Check the LDAP password (if ldap_allow_empty_pass is false login with empty pass is prohibited).
* step2: Get the LDAP groups privileges as a role, saved into ConnectContext.
* step3: Set current userIdentity. If the user account does not exist in Doris, login as a temporary user.
* Otherwise, login to the Doris account.
@@ -107,11 +108,17 @@ private AuthenticateResponse internalAuthenticate(String password, String qualif
}
// check user password by ldap server.
+ // extra check for login with empty LDAP password was added in checkUserPasswd.
try {
if (!Env.getCurrentEnv().getAuth().getLdapManager().checkUserPasswd(qualifiedUser, password)) {
if (LOG.isDebugEnabled()) {
LOG.debug("internalAuthenticate: user={}, success=false", userName);
}
+ // extra log to identify case covered by PR 61440 - login with empty LDAP password is not allowed
+ if (Strings.isNullOrEmpty(password) && !LdapConfig.ldap_allow_empty_pass) {
+ LOG.info(ErrorCode.ERR_EMPTY_PASSWORD, username +"@" + remoteIp);
+ }
+
ErrorReport.report(ErrorCode.ERR_ACCESS_DENIED_ERROR, qualifiedUser, remoteIp, usePasswd);
return AuthenticateResponse.failedResponse;
}
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 2e1e1a26ecbb2b..c75dc02b99d121 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
@@ -122,6 +122,17 @@ public boolean doesUserExist(String fullName) {
return exists;
}
+ //not allow to login in case when empty password is specified but such mode is disabled by configuration
+ public boolean checkLoginWithEmptyPasswordForLdapIsAllowed(String fullName, String passwd) {
+ if (Strings.isNullOrEmpty(passwd) && !LdapConfig.ldap_allow_empty_pass) {
+ LOG.info("User:{} login rejected: empty LDAP password is prohibited (ldap_allow_empty_pass=false)",
+ fullName);
+ return false;
+ } else {
+ return true;
+ }
+ }
+
public boolean checkUserPasswd(String fullName, String passwd) {
long start = System.currentTimeMillis();
String userName = fullName;
@@ -129,6 +140,12 @@ public boolean checkUserPasswd(String fullName, String passwd) {
|| Objects.isNull(passwd)) {
return false;
}
+
+ // extra check for PR 61440 to disable login with empty LDAP password in case when specific property is true
+ if (!checkLoginWithEmptyPasswordForLdapIsAllowed(fullName, passwd)) {
+ return false;
+ }
+
LdapUserInfo ldapUserInfo = getUserInfo(fullName);
if (Objects.isNull(ldapUserInfo) || !ldapUserInfo.isExists()) {
long elapsed = System.currentTimeMillis() - start;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
index 3d03be862fd7bf..d45ff92f3fb110 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
@@ -38,6 +38,7 @@
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeConstants;
+import org.apache.doris.common.LdapConfig;
import org.apache.doris.common.Pair;
import org.apache.doris.common.PatternMatcherException;
import org.apache.doris.common.UserException;
@@ -237,6 +238,10 @@ public void checkPlainPassword(String remoteUser, String remoteHost, String remo
// Check the LDAP password when the user exists in the LDAP service.
if (ldapManager.doesUserExist(remoteUser)) {
if (!ldapManager.checkUserPasswd(remoteUser, remotePasswd, remoteHost, currentUser)) {
+ // extra log to identify case covered by PR 61440 - login with empty LDAP password is not allowed
+ if (Strings.isNullOrEmpty(remotePasswd) && !LdapConfig.ldap_allow_empty_pass) {
+ LOG.info(ErrorCode.ERR_EMPTY_PASSWORD, remoteUser +"@" + remoteHost);
+ }
throw new AuthenticationException(ErrorCode.ERR_ACCESS_DENIED_ERROR, remoteUser + "@" + remoteHost,
Strings.isNullOrEmpty(remotePasswd) ? "NO" : "YES");
}
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 64fffd2c71dbb9..a414201959838e 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.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;
@@ -47,6 +48,12 @@ public class LdapManagerTest {
public void setUp() {
Config.authentication_type = "ldap";
LdapConfig.ldap_default_roles = new String[0];
+ LdapConfig.ldap_allow_empty_pass = true;
+ }
+
+ @After
+ public void tearDown() {
+ LdapConfig.ldap_allow_empty_pass = true;
}
private void mockClient(boolean userExist, boolean passwd) {
@@ -108,6 +115,32 @@ public void testCheckUserPasswd() {
Assert.assertFalse(ldapManager.checkUserPasswd(USER2, "123"));
}
+ @Test
+ public void testUserLoginWithEmptyLDAPPasswordDefault() {
+ LdapManager ldapManager = new LdapManager();
+ Assert.assertTrue(ldapManager.checkLoginWithEmptyPasswordForLdapIsAllowed("username", null));
+ Assert.assertTrue(ldapManager.checkLoginWithEmptyPasswordForLdapIsAllowed("username", ""));
+ Assert.assertTrue(ldapManager.checkLoginWithEmptyPasswordForLdapIsAllowed("username", "password"));
+ }
+
+ @Test
+ public void testUserLoginWithEmptyLDAPPasswordEnabled() {
+ LdapManager ldapManager = new LdapManager();
+ LdapConfig.ldap_allow_empty_pass = true;
+ Assert.assertTrue(ldapManager.checkLoginWithEmptyPasswordForLdapIsAllowed("username", null));
+ Assert.assertTrue(ldapManager.checkLoginWithEmptyPasswordForLdapIsAllowed("username", ""));
+ Assert.assertTrue(ldapManager.checkLoginWithEmptyPasswordForLdapIsAllowed("username", "password"));
+ }
+
+ @Test
+ public void testUserLoginWithEmptyLDAPPasswordDisabled() {
+ LdapManager ldapManager = new LdapManager();
+ LdapConfig.ldap_allow_empty_pass = false;
+ Assert.assertFalse(ldapManager.checkLoginWithEmptyPasswordForLdapIsAllowed("username", null));
+ Assert.assertFalse(ldapManager.checkLoginWithEmptyPasswordForLdapIsAllowed("username", ""));
+ Assert.assertTrue(ldapManager.checkLoginWithEmptyPasswordForLdapIsAllowed("username", "password"));
+ }
+
@Test
public void testGetUserInfoWithLdapDefaultRolesWithoutLdapGroups() {
LdapManager ldapManager = new LdapManager();
diff --git a/git b/git
new file mode 100644
index 00000000000000..e69de29bb2d1d6