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 a6108e2d1b2469..e909823aacbf47 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 @@ -6594,6 +6594,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 da4665eb7d37f5..bfafb0ac686514 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 @@ -37,6 +37,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; @@ -257,12 +258,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); } @@ -273,6 +270,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/catalog/EnvTest.java b/fe/fe-core/src/test/java/org/apache/doris/catalog/EnvTest.java index 436e012d9f75d9..6e4ab8e9f688ed 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,15 +17,20 @@ 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 mockit.Expectations; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import java.io.BufferedInputStream; import java.io.DataInputStream; @@ -34,6 +39,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 { @@ -140,4 +148,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 70361675054eea..37492c799be282 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,52 +17,73 @@ 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.authenticate.TestLogAppender; +import org.apache.doris.mysql.privilege.Auth; +import org.apache.doris.mysql.privilege.Role; -import mockit.Expectations; -import mockit.Mocked; import org.apache.logging.log4j.Level; 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"; - @Mocked - private LdapClient ldapClient; + 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) { - new Expectations() { - { - ldapClient.doesUserExist(anyString); - minTimes = 0; - result = userExist; - - ldapClient.checkPassword(anyString, anyString); - minTimes = 0; - result = passwd; - - ldapClient.getGroups(anyString); - minTimes = 0; - result = new ArrayList<>(); - } - }; + 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(groups); + } + + 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(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); } @Test public void testGetUserInfo() { LdapManager ldapManager = new LdapManager(); + Deencapsulation.setField(ldapManager, "ldapClient", ldapClient); mockClient(true, true); LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1); Assert.assertNotNull(ldapUserInfo); @@ -77,6 +98,7 @@ public void testGetUserInfo() { @Test public void testCheckUserPasswd() { LdapManager ldapManager = new LdapManager(); + Deencapsulation.setField(ldapManager, "ldapClient", ldapClient); mockClient(true, true); Assert.assertTrue(ldapManager.checkUserPasswd(USER1, "123")); LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1); @@ -91,6 +113,7 @@ public void testCheckUserPasswd() { @Test public void testCheckUserPasswdCachedPasswdMatchLogsInfoWithoutThreshold() { LdapManager ldapManager = new LdapManager(); + Deencapsulation.setField(ldapManager, "ldapClient", ldapClient); mockClient(true, true); Assert.assertTrue(ldapManager.checkUserPasswd(USER1, "123")); @@ -106,6 +129,7 @@ public void testCheckUserPasswdCachedPasswdMatchLogsInfoWithoutThreshold() { @Test public void testGetUserInfoLogsInfoWithoutThreshold() { LdapManager ldapManager = new LdapManager(); + Deencapsulation.setField(ldapManager, "ldapClient", ldapClient); mockClient(true, true); try (TestLogAppender appender = TestLogAppender.attach(LdapManager.class)) { @@ -116,4 +140,80 @@ public void testGetUserInfoLogsInfoWithoutThreshold() { "LdapManager.getUserInfo slow: user=user1")); } } + + @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(); + 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()); + } + } }