Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c3ffdd7
feat: add property to manage usage of empty pass for login
iaorekhov-1980 Jan 26, 2026
f04ba72
feat: added logic and test to support allow_empty_pass mode
iaorekhov-1980 Jan 26, 2026
ab5be69
fix: fixing indentation
iaorekhov-1980 Jan 29, 2026
f9742cb
fix: fix indentation
iaorekhov-1980 Jan 30, 2026
ca64c42
fix: improve test to restore property before test
iaorekhov-1980 Feb 6, 2026
267c2e3
fix import order
iaorekhov-1980 Feb 6, 2026
9150fc7
review: introduced changes suggested by copilot review
iaorekhov-1980 Mar 26, 2026
b5be138
review: line was too long
iaorekhov-1980 Mar 26, 2026
76a24b2
review: indentantion was fixed
iaorekhov-1980 Mar 26, 2026
f17892a
review: introduced test for null values and checkPlainPassword() method
iaorekhov-1980 Mar 26, 2026
02961ea
review: fix indents and imports
iaorekhov-1980 Mar 26, 2026
4ed3764
review: fix indents and imports
iaorekhov-1980 Mar 26, 2026
3b9f87b
review: fix indents and imports v2
iaorekhov-1980 Mar 26, 2026
46fbe18
review: fixed imports v2
iaorekhov-1980 Mar 27, 2026
3935ab7
review: fixed imports v3
iaorekhov-1980 Mar 27, 2026
9cd5bf3
review: fixed imports v3
iaorekhov-1980 Mar 27, 2026
ed848d9
review: tests reworked v1
iaorekhov-1980 Mar 27, 2026
d1e504b
review: tests reworked v2
iaorekhov-1980 Mar 27, 2026
92c296f
review: tests reworked v3
iaorekhov-1980 Mar 27, 2026
afca8a5
review: fixed imports v4
iaorekhov-1980 Mar 27, 2026
ac74562
review: fix tests for plain auth
iaorekhov-1980 Mar 30, 2026
2629f6b
review: fix tests for plain auth v2
iaorekhov-1980 Mar 30, 2026
a65fad1
review: fix tests for plain auth v3
iaorekhov-1980 Mar 30, 2026
2363403
review: fix tests for plain auth v4
iaorekhov-1980 Mar 30, 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 @@ -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 LDAP user '%s'. Set ldap_allow_empty_pass=true to allow.");

// 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(mutable = true)
public static boolean ldap_allow_empty_pass = true;
}
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;
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:{} 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 @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// 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 mockit.Expectations;
import mockit.Mocked;
import org.junit.After;
import org.junit.Before;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class PlainAuthWithEmptyPasswordAndLdapTest extends TestWithFeService {
private static final String USER_NAME = "user";
private static final String IP = "192.168.1.1";

@Mocked
private LdapManager ldapManager;

private void setLdapUserExist(boolean res) {
new Expectations() {
{
ldapManager.doesUserExist(anyString);
minTimes = 0;
result = res;
}
};
}

private void setCheckPassword(boolean res) {
new Expectations() {
{
ldapManager.checkUserPasswd(anyString, anyString, anyString, null);
minTimes = 0;
result = res;
}
};
}

@Test
public void testPlainPasswordAuthWithNonEmptyPassword() throws Exception {
System.out.println("1.0 [" + LdapConfig.ldap_allow_empty_pass + "]");
setLdapUserExist(true);
setCheckPassword(true);

//test with true value and non-empty pass: login is allowed
LdapConfig.ldap_allow_empty_pass = true;
System.out.println("1.1 [" + LdapConfig.ldap_allow_empty_pass + "]");
Env.getCurrentEnv().getAuth().checkPlainPassword(USER_NAME, IP, "testPass", null);

//test with false value and non-empty pass: login is allowed
LdapConfig.ldap_allow_empty_pass = false;
System.out.println("1.2 [" + LdapConfig.ldap_allow_empty_pass + "]");
Env.getCurrentEnv().getAuth().checkPlainPassword(USER_NAME, IP, "testPass", null);
}


@Test
public void testPlainPasswordAuthWithEmptyPasswordAllowed() throws Exception {
System.out.println("2.0 [" + LdapConfig.ldap_allow_empty_pass + "]");
setLdapUserExist(true);
setCheckPassword(true);

System.out.println("2.1 [" + LdapConfig.ldap_allow_empty_pass + "]");
//test default (ldap_allow_empty_pass=true) and empty pass: login is allowed
Env.getCurrentEnv().getAuth().checkPlainPassword(USER_NAME, IP, "", null);

//test with true and empty pass: login is allowed
LdapConfig.ldap_allow_empty_pass = true;
System.out.println("2.2 [" + LdapConfig.ldap_allow_empty_pass + "]");
Env.getCurrentEnv().getAuth().checkPlainPassword(USER_NAME, IP, "", null);
}

@Test
public void testPlainPasswordAuthWithEmptyPasswordProhibitted() throws Exception {
System.out.println("3.0 [" + LdapConfig.ldap_allow_empty_pass + "]");
setLdapUserExist(true);
setCheckPassword(true);

//test with false and empty pass: login is not allowed
LdapConfig.ldap_allow_empty_pass = false;
System.out.println("3.1 [" + LdapConfig.ldap_allow_empty_pass + "]");
Assertions.assertThrows(AuthenticationException.class, () -> {
Env.getCurrentEnv().getAuth().checkPlainPassword(USER_NAME, IP, null, null);
});

//test with false and null pass: login is not allowed
LdapConfig.ldap_allow_empty_pass = false;
System.out.println("3.2 [" + LdapConfig.ldap_allow_empty_pass + "]");
Assertions.assertThrows(AuthenticationException.class, () -> {
Env.getCurrentEnv().getAuth().checkPlainPassword(USER_NAME, IP, "", null);
});
}

@After
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
}

@Before
public void setUp() {
System.out.println("5.0 [" + LdapConfig.ldap_allow_empty_pass + "]");
LdapConfig.ldap_allow_empty_pass = true; //restoring default value for other tests
}
}
Loading