Skip to content

Commit 7957ada

Browse files
seawindeJungzhang
andauthored
branch-4.0:[improvement](fe) Support LDAP default roles (#63411) (#64115)
pr: #63411 commitId: 866a2f9 Co-authored-by: Raiden <zhanggen.Jung@gmail.com>
1 parent 013238f commit 7957ada

6 files changed

Lines changed: 193 additions & 25 deletions

File tree

conf/ldap.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
# ldap_user_filter - User lookup filter, the placeholder {login} will be replaced by the user supplied login.
3232
# ldap_group_basedn - Search base for groups.
3333
# ldap_group_filter - Group lookup filter, the placeholder {login} will be replaced by the user supplied login. example : "(&(memberUid={login}))"
34+
# ldap_default_roles - Comma-separated Doris roles granted to every LDAP-authenticated user.
35+
# Online updates of ldap_default_roles refresh the LDAP user cache automatically.
3436
## step2: Restart fe, and use root or admin account to log in to doris.
3537
## step3: Execute sql statement to set ldap admin password:
3638
# set ldap_admin_password = 'password';
@@ -41,6 +43,7 @@ ldap_admin_name = cn=admin,dc=domain,dc=com
4143
ldap_user_basedn = ou=people,dc=domain,dc=com
4244
ldap_user_filter = (&(uid={login}))
4345
ldap_group_basedn = ou=group,dc=domain,dc=com
46+
# ldap_default_roles = ldap_default_role
4447

4548
# ldap_user_cache_timeout_s = 5 * 60;
4649

fe/fe-common/src/main/java/org/apache/doris/common/LdapConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ public class LdapConfig extends ConfigBase {
7272
@ConfigBase.ConfField
7373
public static String ldap_group_filter = "";
7474

75+
/**
76+
* Default Doris roles granted to every LDAP-authenticated user.
77+
*/
78+
@ConfigBase.ConfField(mutable = true)
79+
public static String[] ldap_default_roles = {};
80+
7581
/**
7682
* The user LDAP information cache time.
7783
* After timeout, the user information will be retrieved from the LDAP service again.

fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6594,6 +6594,9 @@ public void replayDropGlobalFunction(FunctionSearchDesc functionSearchDesc) {
65946594
*/
65956595
public void setMutableConfigWithCallback(String key, String value) throws ConfigException {
65966596
ConfigBase.setMutableConfig(key, value);
6597+
if ("ldap_default_roles".equals(key)) {
6598+
getAuth().getLdapManager().refresh(true, null);
6599+
}
65976600
if (configtoThreads.get(key) != null) {
65986601
try {
65996602
// not atomic. maybe delay to aware. but acceptable.

fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.apache.logging.log4j.LogManager;
3838
import org.apache.logging.log4j.Logger;
3939

40+
import java.util.Arrays;
4041
import java.util.Collections;
4142
import java.util.List;
4243
import java.util.Map;
@@ -257,12 +258,8 @@ private Set<Role> getLdapGroupsRoles(String userName) throws DdlException {
257258
// get user ldap group. the ldap group name should be the same as the doris role name
258259
List<String> ldapGroups = ldapClient.getGroups(userName);
259260
Set<Role> roles = Sets.newHashSet();
260-
for (String group : ldapGroups) {
261-
String qualifiedRole = group;
262-
if (Env.getCurrentEnv().getAuth().doesRoleExist(qualifiedRole)) {
263-
roles.add(Env.getCurrentEnv().getAuth().getRoleByName(qualifiedRole));
264-
}
265-
}
261+
addExistingRoles(roles, ldapGroups, false);
262+
addExistingRoles(roles, Arrays.asList(LdapConfig.ldap_default_roles), true);
266263
if (LOG.isDebugEnabled()) {
267264
LOG.debug("get user:{} ldap groups:{} and doris roles:{}", userName, ldapGroups, roles);
268265
}
@@ -273,6 +270,27 @@ private Set<Role> getLdapGroupsRoles(String userName) throws DdlException {
273270
return roles;
274271
}
275272

273+
private void addExistingRoles(Set<Role> roles, Iterable<String> roleNames, boolean warnIfMissing) {
274+
Auth auth = null;
275+
for (String roleName : roleNames) {
276+
if (Strings.isNullOrEmpty(roleName)) {
277+
continue;
278+
}
279+
String qualifiedRole = roleName.trim();
280+
if (Strings.isNullOrEmpty(qualifiedRole)) {
281+
continue;
282+
}
283+
if (auth == null) {
284+
auth = Env.getCurrentEnv().getAuth();
285+
}
286+
if (auth.doesRoleExist(qualifiedRole)) {
287+
roles.add(auth.getRoleByName(qualifiedRole));
288+
} else if (warnIfMissing) {
289+
LOG.warn("LDAP default role {} does not exist in Doris, ignore it.", qualifiedRole);
290+
}
291+
}
292+
}
293+
276294
public void refresh(boolean isAll, String fullName) {
277295
writeLock();
278296
try {

fe/fe-core/src/test/java/org/apache/doris/catalog/EnvTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717

1818
package org.apache.doris.catalog;
1919

20+
import org.apache.doris.common.ConfigBase;
2021
import org.apache.doris.common.FeConstants;
22+
import org.apache.doris.common.LdapConfig;
2123
import org.apache.doris.common.io.CountingDataOutputStream;
2224
import org.apache.doris.meta.MetaContext;
25+
import org.apache.doris.mysql.authenticate.ldap.LdapManager;
26+
import org.apache.doris.mysql.privilege.Auth;
2327
import org.apache.doris.persist.meta.MetaHeader;
2428

2529
import mockit.Expectations;
2630
import org.junit.Assert;
2731
import org.junit.Before;
2832
import org.junit.Test;
33+
import org.mockito.Mockito;
2934

3035
import java.io.BufferedInputStream;
3136
import java.io.DataInputStream;
@@ -34,6 +39,9 @@
3439
import java.io.FileOutputStream;
3540
import java.io.FileWriter;
3641
import java.io.IOException;
42+
import java.lang.reflect.Field;
43+
import java.util.HashMap;
44+
import java.util.Map;
3745
import java.util.Random;
3846

3947
public class EnvTest {
@@ -140,4 +148,34 @@ public void testSaveLoadHeader() throws Exception {
140148

141149
deleteDir(dir);
142150
}
151+
152+
@Test
153+
public void testSetLdapDefaultRolesConfigRefreshesLdapCache() throws Exception {
154+
Env env = Mockito.spy(new Env(false));
155+
Auth auth = Mockito.mock(Auth.class);
156+
LdapManager ldapManager = Mockito.mock(LdapManager.class);
157+
Mockito.doReturn(auth).when(env).getAuth();
158+
Mockito.when(auth.getLdapManager()).thenReturn(ldapManager);
159+
160+
Map<String, Field> oldConfFields = ConfigBase.confFields;
161+
Field oldLdapDefaultRolesField = ConfigBase.ldapConfFields.put("ldap_default_roles",
162+
LdapConfig.class.getField("ldap_default_roles"));
163+
String[] oldLdapDefaultRoles = LdapConfig.ldap_default_roles;
164+
try {
165+
ConfigBase.confFields = new HashMap<>();
166+
167+
env.setMutableConfigWithCallback("ldap_default_roles", "role1,role2");
168+
169+
Assert.assertArrayEquals(new String[] {"role1", "role2"}, LdapConfig.ldap_default_roles);
170+
Mockito.verify(ldapManager).refresh(true, null);
171+
} finally {
172+
ConfigBase.confFields = oldConfFields;
173+
if (oldLdapDefaultRolesField == null) {
174+
ConfigBase.ldapConfFields.remove("ldap_default_roles");
175+
} else {
176+
ConfigBase.ldapConfFields.put("ldap_default_roles", oldLdapDefaultRolesField);
177+
}
178+
LdapConfig.ldap_default_roles = oldLdapDefaultRoles;
179+
}
180+
}
143181
}

fe/fe-core/src/test/java/org/apache/doris/mysql/authenticate/ldap/LdapManagerTest.java

Lines changed: 119 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,52 +17,73 @@
1717

1818
package org.apache.doris.mysql.authenticate.ldap;
1919

20+
import org.apache.doris.catalog.Env;
2021
import org.apache.doris.common.Config;
22+
import org.apache.doris.common.LdapConfig;
23+
import org.apache.doris.common.jmockit.Deencapsulation;
2124
import org.apache.doris.mysql.authenticate.TestLogAppender;
25+
import org.apache.doris.mysql.privilege.Auth;
26+
import org.apache.doris.mysql.privilege.Role;
2227

23-
import mockit.Expectations;
24-
import mockit.Mocked;
2528
import org.apache.logging.log4j.Level;
2629
import org.junit.Assert;
2730
import org.junit.Before;
2831
import org.junit.Test;
32+
import org.mockito.MockedStatic;
33+
import org.mockito.Mockito;
2934

3035
import java.util.ArrayList;
36+
import java.util.Arrays;
3137

3238
public class LdapManagerTest {
3339

3440
private static final String USER1 = "user1";
3541
private static final String USER2 = "user2";
42+
private static final String LDAP_GROUP_ROLE = "ldap_group_role";
43+
private static final String LDAP_DEFAULT_ROLE = "ldap_default_role";
44+
private static final String MISSING_LDAP_DEFAULT_ROLE = "missing_ldap_default_role";
3645

37-
@Mocked
38-
private LdapClient ldapClient;
46+
private LdapClient ldapClient = Mockito.mock(LdapClient.class);
3947

4048
@Before
4149
public void setUp() {
4250
Config.authentication_type = "ldap";
51+
LdapConfig.ldap_default_roles = new String[0];
4352
}
4453

4554
private void mockClient(boolean userExist, boolean passwd) {
46-
new Expectations() {
47-
{
48-
ldapClient.doesUserExist(anyString);
49-
minTimes = 0;
50-
result = userExist;
51-
52-
ldapClient.checkPassword(anyString, anyString);
53-
minTimes = 0;
54-
result = passwd;
55-
56-
ldapClient.getGroups(anyString);
57-
minTimes = 0;
58-
result = new ArrayList<>();
59-
}
60-
};
55+
mockClient(userExist, passwd, new ArrayList<>());
56+
}
57+
58+
private void mockClient(boolean userExist, boolean passwd, ArrayList<String> groups) {
59+
Mockito.when(ldapClient.doesUserExist(Mockito.anyString())).thenReturn(userExist);
60+
Mockito.when(ldapClient.checkPassword(Mockito.anyString(), Mockito.anyString())).thenReturn(passwd);
61+
Mockito.when(ldapClient.getGroups(Mockito.anyString())).thenReturn(groups);
62+
}
63+
64+
private void mockAuth(MockedStatic<Env> envMockedStatic, Role ldapGroupRole, Role ldapDefaultRole) {
65+
mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole, true);
66+
}
67+
68+
private void mockAuth(MockedStatic<Env> envMockedStatic, Role ldapGroupRole, Role ldapDefaultRole,
69+
boolean ldapGroupRoleExists) {
70+
Env env = Mockito.mock(Env.class);
71+
Auth auth = Mockito.mock(Auth.class);
72+
envMockedStatic.when(Env::getCurrentEnv).thenReturn(env);
73+
Mockito.when(env.getAuth()).thenReturn(auth);
74+
Mockito.when(auth.doesRoleExist(LDAP_GROUP_ROLE)).thenReturn(ldapGroupRoleExists);
75+
if (ldapGroupRoleExists) {
76+
Mockito.when(auth.getRoleByName(LDAP_GROUP_ROLE)).thenReturn(ldapGroupRole);
77+
}
78+
Mockito.when(auth.doesRoleExist(LDAP_DEFAULT_ROLE)).thenReturn(true);
79+
Mockito.when(auth.getRoleByName(LDAP_DEFAULT_ROLE)).thenReturn(ldapDefaultRole);
80+
Mockito.when(auth.doesRoleExist(MISSING_LDAP_DEFAULT_ROLE)).thenReturn(false);
6181
}
6282

6383
@Test
6484
public void testGetUserInfo() {
6585
LdapManager ldapManager = new LdapManager();
86+
Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
6687
mockClient(true, true);
6788
LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1);
6889
Assert.assertNotNull(ldapUserInfo);
@@ -77,6 +98,7 @@ public void testGetUserInfo() {
7798
@Test
7899
public void testCheckUserPasswd() {
79100
LdapManager ldapManager = new LdapManager();
101+
Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
80102
mockClient(true, true);
81103
Assert.assertTrue(ldapManager.checkUserPasswd(USER1, "123"));
82104
LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1);
@@ -91,6 +113,7 @@ public void testCheckUserPasswd() {
91113
@Test
92114
public void testCheckUserPasswdCachedPasswdMatchLogsInfoWithoutThreshold() {
93115
LdapManager ldapManager = new LdapManager();
116+
Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
94117
mockClient(true, true);
95118
Assert.assertTrue(ldapManager.checkUserPasswd(USER1, "123"));
96119

@@ -106,6 +129,7 @@ public void testCheckUserPasswdCachedPasswdMatchLogsInfoWithoutThreshold() {
106129
@Test
107130
public void testGetUserInfoLogsInfoWithoutThreshold() {
108131
LdapManager ldapManager = new LdapManager();
132+
Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
109133
mockClient(true, true);
110134

111135
try (TestLogAppender appender = TestLogAppender.attach(LdapManager.class)) {
@@ -116,4 +140,80 @@ public void testGetUserInfoLogsInfoWithoutThreshold() {
116140
"LdapManager.getUserInfo slow: user=user1"));
117141
}
118142
}
143+
144+
@Test
145+
public void testGetUserInfoWithLdapDefaultRolesWithoutLdapGroups() {
146+
LdapManager ldapManager = new LdapManager();
147+
Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
148+
LdapConfig.ldap_default_roles = new String[] {LDAP_DEFAULT_ROLE, MISSING_LDAP_DEFAULT_ROLE};
149+
Role ldapGroupRole = new Role(LDAP_GROUP_ROLE);
150+
Role ldapDefaultRole = new Role(LDAP_DEFAULT_ROLE);
151+
mockClient(true, true, new ArrayList<>());
152+
try (MockedStatic<Env> envMockedStatic = Mockito.mockStatic(Env.class)) {
153+
mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole);
154+
155+
LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1);
156+
Assert.assertNotNull(ldapUserInfo);
157+
Assert.assertFalse(ldapUserInfo.getRoles().contains(ldapGroupRole));
158+
Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapDefaultRole));
159+
Assert.assertEquals(2, ldapUserInfo.getRoles().size());
160+
}
161+
}
162+
163+
@Test
164+
public void testGetUserInfoWithLdapDefaultRolesWhenLdapGroupRoleMissing() {
165+
LdapManager ldapManager = new LdapManager();
166+
Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
167+
LdapConfig.ldap_default_roles = new String[] {LDAP_DEFAULT_ROLE, MISSING_LDAP_DEFAULT_ROLE};
168+
Role ldapGroupRole = new Role(LDAP_GROUP_ROLE);
169+
Role ldapDefaultRole = new Role(LDAP_DEFAULT_ROLE);
170+
mockClient(true, true, new ArrayList<>(Arrays.asList(LDAP_GROUP_ROLE)));
171+
try (MockedStatic<Env> envMockedStatic = Mockito.mockStatic(Env.class)) {
172+
mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole, false);
173+
174+
LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1);
175+
Assert.assertNotNull(ldapUserInfo);
176+
Assert.assertFalse(ldapUserInfo.getRoles().contains(ldapGroupRole));
177+
Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapDefaultRole));
178+
Assert.assertEquals(2, ldapUserInfo.getRoles().size());
179+
}
180+
}
181+
182+
@Test
183+
public void testGetUserInfoWithBlankLdapDefaultRoles() {
184+
LdapManager ldapManager = new LdapManager();
185+
Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
186+
LdapConfig.ldap_default_roles = new String[] {null, "", " ", LDAP_DEFAULT_ROLE};
187+
Role ldapGroupRole = new Role(LDAP_GROUP_ROLE);
188+
Role ldapDefaultRole = new Role(LDAP_DEFAULT_ROLE);
189+
mockClient(true, true, new ArrayList<>(Arrays.asList(LDAP_GROUP_ROLE)));
190+
try (MockedStatic<Env> envMockedStatic = Mockito.mockStatic(Env.class)) {
191+
mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole);
192+
193+
LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1);
194+
Assert.assertNotNull(ldapUserInfo);
195+
Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapGroupRole));
196+
Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapDefaultRole));
197+
Assert.assertEquals(3, ldapUserInfo.getRoles().size());
198+
}
199+
}
200+
201+
@Test
202+
public void testGetUserInfoWithLdapDefaultRoles() {
203+
LdapManager ldapManager = new LdapManager();
204+
Deencapsulation.setField(ldapManager, "ldapClient", ldapClient);
205+
LdapConfig.ldap_default_roles = new String[] {LDAP_DEFAULT_ROLE, MISSING_LDAP_DEFAULT_ROLE};
206+
Role ldapGroupRole = new Role(LDAP_GROUP_ROLE);
207+
Role ldapDefaultRole = new Role(LDAP_DEFAULT_ROLE);
208+
mockClient(true, true, new ArrayList<>(Arrays.asList(LDAP_GROUP_ROLE)));
209+
try (MockedStatic<Env> envMockedStatic = Mockito.mockStatic(Env.class)) {
210+
mockAuth(envMockedStatic, ldapGroupRole, ldapDefaultRole);
211+
212+
LdapUserInfo ldapUserInfo = ldapManager.getUserInfo(USER1);
213+
Assert.assertNotNull(ldapUserInfo);
214+
Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapGroupRole));
215+
Assert.assertTrue(ldapUserInfo.getRoles().contains(ldapDefaultRole));
216+
Assert.assertEquals(3, ldapUserInfo.getRoles().size());
217+
}
218+
}
119219
}

0 commit comments

Comments
 (0)