Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions conf/ldap.conf
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ ldap_group_basedn = ou=group,dc=domain,dc=com

# ldap_user_cache_timeout_s = 5 * 60;

## 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
24 changes: 24 additions & 0 deletions fe/fe-common/src/main/java/org/apache/doris/common/LdapConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,28 @@ public class LdapConfig extends ConfigBase {
*/
@ConfigBase.ConfField
public static boolean ldap_pool_test_while_idle = true;

/**
* Flag to enable usage of LDAPS.
*/
@ConfigBase.ConfField
public static boolean ldap_use_ssl = 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.
* If {@code ldap_use_ssl} is true, LDAPS is used as the protocol.
* If {@code ldap_use_ssl} is false or not specified, LDAP is used as the protocol.
* @param hostPortInAccessibleFormat the host and port in accessible format (for example, "host:port")
* @return the LDAP or LDAPS connection URL string
*/
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;
}
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");

// This is error code
private final int code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.doris.cluster.ClusterNamespace;
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;
Expand Down Expand Up @@ -84,7 +85,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 @@ -96,6 +97,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 @@ -65,8 +65,8 @@ public ClientInfo(String ldapPassword) {

private void setLdapTemplateNoPool(String ldapPassword) {
LdapContextSource contextSource = new LdapContextSource();
String url = "ldap://" + NetUtils
.getHostPortInAccessibleFormat(LdapConfig.ldap_host, LdapConfig.ldap_port);
String url = LdapConfig.getConnectionURL(
NetUtils.getHostPortInAccessibleFormat(LdapConfig.ldap_host, LdapConfig.ldap_port));

contextSource.setUrl(url);
contextSource.setUserDn(LdapConfig.ldap_admin_name);
Expand All @@ -78,8 +78,8 @@ private void setLdapTemplateNoPool(String ldapPassword) {

private void setLdapTemplatePool(String ldapPassword) {
LdapContextSource contextSource = new LdapContextSource();
String url = "ldap://" + NetUtils
.getHostPortInAccessibleFormat(LdapConfig.ldap_host, LdapConfig.ldap_port);
String url = LdapConfig.getConnectionURL(
NetUtils.getHostPortInAccessibleFormat(LdapConfig.ldap_host, LdapConfig.ldap_port));

contextSource.setUrl(url);
contextSource.setUserDn(LdapConfig.ldap_admin_name);
Expand Down Expand Up @@ -108,6 +108,7 @@ private void setLdapTemplatePool(String ldapPassword) {
public boolean checkUpdate(String ldapPassword) {
return this.ldapPassword == null || !this.ldapPassword.equals(ldapPassword);
}

}

private void init() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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 @@ -228,6 +229,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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

import org.apache.doris.common.Config;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.common.util.NetUtils;

import mockit.Expectations;
import mockit.Tested;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
Expand All @@ -43,6 +45,7 @@ public void setUp() {
LdapConfig.ldap_user_basedn = "dc=baidu,dc=com";
LdapConfig.ldap_group_basedn = "ou=group,dc=baidu,dc=com";
LdapConfig.ldap_user_filter = "(&(uid={login}))";
LdapConfig.ldap_use_ssl = false;
}

@Test
Expand Down Expand Up @@ -95,4 +98,28 @@ public void testGetGroups() {
};
Assert.assertEquals(1, ldapClient.getGroups("zhangsan").size());
}

@Test
public void testSecuredProtocolIsUsed() {
//testing default case with not specified property ldap_use_ssl or it is specified as false
String insecureUrl = LdapConfig.getConnectionURL(
NetUtils.getHostPortInAccessibleFormat(LdapConfig.ldap_host, LdapConfig.ldap_port));

Assert.assertNotNull("connection URL should not be null", insecureUrl);
Assert.assertTrue("with ldap_use_ssl = false or not specified URL should start with ldap, but received: " + insecureUrl,
insecureUrl.startsWith("ldap://"));

//testing new case with specified property ldap_use_ssl as true
LdapConfig.ldap_use_ssl = true;
String secureUrl = LdapConfig.getConnectionURL(
NetUtils.getHostPortInAccessibleFormat(LdapConfig.ldap_host, LdapConfig.ldap_port));
Assert.assertNotNull("connection URL should not be null", secureUrl);
Assert.assertTrue("with ldap_use_ssl = true URL should start with ldaps, but received: " + secureUrl,
secureUrl.startsWith("ldaps://"));
}

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