diff --git a/conf/ldap.conf b/conf/ldap.conf index deb1a06a1d0479..15a62c9134f159 100644 --- a/conf/ldap.conf +++ b/conf/ldap.conf @@ -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 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 9499fcc2a1b88f..d169d93cbd5382 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 @@ -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; } diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/ErrorCode.java b/fe/fe-core/src/main/java/org/apache/doris/common/ErrorCode.java index a678c2b38f9562..0707a2ccfe7631 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/common/ErrorCode.java +++ b/fe/fe-core/src/main/java/org/apache/doris/common/ErrorCode.java @@ -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; 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 cd9cef469d2520..2098680e97cd35 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 @@ -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; @@ -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. @@ -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)) { diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapClient.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapClient.java index d5641ac6c09b82..79248ab0212ecd 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapClient.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapClient.java @@ -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); @@ -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); @@ -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() { 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 dbfaaa15c2d49d..48f4d431c89d1c 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 @@ -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; @@ -228,6 +229,11 @@ public void checkPlainPassword(String remoteUser, String remoteHost, String remo List 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"); diff --git a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticatorTest.java b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticatorTest.java index 99cbcdb5fad643..5a17a3caa80f31 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticatorTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticatorTest.java @@ -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; @@ -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; @@ -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 + } } diff --git a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapClientTest.java b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapClientTest.java index 866a84e752819d..c0d6c36f83ba99 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapClientTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapClientTest.java @@ -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; @@ -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 @@ -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 + } }