From ecff0457699528a9b298405b1668e9d0867182a3 Mon Sep 17 00:00:00 2001 From: Raiden Date: Tue, 19 May 2026 22:27:19 +0800 Subject: [PATCH 1/2] [improvement](fe) Support LDAP default roles Issue Number: N/A Related PR: N/A Problem Summary: LDAP temporary users could only receive roles mapped from LDAP groups and the built-in information_schema-only role. Add ldap_default_roles so every LDAP-authenticated user can receive configured Doris roles while still keeping LDAP group roles. Support configuring default Doris roles for LDAP-authenticated users through ldap_default_roles. - Test: Unit Test - Ran `env PATH=/private/tmp/doris-brew-shim:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin FE_UT_PARALLEL=1 JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home CUSTOM_MVN=/Users/zhanggen/.m2/wrapper/dists/apache-maven-3.9.5-bin/32db9c34/apache-maven-3.9.5/bin/mvn ./run-fe-ut.sh --run 'org.apache.doris.mysql.authenticate.ldap.LdapManagerTest#testGetUserInfoWithLdapDefaultRoles'` - Ran `env PATH=/private/tmp/doris-brew-shim:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin FE_UT_PARALLEL=1 JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home CUSTOM_MVN=/Users/zhanggen/.m2/wrapper/dists/apache-maven-3.9.5-bin/32db9c34/apache-maven-3.9.5/bin/mvn ./run-fe-ut.sh --run org.apache.doris.mysql.authenticate.ldap.LdapManagerTest` - Ran `env JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home /Users/zhanggen/.m2/wrapper/dists/apache-maven-3.9.5-bin/32db9c34/apache-maven-3.9.5/bin/mvn checkstyle:check -pl fe-core` - Behavior changed: Yes. LDAP-authenticated users can receive configured default Doris roles in addition to LDAP group roles, and online updates of ldap_default_roles refresh LDAP user cache. - Does this need documentation: Yes. Added ldap.conf template entry. --- conf/ldap.conf | 3 ++ .../org/apache/doris/common/LdapConfig.java | 6 +++ .../java/org/apache/doris/catalog/Env.java | 3 ++ .../mysql/authenticate/ldap/LdapManager.java | 30 +++++++++--- .../authenticate/ldap/LdapManagerTest.java | 47 ++++++++++++++++++- 5 files changed, 82 insertions(+), 7 deletions(-) diff --git a/conf/ldap.conf b/conf/ldap.conf index 9388ae7ee50b1e..00647819273ce1 100644 --- a/conf/ldap.conf +++ b/conf/ldap.conf @@ -31,6 +31,8 @@ # ldap_user_filter - User lookup filter, the placeholder {login} will be replaced by the user supplied login. # ldap_group_basedn - Search base for groups. # ldap_group_filter - Group lookup filter, the placeholder {login} will be replaced by the user supplied login. example : "(&(memberUid={login}))" +# ldap_default_roles - Comma-separated Doris roles granted to every LDAP-authenticated user. +# Online updates of ldap_default_roles refresh the LDAP user cache automatically. ## step2: Restart fe, and use root or admin account to log in to doris. ## step3: Execute sql statement to set ldap admin password: # set ldap_admin_password = 'password'; @@ -41,6 +43,7 @@ ldap_admin_name = cn=admin,dc=domain,dc=com ldap_user_basedn = ou=people,dc=domain,dc=com ldap_user_filter = (&(uid={login})) ldap_group_basedn = ou=group,dc=domain,dc=com +# ldap_default_roles = ldap_default_role # ldap_user_cache_timeout_s = 5 * 60; 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..82966af525b0d4 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 @@ -72,6 +72,12 @@ public class LdapConfig extends ConfigBase { @ConfigBase.ConfField public static String ldap_group_filter = ""; + /** + * Default Doris roles granted to every LDAP-authenticated user. + */ + @ConfigBase.ConfField(mutable = true) + public static String[] ldap_default_roles = {}; + /** * The user LDAP information cache time. * After timeout, the user information will be retrieved from the LDAP service again. diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java index 53b6a67cdb57e1..b13666a2a744f8 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java +++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java @@ -6790,6 +6790,9 @@ public void replayDropGlobalFunction(FunctionSearchDesc functionSearchDesc) { */ public void setMutableConfigWithCallback(String key, String value) throws ConfigException { ConfigBase.setMutableConfig(key, value); + if ("ldap_default_roles".equals(key)) { + getAuth().getLdapManager().refresh(true, null); + } if (configtoThreads.get(key) != null) { try { // not atomic. maybe delay to aware. but acceptable. diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java index f279647f2a5134..2e1e1a26ecbb2b 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java @@ -36,6 +36,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -256,12 +257,8 @@ private Set getLdapGroupsRoles(String userName) throws DdlException { // get user ldap group. the ldap group name should be the same as the doris role name List ldapGroups = ldapClient.getGroups(userName); Set roles = Sets.newHashSet(); - for (String group : ldapGroups) { - String qualifiedRole = group; - if (Env.getCurrentEnv().getAuth().doesRoleExist(qualifiedRole)) { - roles.add(Env.getCurrentEnv().getAuth().getRoleByName(qualifiedRole)); - } - } + addExistingRoles(roles, ldapGroups, false); + addExistingRoles(roles, Arrays.asList(LdapConfig.ldap_default_roles), true); if (LOG.isDebugEnabled()) { LOG.debug("get user:{} ldap groups:{} and doris roles:{}", userName, ldapGroups, roles); } @@ -272,6 +269,27 @@ private Set getLdapGroupsRoles(String userName) throws DdlException { return roles; } + private void addExistingRoles(Set roles, Iterable roleNames, boolean warnIfMissing) { + Auth auth = null; + for (String roleName : roleNames) { + if (Strings.isNullOrEmpty(roleName)) { + continue; + } + String qualifiedRole = roleName.trim(); + if (Strings.isNullOrEmpty(qualifiedRole)) { + continue; + } + if (auth == null) { + auth = Env.getCurrentEnv().getAuth(); + } + if (auth.doesRoleExist(qualifiedRole)) { + roles.add(auth.getRoleByName(qualifiedRole)); + } else if (warnIfMissing) { + LOG.warn("LDAP default role {} does not exist in Doris, ignore it.", qualifiedRole); + } + } + } + public void refresh(boolean isAll, String fullName) { writeLock(); try { diff --git a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java index c3c08e606bda8d..c9bd974f21837f 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java @@ -17,32 +17,58 @@ package org.apache.doris.mysql.authenticate.ldap; +import org.apache.doris.catalog.Env; import org.apache.doris.common.Config; +import org.apache.doris.common.LdapConfig; import org.apache.doris.common.jmockit.Deencapsulation; +import org.apache.doris.mysql.privilege.Auth; +import org.apache.doris.mysql.privilege.Role; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.MockedStatic; import org.mockito.Mockito; import java.util.ArrayList; +import java.util.Arrays; public class LdapManagerTest { private static final String USER1 = "user1"; private static final String USER2 = "user2"; + private static final String LDAP_GROUP_ROLE = "ldap_group_role"; + private static final String LDAP_DEFAULT_ROLE = "ldap_default_role"; + private static final String MISSING_LDAP_DEFAULT_ROLE = "missing_ldap_default_role"; private LdapClient ldapClient = Mockito.mock(LdapClient.class); @Before public void setUp() { Config.authentication_type = "ldap"; + LdapConfig.ldap_default_roles = new String[0]; } private void mockClient(boolean userExist, boolean passwd) { + mockClient(userExist, passwd, new ArrayList<>()); + } + + private void mockClient(boolean userExist, boolean passwd, ArrayList groups) { Mockito.when(ldapClient.doesUserExist(Mockito.anyString())).thenReturn(userExist); Mockito.when(ldapClient.checkPassword(Mockito.anyString(), Mockito.anyString())).thenReturn(passwd); - Mockito.when(ldapClient.getGroups(Mockito.anyString())).thenReturn(new ArrayList<>()); + Mockito.when(ldapClient.getGroups(Mockito.anyString())).thenReturn(groups); + } + + private void mockAuth(MockedStatic envMockedStatic, Role ldapGroupRole, Role ldapDefaultRole) { + Env env = Mockito.mock(Env.class); + Auth auth = Mockito.mock(Auth.class); + envMockedStatic.when(Env::getCurrentEnv).thenReturn(env); + Mockito.when(env.getAuth()).thenReturn(auth); + Mockito.when(auth.doesRoleExist(LDAP_GROUP_ROLE)).thenReturn(true); + Mockito.when(auth.getRoleByName(LDAP_GROUP_ROLE)).thenReturn(ldapGroupRole); + Mockito.when(auth.doesRoleExist(LDAP_DEFAULT_ROLE)).thenReturn(true); + Mockito.when(auth.getRoleByName(LDAP_DEFAULT_ROLE)).thenReturn(ldapDefaultRole); + Mockito.when(auth.doesRoleExist(MISSING_LDAP_DEFAULT_ROLE)).thenReturn(false); } @Test @@ -74,4 +100,23 @@ public void testCheckUserPasswd() { mockClient(true, false); Assert.assertFalse(ldapManager.checkUserPasswd(USER2, "123")); } + + @Test + public void testGetUserInfoWithLdapDefaultRoles() { + LdapManager ldapManager = new LdapManager(); + Deencapsulation.setField(ldapManager, "ldapClient", ldapClient); + LdapConfig.ldap_default_roles = new String[] {LDAP_DEFAULT_ROLE, MISSING_LDAP_DEFAULT_ROLE}; + Role ldapGroupRole = new Role(LDAP_GROUP_ROLE); + Role ldapDefaultRole = new Role(LDAP_DEFAULT_ROLE); + mockClient(true, true, new ArrayList<>(Arrays.asList(LDAP_GROUP_ROLE))); + try (MockedStatic envMockedStatic = Mockito.mockStatic(Env.class)) { + mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole); + + LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1); + Assert.assertNotNull(ldapUserInfo); + Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapGroupRole)); + Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapDefaultRole)); + Assert.assertEquals(3, ldapUserInfo.getRoles().size()); + } + } } From c5017be98dc7e1830afae96f8b98090cdb819597 Mon Sep 17 00:00:00 2001 From: Raiden Date: Thu, 28 May 2026 16:45:27 +0800 Subject: [PATCH 2/2] [test](fe) Add LDAP default role coverage ### What problem does this PR solve? Issue Number: N/A Related PR: N/A Problem Summary: Add FE unit coverage for LDAP default role behavior. The tests verify that blank LDAP default role entries are ignored while valid LDAP group and default roles are preserved, and that online updates of ldap_default_roles refresh LDAP user cache through Env.setMutableConfigWithCallback. ### Release note None ### Check List (For Author) - Test: Unit Test - Ran `env PATH=/private/tmp/doris-brew-shim:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin FE_UT_PARALLEL=1 JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home CUSTOM_MVN=/Users/zhanggen/.m2/wrapper/dists/apache-maven-3.9.5-bin/32db9c34/apache-maven-3.9.5/bin/mvn ./run-fe-ut.sh --coverage --run 'org.apache.doris.mysql.authenticate.ldap.LdapManagerTest,org.apache.doris.catalog.EnvTest'` - Behavior changed: No - Does this need documentation: No --- .../org/apache/doris/catalog/EnvTest.java | 37 ++++++++++ .../authenticate/ldap/LdapManagerTest.java | 68 ++++++++++++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/fe/fe-core/src/test/java/org/apache/doris/catalog/EnvTest.java b/fe/fe-core/src/test/java/org/apache/doris/catalog/EnvTest.java index 69a1898a9f1e8c..fe3f4aebadde40 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/catalog/EnvTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/EnvTest.java @@ -17,9 +17,13 @@ package org.apache.doris.catalog; +import org.apache.doris.common.ConfigBase; import org.apache.doris.common.FeConstants; +import org.apache.doris.common.LdapConfig; import org.apache.doris.common.io.CountingDataOutputStream; import org.apache.doris.meta.MetaContext; +import org.apache.doris.mysql.authenticate.ldap.LdapManager; +import org.apache.doris.mysql.privilege.Auth; import org.apache.doris.persist.meta.MetaHeader; import org.junit.After; @@ -36,6 +40,9 @@ import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; import java.util.Random; public class EnvTest { @@ -146,4 +153,34 @@ public void testSaveLoadHeader() throws Exception { deleteDir(dir); } + + @Test + public void testSetLdapDefaultRolesConfigRefreshesLdapCache() throws Exception { + Env env = Mockito.spy(new Env(false)); + Auth auth = Mockito.mock(Auth.class); + LdapManager ldapManager = Mockito.mock(LdapManager.class); + Mockito.doReturn(auth).when(env).getAuth(); + Mockito.when(auth.getLdapManager()).thenReturn(ldapManager); + + Map oldConfFields = ConfigBase.confFields; + Field oldLdapDefaultRolesField = ConfigBase.ldapConfFields.put("ldap_default_roles", + LdapConfig.class.getField("ldap_default_roles")); + String[] oldLdapDefaultRoles = LdapConfig.ldap_default_roles; + try { + ConfigBase.confFields = new HashMap<>(); + + env.setMutableConfigWithCallback("ldap_default_roles", "role1,role2"); + + Assert.assertArrayEquals(new String[] {"role1", "role2"}, LdapConfig.ldap_default_roles); + Mockito.verify(ldapManager).refresh(true, null); + } finally { + ConfigBase.confFields = oldConfFields; + if (oldLdapDefaultRolesField == null) { + ConfigBase.ldapConfFields.remove("ldap_default_roles"); + } else { + ConfigBase.ldapConfFields.put("ldap_default_roles", oldLdapDefaultRolesField); + } + LdapConfig.ldap_default_roles = oldLdapDefaultRoles; + } + } } diff --git a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java index c9bd974f21837f..64fffd2c71dbb9 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java @@ -60,12 +60,19 @@ private void mockClient(boolean userExist, boolean passwd, ArrayList gro } private void mockAuth(MockedStatic envMockedStatic, Role ldapGroupRole, Role ldapDefaultRole) { + mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole, true); + } + + private void mockAuth(MockedStatic envMockedStatic, Role ldapGroupRole, Role ldapDefaultRole, + boolean ldapGroupRoleExists) { Env env = Mockito.mock(Env.class); Auth auth = Mockito.mock(Auth.class); envMockedStatic.when(Env::getCurrentEnv).thenReturn(env); Mockito.when(env.getAuth()).thenReturn(auth); - Mockito.when(auth.doesRoleExist(LDAP_GROUP_ROLE)).thenReturn(true); - Mockito.when(auth.getRoleByName(LDAP_GROUP_ROLE)).thenReturn(ldapGroupRole); + Mockito.when(auth.doesRoleExist(LDAP_GROUP_ROLE)).thenReturn(ldapGroupRoleExists); + if (ldapGroupRoleExists) { + Mockito.when(auth.getRoleByName(LDAP_GROUP_ROLE)).thenReturn(ldapGroupRole); + } Mockito.when(auth.doesRoleExist(LDAP_DEFAULT_ROLE)).thenReturn(true); Mockito.when(auth.getRoleByName(LDAP_DEFAULT_ROLE)).thenReturn(ldapDefaultRole); Mockito.when(auth.doesRoleExist(MISSING_LDAP_DEFAULT_ROLE)).thenReturn(false); @@ -101,6 +108,63 @@ public void testCheckUserPasswd() { Assert.assertFalse(ldapManager.checkUserPasswd(USER2, "123")); } + @Test + public void testGetUserInfoWithLdapDefaultRolesWithoutLdapGroups() { + LdapManager ldapManager = new LdapManager(); + Deencapsulation.setField(ldapManager, "ldapClient", ldapClient); + LdapConfig.ldap_default_roles = new String[] {LDAP_DEFAULT_ROLE, MISSING_LDAP_DEFAULT_ROLE}; + Role ldapGroupRole = new Role(LDAP_GROUP_ROLE); + Role ldapDefaultRole = new Role(LDAP_DEFAULT_ROLE); + mockClient(true, true, new ArrayList<>()); + try (MockedStatic envMockedStatic = Mockito.mockStatic(Env.class)) { + mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole); + + LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1); + Assert.assertNotNull(ldapUserInfo); + Assert.assertFalse(ldapUserInfo.getRoles().contains(ldapGroupRole)); + Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapDefaultRole)); + Assert.assertEquals(2, ldapUserInfo.getRoles().size()); + } + } + + @Test + public void testGetUserInfoWithLdapDefaultRolesWhenLdapGroupRoleMissing() { + LdapManager ldapManager = new LdapManager(); + Deencapsulation.setField(ldapManager, "ldapClient", ldapClient); + LdapConfig.ldap_default_roles = new String[] {LDAP_DEFAULT_ROLE, MISSING_LDAP_DEFAULT_ROLE}; + Role ldapGroupRole = new Role(LDAP_GROUP_ROLE); + Role ldapDefaultRole = new Role(LDAP_DEFAULT_ROLE); + mockClient(true, true, new ArrayList<>(Arrays.asList(LDAP_GROUP_ROLE))); + try (MockedStatic envMockedStatic = Mockito.mockStatic(Env.class)) { + mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole, false); + + LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1); + Assert.assertNotNull(ldapUserInfo); + Assert.assertFalse(ldapUserInfo.getRoles().contains(ldapGroupRole)); + Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapDefaultRole)); + Assert.assertEquals(2, ldapUserInfo.getRoles().size()); + } + } + + @Test + public void testGetUserInfoWithBlankLdapDefaultRoles() { + LdapManager ldapManager = new LdapManager(); + Deencapsulation.setField(ldapManager, "ldapClient", ldapClient); + LdapConfig.ldap_default_roles = new String[] {null, "", " ", LDAP_DEFAULT_ROLE}; + Role ldapGroupRole = new Role(LDAP_GROUP_ROLE); + Role ldapDefaultRole = new Role(LDAP_DEFAULT_ROLE); + mockClient(true, true, new ArrayList<>(Arrays.asList(LDAP_GROUP_ROLE))); + try (MockedStatic envMockedStatic = Mockito.mockStatic(Env.class)) { + mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole); + + LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1); + Assert.assertNotNull(ldapUserInfo); + Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapGroupRole)); + Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapDefaultRole)); + Assert.assertEquals(3, ldapUserInfo.getRoles().size()); + } + } + @Test public void testGetUserInfoWithLdapDefaultRoles() { LdapManager ldapManager = new LdapManager();