Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions conf/ldap.conf
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1233,7 +1233,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[]{'4', '2', '0', '0', '0'},
"Access with empty password is prohibited for user %s because of current mode");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The phrase "because of current mode" is vague and doesn't help the user understand what to do. Consider a more specific message:

"Access with empty password is prohibited for LDAP user '%s'. Set ldap_allow_empty_pass=true to allow."

This tells the user both what happened and how to resolve it.

// This is error code
private final int code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This config should be mutable = true so it can be toggled at runtime without restarting FE:

@ConfigBase.ConfField(mutable = true)

Rationale: This is a security policy toggle checked at authentication time with no cached state or initialization dependency. An admin discovering empty-password logins as a security risk would want to disable this immediately without restarting FE (which disconnects all users). The precedent in this file (ldap_user_cache_timeout_s, ldap_cache_timeout_day) uses mutable = true for runtime-tunable configs, and this config is an even stronger candidate since it's a pure runtime check.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit] The log message includes ldap_allow_empty_pass:{} but since this branch is only entered when ldap_allow_empty_pass is false, the logged value is always false — it adds no information. Consider simplifying:

LOG.info("user:{} login rejected: empty LDAP password is prohibited (ldap_allow_empty_pass=false)", userName);

Expand Down Expand Up @@ -83,7 +84,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.
Expand All @@ -95,6 +96,14 @@ private AuthenticateResponse internalAuthenticate(String password, String qualif
LOG.debug("user:{}", userName);
}

//not allow to login in case when empty password is specified but such mode is disabled by configuration
if (Strings.isNullOrEmpty(password) && !LdapConfig.ldap_allow_empty_pass) {
LOG.info("user:{} is not allowed to login to LDAP with empty password because ldap_allow_empty_pass:{}",
userName, LdapConfig.ldap_allow_empty_pass);
ErrorReport.report(ErrorCode.ERR_EMPTY_PASSWORD, qualifiedUser + "@" + remoteIp);
return AuthenticateResponse.failedResponse;
}

// check user password by ldap server.
try {
if (!Env.getCurrentEnv().getAuth().getLdapManager().checkUserPasswd(qualifiedUser, password)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -227,6 +228,11 @@ public void checkPlainPassword(String remoteUser, String remoteHost, String remo
List<UserIdentity> currentUser) throws AuthenticationException {
// Check the LDAP password when the user exists in the LDAP service.
if (ldapManager.doesUserExist(remoteUser)) {
//not allow to login in case when empty password is specified but such mode is disabled by configuration
if (Strings.isNullOrEmpty(remotePasswd) && !LdapConfig.ldap_allow_empty_pass) {
throw new AuthenticationException(ErrorCode.ERR_EMPTY_PASSWORD, remoteUser + "@" + remoteHost);
}

if (!ldapManager.checkUserPasswd(remoteUser, remotePasswd, remoteHost, currentUser)) {
throw new AuthenticationException(ErrorCode.ERR_ACCESS_DENIED_ERROR, remoteUser + "@" + remoteHost,
Strings.isNullOrEmpty(remotePasswd) ? "NO" : "YES");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.doris.mysql.authenticate.ldap;

import org.apache.doris.analysis.UserIdentity;
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.password.ClearPassword;
Expand All @@ -27,7 +28,9 @@
import com.google.common.collect.Lists;
import mockit.Expectations;
import mockit.Mocked;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
Expand Down Expand Up @@ -143,4 +146,32 @@ public void testCanDeal() {
public void testGetPasswordResolver() {
Assert.assertTrue(ldapAuthenticator.getPasswordResolver() instanceof ClearPasswordResolver);
}

@Test
public void testEmptyPassword() throws IOException {
setCheckPassword(true);
setGetUserInDoris(true);
AuthenticateRequest request = new AuthenticateRequest(USER_NAME, new ClearPassword(""), IP);
//running test with non-specified value - ldap_allow_empty_pass should be true
AuthenticateResponse response = ldapAuthenticator.authenticate(request);
Assert.assertTrue(response.isSuccess());
//running test with specified value - true - ldap_allow_empty_pass is explicitly set to true
LdapConfig.ldap_allow_empty_pass = true;
response = ldapAuthenticator.authenticate(request);
Assert.assertTrue(response.isSuccess());
//running test with specified value - false - ldap_allow_empty_pass is explicitly set to false
LdapConfig.ldap_allow_empty_pass = false;
response = ldapAuthenticator.authenticate(request);
Assert.assertFalse(response.isSuccess());
}

@After
public void tearDown() {
LdapConfig.ldap_allow_empty_pass = true; // restoring default value for other tests
}

@Before
public void setUp() {
LdapConfig.ldap_allow_empty_pass = true; //restoring default value for other tests
}
}
Loading