diff --git a/conf/ldap.conf b/conf/ldap.conf index 9388ae7ee50b1e..15a62c9134f159 100644 --- a/conf/ldap.conf +++ b/conf/ldap.conf @@ -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 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..8540f7f03314f5 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[]{'4', '2', '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 be25b5af0a4761..42fe1ad6a06715 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 @@ -204,4 +204,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(mutable = true) + 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..005f20c18e49e6 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. @@ -106,6 +107,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:{} login rejected: empty LDAP password is prohibited (ldap_allow_empty_pass=false)", + userName); + 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/privilege/Auth.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java index d4c3384599ebe9..7109397a9a0e01 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; @@ -226,9 +227,17 @@ public Set getRolesByUser(UserIdentity user, boolean showUserDefaultRole public void checkPlainPassword(String remoteUser, String remoteHost, String remotePasswd, List currentUser) throws AuthenticationException { // Check the LDAP password when the user exists in the LDAP service. - if (ldapManager.doesUserExist(remoteUser)) { - if (!ldapManager.checkUserPasswd(remoteUser, remotePasswd, remoteHost, currentUser)) { - throw new AuthenticationException(ErrorCode.ERR_ACCESS_DENIED_ERROR, remoteUser + "@" + remoteHost, + if (getLdapManager().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) { + LOG.info("empty pass branch was activated: for user {}, pass {}, mode {}", + remoteUser, remotePasswd, LdapConfig.ldap_allow_empty_pass); + throw new AuthenticationException(ErrorCode.ERR_EMPTY_PASSWORD, remoteUser + "@" + remoteHost); + } + + if (!getLdapManager().checkUserPasswd(remoteUser, remotePasswd, remoteHost, currentUser)) { + throw new AuthenticationException(ErrorCode.ERR_ACCESS_DENIED_ERROR, + remoteUser + "@" + remoteHost + " via LDAP", Strings.isNullOrEmpty(remotePasswd) ? "NO" : "YES"); } } else { 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 6045b1ff339189..cf9c44165c8dcb 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 @@ -19,6 +19,7 @@ import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; +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; @@ -54,11 +55,13 @@ public void setUp() { mockedEnvStatic.when(Env::getCurrentEnv).thenReturn(env); Mockito.when(env.getAuth()).thenReturn(auth); Mockito.when(auth.getLdapManager()).thenReturn(ldapManager); + LdapConfig.ldap_allow_empty_pass = true; // restoring default value for other tests } @After public void tearDown() { mockedEnvStatic.close(); + LdapConfig.ldap_allow_empty_pass = true; //restoring default value for other tests } private void setCheckPassword(boolean res) { @@ -135,4 +138,57 @@ public void testCanDeal() { public void testGetPasswordResolver() { Assert.assertTrue(ldapAuthenticator.getPasswordResolver() instanceof ClearPasswordResolver); } + + @Test + public void testEmptyPasswordWithAllowEmptyPassDefault() throws IOException { + setCheckPassword(true); + setGetUserInDoris(true); + //running test with non-specified value - ldap_allow_empty_pass should be true + //test with empty pass - success + AuthenticateRequest request = new AuthenticateRequest("user1.1", new ClearPassword(""), IP); + Assert.assertTrue(LdapConfig.ldap_allow_empty_pass); + AuthenticateResponse response = ldapAuthenticator.authenticate(request); + Assert.assertTrue(response.isSuccess()); + //test with non empty pass - success + request = new AuthenticateRequest("user1.2", new ClearPassword("pass"), IP); + Assert.assertTrue(LdapConfig.ldap_allow_empty_pass); + response = ldapAuthenticator.authenticate(request); + Assert.assertTrue(response.isSuccess()); + } + + @Test + public void testEmptyPasswordWithAllowEmptyPassTrue() throws IOException { + setCheckPassword(true); + setGetUserInDoris(true); + //running test with specified value - ldap_allow_empty_pass is true + LdapConfig.ldap_allow_empty_pass = true; + //test with empty pass - success + AuthenticateRequest request = new AuthenticateRequest("user2.1", new ClearPassword(""), IP); + Assert.assertTrue(LdapConfig.ldap_allow_empty_pass); + AuthenticateResponse response = ldapAuthenticator.authenticate(request); + Assert.assertTrue(response.isSuccess()); + //test with non empty pass - success + request = new AuthenticateRequest("user2.2", new ClearPassword("pass"), IP); + Assert.assertTrue(LdapConfig.ldap_allow_empty_pass); + response = ldapAuthenticator.authenticate(request); + Assert.assertTrue(response.isSuccess()); + } + + @Test + public void testEmptyPasswordWithAllowEmptyPassFalse() throws IOException { + setCheckPassword(true); + setGetUserInDoris(true); + //running test with specified value - ldap_allow_empty_pass is false + LdapConfig.ldap_allow_empty_pass = false; + //test with empty pass - failure + AuthenticateRequest request = new AuthenticateRequest("user3.1", new ClearPassword(""), IP); + Assert.assertFalse(LdapConfig.ldap_allow_empty_pass); + AuthenticateResponse response = ldapAuthenticator.authenticate(request); + Assert.assertFalse(response.isSuccess()); + //test with non empty pass - success + request = new AuthenticateRequest("user3.2", new ClearPassword("pass"), IP); + Assert.assertFalse(LdapConfig.ldap_allow_empty_pass); + response = ldapAuthenticator.authenticate(request); + Assert.assertTrue(response.isSuccess()); + } } diff --git a/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/PlainAuthWithEmptyPasswordAndLdapTest.java b/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/PlainAuthWithEmptyPasswordAndLdapTest.java new file mode 100644 index 00000000000000..d9afa3602af1a1 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/PlainAuthWithEmptyPasswordAndLdapTest.java @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.mysql.privilege; + +import org.apache.doris.catalog.Env; +import org.apache.doris.common.AuthenticationException; +import org.apache.doris.common.LdapConfig; +import org.apache.doris.mysql.authenticate.ldap.LdapManager; +import org.apache.doris.utframe.TestWithFeService; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + + +public class PlainAuthWithEmptyPasswordAndLdapTest extends TestWithFeService { + private static final String IP = "192.168.1.1"; + + private LdapManager ldapManager = Mockito.mock(LdapManager.class); + private MockedStatic mockedEnv; + + @Test + public void testPlainPasswordAuthWithAllowEmptyPassDefault() throws Exception { + //running test with non-specified value - ldap_allow_empty_pass should be true + //non empty pass - success + Assertions.assertTrue(LdapConfig.ldap_allow_empty_pass); + Env.getCurrentEnv().getAuth().checkPlainPassword("user1.2", IP, "testPass", null); + //empty pass - success + Assertions.assertTrue(LdapConfig.ldap_allow_empty_pass); + Env.getCurrentEnv().getAuth().checkPlainPassword("user1.1", IP, "", null); + } + + + @Test + public void testPlainPasswordAuthWithAllowEmptyPassTrue() throws Exception { + //running test with specified value - ldap_allow_empty_pass is be true + LdapConfig.ldap_allow_empty_pass = true; + + //non empty pass - success + Assertions.assertTrue(LdapConfig.ldap_allow_empty_pass); + Env.getCurrentEnv().getAuth().checkPlainPassword("user2.2", IP, "testPass", null); + //empty pass - success + Assertions.assertTrue(LdapConfig.ldap_allow_empty_pass); + Env.getCurrentEnv().getAuth().checkPlainPassword("user2.1", IP, "", null); + } + + @Test + public void testPlainPasswordAuthWithAllowEmptyPassFalse() throws Exception { + //running test with specified value - ldap_allow_empty_pass is false + LdapConfig.ldap_allow_empty_pass = false; + + //empty pass - failure + Assertions.assertFalse(LdapConfig.ldap_allow_empty_pass); + Assertions.assertThrows(AuthenticationException.class, () -> { + Env.getCurrentEnv().getAuth().checkPlainPassword("user3.1", IP, "", null); + }); + + //non empty pass - success + Assertions.assertFalse(LdapConfig.ldap_allow_empty_pass); + Env.getCurrentEnv().getAuth().checkPlainPassword("user3.2", IP, "testPass", null); + } + + @AfterEach + public void tearDown() { + System.out.println("4.0 [" + LdapConfig.ldap_allow_empty_pass + "]"); + LdapConfig.ldap_allow_empty_pass = true; // restoring default value for other tests + mockedEnv.close(); + } + + @BeforeEach + public void setUp() { + LdapConfig.ldap_allow_empty_pass = true; // restoring default value for other tests + Auth realAuth = Env.getCurrentEnv().getAuth(); + + mockedEnv = Mockito.mockStatic(Env.class); + Env mockedEnvInstance = Mockito.mock(Env.class); + + Auth authSpy = Mockito.spy(realAuth); + + Mockito.when(ldapManager.doesUserExist(Mockito.anyString())).thenReturn(true); + Mockito.when(ldapManager.checkUserPasswd( + Mockito.anyString(), Mockito.anyString(), + Mockito.anyString(), Mockito.any())).thenReturn(true); + + Mockito.when(authSpy.getLdapManager()).thenReturn(ldapManager); + + Mockito.when(mockedEnvInstance.getAuth()).thenReturn(authSpy); + mockedEnv.when(Env::getCurrentEnv).thenReturn(mockedEnvInstance); + } +}