Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
156c2fe
feat: add property to manage usage of empty pass for login
iaorekhov-1980 Jan 26, 2026
95ad00c
:
iaorekhov-1980 Jan 26, 2026
16c20ee
fix: fixing indentation
iaorekhov-1980 Jan 29, 2026
59c509c
fix: fix indentation
iaorekhov-1980 Jan 30, 2026
624c1cd
fix: improve test to restore property before test
iaorekhov-1980 Feb 6, 2026
1bd81e5
fix import order
iaorekhov-1980 Feb 6, 2026
1cc2f09
review: introduced changes suggested by copilot review
iaorekhov-1980 Mar 26, 2026
7be33e0
review: line was too long
iaorekhov-1980 Mar 26, 2026
01c9032
review: indentantion was fixed
iaorekhov-1980 Mar 26, 2026
ac6288d
review: introduced test for null values and checkPlainPassword() method
iaorekhov-1980 Mar 26, 2026
3752fb7
review: fix indents and imports
iaorekhov-1980 Mar 26, 2026
41c0f46
review: fix indents and imports
iaorekhov-1980 Mar 26, 2026
d104162
review: fix indents and imports v2
iaorekhov-1980 Mar 26, 2026
894618b
review: fixed imports v2
iaorekhov-1980 Mar 27, 2026
4ae7c48
review: fixed imports v3
iaorekhov-1980 Mar 27, 2026
b6fc118
review: fixed imports v3
iaorekhov-1980 Mar 27, 2026
19bdc4a
review: tests reworked v1
iaorekhov-1980 Mar 27, 2026
eed46fe
review: tests reworked v3
iaorekhov-1980 Mar 27, 2026
c2fc809
review: fixed imports v4
iaorekhov-1980 Mar 27, 2026
8fb47a5
review: fix tests for plain auth
iaorekhov-1980 Mar 30, 2026
8e28fb4
review: fix tests for plain auth v2
iaorekhov-1980 Mar 30, 2026
c5163f5
review: fix tests for plain auth v3
iaorekhov-1980 Mar 30, 2026
455cc18
review: fix tests for plain auth v4
iaorekhov-1980 Mar 30, 2026
335c3fd
review: fix tests for plain auth v4
iaorekhov-1980 Mar 30, 2026
434d325
review: fix tests for plain auth v5
iaorekhov-1980 Mar 30, 2026
1c4136a
review: fix tests for plain auth v6
iaorekhov-1980 Mar 30, 2026
174612e
review: fix tests for plain auth v7 - without mutable
iaorekhov-1980 Mar 30, 2026
1c3b4c3
review: fix tests for plain auth v7 - with mutable and junit5
iaorekhov-1980 Mar 30, 2026
411f5b5
review: fix tests for plain auth v8 - without spaces
iaorekhov-1980 Mar 30, 2026
501370e
fixing imports after rebase
iaorekhov-1980 Apr 13, 2026
e906e09
fixing tests to new mock framework after rebase
iaorekhov-1980 Apr 13, 2026
d6c2c30
fixing tests to new mock framework after rebase
iaorekhov-1980 Apr 13, 2026
84e3ee7
fixing imports after rebase
iaorekhov-1980 Apr 13, 2026
02fbec8
test: changed test approach for mockito
iaorekhov-1980 Apr 14, 2026
7c689f6
test: changed test approach for mockito v2
iaorekhov-1980 Apr 14, 2026
ff6e23c
test: changed test approach for mockito v3
iaorekhov-1980 Apr 14, 2026
05452e7
test: added debug
iaorekhov-1980 Apr 14, 2026
eb3dfe2
test: added debug v2
iaorekhov-1980 Apr 14, 2026
e191cc6
test: changed test approach for mockito v3 - correct method mocked
iaorekhov-1980 Apr 14, 2026
343022a
test: changed test approach for mockito v3 - correct method mocked v2
iaorekhov-1980 Apr 14, 2026
bfa93c3
test: changed test approach for mockito v3 - correct method mocked v3
iaorekhov-1980 Apr 14, 2026
08a436a
test: changed test approach for mockito v3 - correct method mocked v4
iaorekhov-1980 Apr 14, 2026
ea7eb0c
test: changed test approach for mockito v3 - use getLdapManager inste…
iaorekhov-1980 Apr 14, 2026
e168964
test: changed test approach for mockito v3 - refactor v1
iaorekhov-1980 Apr 14, 2026
f61764c
review: extra test case added
iaorekhov-1980 Apr 15, 2026
49f6f69
review: extra test case added v1
iaorekhov-1980 Apr 15, 2026
df9e737
review: extra test case added
iaorekhov-1980 Apr 15, 2026
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 @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -220,8 +231,10 @@ 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)
Expand All @@ -233,6 +246,39 @@ void testAuthenticateEmptyPassword() throws Exception {
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)
.credential("".getBytes(StandardCharsets.UTF_8))
.build();

AuthenticationResult result = plugin.authenticate(request, integration);

Assertions.assertTrue(result.isFailure());
}

// ==================== Group Extraction Tests ====================

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");

Comment thread
iaorekhov-1980 marked this conversation as resolved.
// This is error code
private final int code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
public static boolean ldap_allow_empty_pass = true;
Comment thread
iaorekhov-1980 marked this conversation as resolved.
Comment thread
iaorekhov-1980 marked this conversation as resolved.
}
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;
Comment thread
iaorekhov-1980 marked this conversation as resolved.
Expand Down Expand Up @@ -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.
Expand All @@ -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)) {
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 @@ -226,9 +227,17 @@ public Set<String> getRolesByUser(UserIdentity user, boolean showUserDefaultRole
public void checkPlainPassword(String remoteUser, String remoteHost, String remotePasswd,
List<UserIdentity> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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<Env> 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);
}
}
Loading