[Enhancement] Support use DN to match group in group provider (backport #62711) (#62885)

Co-authored-by: Harbor Liu <460660596@qq.com>
This commit is contained in:
mergify[bot] 2025-09-10 13:38:46 +08:00 committed by GitHub
parent 94342fc8e6
commit 2484b49178
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 785 additions and 87 deletions

View File

@ -98,7 +98,7 @@ public class AuthenticationHandler {
provider.authenticate(context, matchedUserIdentity.getKey(), authResponse);
}
return new AuthenticationResult(matchedUserIdentity.getKey(), List.of(Config.group_provider), null);
return new AuthenticationResult(matchedUserIdentity.getKey(), List.of(Config.group_provider), null, "native");
}
}
@ -138,7 +138,8 @@ public class AuthenticationHandler {
UserIdentity.createEphemeralUserIdent(user, remoteHost),
securityIntegration.getGroupProviderName() == null ?
List.of(Config.group_provider) : securityIntegration.getGroupProviderName(),
securityIntegration.getGroupAllowedLoginList());
securityIntegration.getGroupAllowedLoginList(),
authMechanism);
}
if (authenticationResult == null && !exceptions.isEmpty()) {
@ -154,19 +155,38 @@ public class AuthenticationHandler {
throws AuthenticationException {
String user = authenticationResult.authenticatedUser.getUser();
// Step 1: Set user identity to context
// Set the authenticated user identity as the current user for authorization purposes
context.setCurrentUserIdentity(authenticationResult.authenticatedUser);
if (!authenticationResult.authenticatedUser.isEphemeral()) {
context.setCurrentRoleIds(authenticationResult.authenticatedUser);
UserProperty userProperty = GlobalStateMgr.getCurrentState().getAuthenticationMgr()
.getUserProperty(authenticationResult.authenticatedUser.getUser());
context.updateByUserProperty(userProperty);
}
// Set the qualified username for this connection session
context.setQualifiedUser(user);
Set<String> groups = getGroups(authenticationResult.authenticatedUser, authenticationResult.groupProviderName);
context.setGroups(groups);
// Step 2: Set distinguished name to context if it is empty
// Distinguished name is used for LDAP authentication and group resolution
// If not already set, use the username as the distinguished name
if (context.getDistinguishedName().isEmpty()) {
context.setDistinguishedName(user);
}
// Step 3: Set security integration to context
// Record which security integration method was used for authentication
// This helps track authentication method (native, LDAP, OAuth2, etc.)
if (authenticationResult.securityIntegration != null) {
context.setSecurityIntegration(authenticationResult.securityIntegration);
}
// Step 4: Resolve and set user groups
// Get user groups from configured group providers (e.g., LDAP groups)
// Groups are used for role-based access control and permission management
Set<String> groups = getGroups(context.getCurrentUserIdentity(), context.getDistinguishedName(),
authenticationResult.groupProviderName);
context.setGroups(groups);
// Set current role IDs based on the authenticated user and groups
context.setCurrentRoleIds(authenticationResult.authenticatedUser, groups);
// Step 5: Validate group access permissions
// If authentication result specifies allowed groups, verify user belongs to at least one
// This ensures users can only access groups they are authorized for
if (authenticationResult.authenticatedGroupList != null && !authenticationResult.authenticatedGroupList.isEmpty()) {
Set<String> intersection = new HashSet<>(groups);
intersection.retainAll(authenticationResult.authenticatedGroupList);
@ -174,23 +194,35 @@ public class AuthenticationHandler {
throw new AuthenticationException(ErrorCode.ERR_GROUP_ACCESS_DENY, user, Joiner.on(",").join(groups));
}
}
}
private static class AuthenticationResult {
private UserIdentity authenticatedUser = null;
private List<String> groupProviderName = null;
private List<String> authenticatedGroupList = null;
public AuthenticationResult(UserIdentity authenticatedUser,
List<String> groupProviderName,
List<String> authenticatedGroupList) {
this.authenticatedUser = authenticatedUser;
this.groupProviderName = groupProviderName;
this.authenticatedGroupList = authenticatedGroupList;
// Step 6: Apply user properties for non-ephemeral users
// Load and apply user-specific properties (session variables, resource limits, etc.)
// Ephemeral users (from external auth) don't have stored properties
if (!authenticationResult.authenticatedUser.isEphemeral()) {
UserProperty userProperty = GlobalStateMgr.getCurrentState().getAuthenticationMgr()
.getUserProperty(authenticationResult.authenticatedUser.getUser());
context.updateByUserProperty(userProperty);
}
}
public static Set<String> getGroups(UserIdentity userIdentity, List<String> groupProviderList) {
private static class AuthenticationResult {
private final UserIdentity authenticatedUser;
private final List<String> groupProviderName;
private final List<String> authenticatedGroupList;
private final String securityIntegration;
public AuthenticationResult(UserIdentity authenticatedUser,
List<String> groupProviderName,
List<String> authenticatedGroupList,
String securityIntegration) {
this.authenticatedUser = authenticatedUser;
this.groupProviderName = groupProviderName;
this.authenticatedGroupList = authenticatedGroupList;
this.securityIntegration = securityIntegration;
}
}
public static Set<String> getGroups(UserIdentity userIdentity, String distinguishedName, List<String> groupProviderList) {
AuthenticationMgr authenticationMgr = GlobalStateMgr.getCurrentState().getAuthenticationMgr();
HashSet<String> groups = new HashSet<>();
@ -199,7 +231,7 @@ public class AuthenticationHandler {
if (groupProvider == null) {
continue;
}
groups.addAll(groupProvider.getGroup(userIdentity));
groups.addAll(groupProvider.getGroup(userIdentity, distinguishedName));
}
return groups;

View File

@ -643,7 +643,7 @@ public class AuthenticationMgr {
public void dropGroupProviderStatement(DropGroupProviderStmt stmt, ConnectContext context) {
GroupProvider groupProvider = this.nameToGroupProviderMap.remove(stmt.getName());
groupProvider.destory();
groupProvider.destroy();
GlobalStateMgr.getCurrentState().getEditLog().logEdit(OperationType.OP_DROP_GROUP_PROVIDER,
new GroupProviderLog(stmt.getName(), null));
@ -651,7 +651,7 @@ public class AuthenticationMgr {
public void replayDropGroupProvider(String name) {
GroupProvider groupProvider = this.nameToGroupProviderMap.remove(name);
groupProvider.destory();
groupProvider.destroy();
}
public List<GroupProvider> getAllGroupProviders() {

View File

@ -15,12 +15,9 @@
package com.starrocks.authentication;
import com.google.common.annotations.VisibleForTesting;
import com.starrocks.StarRocksFE;
import com.starrocks.common.DdlException;
import com.starrocks.sql.analyzer.SemanticException;
import com.starrocks.sql.ast.UserIdentity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.FileInputStream;
import java.io.IOException;
@ -37,8 +34,6 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class FileGroupProvider extends GroupProvider {
private static final Logger LOG = LogManager.getLogger(FileGroupProvider.class);
public static final String TYPE = "file";
public static final String GROUP_FILE_URL = "group_file_url";
public static final Set<String> REQUIRED_PROPERTIES = new HashSet<>(List.of(FileGroupProvider.GROUP_FILE_URL));
@ -79,7 +74,7 @@ public class FileGroupProvider extends GroupProvider {
}
@Override
public Set<String> getGroup(UserIdentity userIdentity) {
public Set<String> getGroup(UserIdentity userIdentity, String distinguishedName) {
return userGroups.getOrDefault(userIdentity.getUser(), new HashSet<>());
}
@ -97,7 +92,14 @@ public class FileGroupProvider extends GroupProvider {
if (groupFileUrl.startsWith("http://") || groupFileUrl.startsWith("https://")) {
return new URL(groupFileUrl).openStream();
} else {
String filePath = StarRocksFE.STARROCKS_HOME_DIR + "/conf/" + groupFileUrl;
String starRocksHome = System.getenv("STARROCKS_HOME");
String filePath;
if (starRocksHome != null) {
filePath = starRocksHome + "/conf/" + groupFileUrl;
} else {
// If STARROCKS_HOME is not set, use absolute path
filePath = groupFileUrl;
}
return new FileInputStream(filePath);
}
}

View File

@ -39,7 +39,7 @@ public abstract class GroupProvider {
}
public void destory() {
public void destroy() {
}
@ -59,7 +59,7 @@ public abstract class GroupProvider {
return "";
}
public abstract Set<String> getGroup(UserIdentity userIdentity);
public abstract Set<String> getGroup(UserIdentity userIdentity, String distinguishedName);
public abstract void checkProperty() throws SemanticException;
}

View File

@ -14,6 +14,7 @@
package com.starrocks.authentication;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.starrocks.common.util.NetUtils;
import com.starrocks.qe.ConnectContext;
@ -80,11 +81,17 @@ public class LDAPAuthProvider implements AuthenticationProvider {
}
try {
String distinguishedName;
if (!Strings.isNullOrEmpty(ldapUserDN)) {
checkPassword(ldapUserDN, new String(clearPassword, StandardCharsets.UTF_8));
distinguishedName = ldapUserDN;
} else {
checkPasswordByRoot(userIdentity.getUser(), new String(clearPassword, StandardCharsets.UTF_8));
distinguishedName = findUserDNByRoot(userIdentity.getUser());
}
Preconditions.checkNotNull(distinguishedName);
checkPassword(distinguishedName, new String(clearPassword, StandardCharsets.UTF_8));
// set distinguished name to auth context
context.setDistinguishedName(distinguishedName);
} catch (Exception e) {
LOG.warn("check password failed for user: {}", userIdentity.getUser(), e);
throw new AuthenticationException(e.getMessage());
@ -111,7 +118,7 @@ public class LDAPAuthProvider implements AuthenticationProvider {
}
//bind to ldap server to check password
public void checkPassword(String dn, String password) throws Exception {
protected void checkPassword(String dn, String password) throws Exception {
if (Strings.isNullOrEmpty(password)) {
throw new AuthenticationException("empty password is not allowed for simple authentication");
}
@ -143,8 +150,8 @@ public class LDAPAuthProvider implements AuthenticationProvider {
//1. bind ldap server by root dn
//2. search user
//3. if match exactly one, check password
public void checkPasswordByRoot(String user, String password) throws Exception {
//3. if match exactly one, return the user's actual DN
protected String findUserDNByRoot(String user) throws Exception {
if (Strings.isNullOrEmpty(ldapBindRootPwd)) {
throw new AuthenticationException("empty password is not allowed for simple authentication");
}
@ -198,7 +205,7 @@ public class LDAPAuthProvider implements AuthenticationProvider {
throw new AuthenticationException("ldap search matched user count " + matched);
}
checkPassword(userDN, password);
return userDN;
} finally {
if (ctx != null) {
try {

View File

@ -14,6 +14,7 @@
package com.starrocks.authentication;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.starrocks.common.Config;
import com.starrocks.common.DdlException;
@ -128,16 +129,22 @@ public class LDAPGroupProvider extends GroupProvider {
}
@Override
public void destory() {
public void destroy() {
scheduleTask.cancel(true);
}
@Override
public Set<String> getGroup(UserIdentity userIdentity) {
return userToGroupCache.getOrDefault(userIdentity.getUser(), Set.of());
public Set<String> getGroup(UserIdentity userIdentity, String distinguishedName) {
String ldapUserSearchAttr = getLdapUserSearchAttr();
if (ldapUserSearchAttr != null) {
return userToGroupCache.getOrDefault(userIdentity.getUser(), Set.of());
} else {
return userToGroupCache.getOrDefault(distinguishedName, Set.of());
}
}
public void refreshGroups() {
LOG.info("refresh ldap group cache for group provider: {}", name);
Map<String, Set<String>> groups = new ConcurrentHashMap<>();
try {
DirContext ctx = createDirContextOnConnection(getLdapBindRootDn(), getLdapBindRootPwd());
@ -397,4 +404,9 @@ public class LDAPGroupProvider extends GroupProvider {
}
}
}
@VisibleForTesting
public void setUserToGroupCache(Map<String, Set<String>> userToGroupCache) {
this.userToGroupCache = userToGroupCache;
}
}

View File

@ -29,7 +29,7 @@ public class UnixGroupProvider extends GroupProvider {
}
@Override
public Set<String> getGroup(UserIdentity userIdentity) {
public Set<String> getGroup(UserIdentity userIdentity, String distinguishedName) {
Set<String> userGroups = Set.of();
UserGroupInformation ugi = UserGroupInformation.createRemoteUser(userIdentity.getUser());

View File

@ -129,6 +129,14 @@ public class ErrorReport {
report(null, errorCode, objs);
}
public static void report(ErrorCode errorCode, String errMsg) {
ConnectContext ctx = ConnectContext.get();
if (ctx != null) {
ctx.getState().setError(errMsg);
ctx.getState().setErrorCode(errorCode);
}
}
public static void report(String pattern, ErrorCode errorCode, Object... objs) {
reportCommon(pattern, errorCode, objs);
}

View File

@ -199,6 +199,15 @@ public class ConnectContext {
//Auth Data salt generated at mysql negotiate used for password salting
private byte[] authDataSalt = null;
// The security integration method used for authentication.
protected String securityIntegration = "native";
// Distinguished name (DN) used for LDAP authentication and group resolution
// In LDAP context, this represents the unique identifier of a user in the directory
// For non-LDAP authentication, this typically defaults to the username
// Used by group providers to resolve user group memberships
protected String distinguishedName = "";
// Serializer used to pack MySQL packet.
protected MysqlSerializer serializer;
// Variables belong to this session.
@ -491,21 +500,33 @@ public class ConnectContext {
this.currentUserIdentity = currentUserIdentity;
}
public void setDistinguishedName(String distinguishedName) {
this.distinguishedName = distinguishedName;
}
public String getDistinguishedName() {
return distinguishedName;
}
public Set<Long> getCurrentRoleIds() {
return currentRoleIds;
}
public void setCurrentRoleIds(UserIdentity user) {
try {
Set<Long> defaultRoleIds;
if (GlobalVariable.isActivateAllRolesOnLogin()) {
defaultRoleIds = globalStateMgr.getAuthorizationMgr().getRoleIdsByUser(user);
} else {
defaultRoleIds = globalStateMgr.getAuthorizationMgr().getDefaultRoleIdsByUser(user);
if (user.isEphemeral()) {
this.currentRoleIds = new HashSet<>();
} else {
try {
Set<Long> defaultRoleIds;
if (GlobalVariable.isActivateAllRolesOnLogin()) {
defaultRoleIds = globalStateMgr.getAuthorizationMgr().getRoleIdsByUser(user);
} else {
defaultRoleIds = globalStateMgr.getAuthorizationMgr().getDefaultRoleIdsByUser(user);
}
this.currentRoleIds = defaultRoleIds;
} catch (PrivilegeException e) {
LOG.warn("Set current role fail : {}", e.getMessage());
}
this.currentRoleIds = defaultRoleIds;
} catch (PrivilegeException e) {
LOG.warn("Set current role fail : {}", e.getMessage());
}
}
@ -513,6 +534,10 @@ public class ConnectContext {
this.currentRoleIds = roleIds;
}
public void setCurrentRoleIds(UserIdentity userIdentity, Set<String> groups) {
setCurrentRoleIds(userIdentity);
}
public void setAuthInfoFromThrift(TAuthInfo authInfo) {
if (authInfo.isSetCurrent_user_ident()) {
setAuthInfoFromThrift(authInfo.getCurrent_user_ident());
@ -571,6 +596,14 @@ public class ConnectContext {
return authDataSalt;
}
public String getSecurityIntegration() {
return securityIntegration;
}
public void setSecurityIntegration(String securityIntegration) {
this.securityIntegration = securityIntegration;
}
public void modifySystemVariable(SystemVariable setVar, boolean onlySetSessionVar) throws DdlException {
globalStateMgr.getVariableMgr().setSystemVariable(sessionVariable, setVar, onlySetSessionVar);
if (!SetType.GLOBAL.equals(setVar.getType()) && globalStateMgr.getVariableMgr()
@ -1010,6 +1043,7 @@ public class ConnectContext {
/**
* Get the current compute resource, acquire it if not set.
* NOTE: This method will acquire compute resource if it is not set.
*
* @return: the current compute resource, or the default resource if not in shared data mode.
*/
public ComputeResource getCurrentComputeResource() {
@ -1025,6 +1059,7 @@ public class ConnectContext {
/**
* Get the name of the current compute resource.
* NOTE: this method will not acquire compute resource if it is not set.
*
* @return: the name of the current compute resource, or empty string if not set.
*/
public String getCurrentComputeResourceName() {
@ -1037,6 +1072,7 @@ public class ConnectContext {
/**
* Get the current compute resource without acquiring it.
*
* @return: the current compute resource(null if not set), or the default resource if not in shared data mode.
*/
public ComputeResource getCurrentComputeResourceNoAcquire() {
@ -1181,7 +1217,8 @@ public class ConnectContext {
/**
* NOTE: The ExecTimeout should not contain the pending time which may be caused by QueryQueue's scheduler.
* </p>
* @return Get the timeout for this session, unit: second
*
* @return Get the timeout for this session, unit: second
*/
public int getExecTimeout() {
return pendingTimeSecond + getExecTimeoutWithoutPendingTime();
@ -1193,6 +1230,7 @@ public class ConnectContext {
/**
* update the pending time for this session, unit: second
*
* @param pendingTimeSecond: the pending time for this session
*/
public void setPendingTimeSecond(int pendingTimeSecond) {
@ -1213,6 +1251,7 @@ public class ConnectContext {
/**
* Check the connect context is timeout or not. If true, kill the connection, otherwise, return false.
*
* @param now : current time in milliseconds
* @return true if timeout, false otherwise
*/

View File

@ -12,16 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.qe;
import com.google.common.base.Preconditions;
import com.starrocks.authentication.AuthenticationHandler;
import com.starrocks.authentication.UserProperty;
import com.starrocks.common.Config;
import com.starrocks.sql.ast.ExecuteAsStmt;
import com.starrocks.sql.ast.UserIdentity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Set;
public class ExecuteAsExecutor {
private static final Logger LOG = LogManager.getLogger(ExecuteAsStmt.class);
@ -29,7 +33,7 @@ public class ExecuteAsExecutor {
* Only set current user, won't reset any other context, for example, current database.
* Because mysql client still think that this session is using old databases and will show such hint,
* which will only confuse the user
*
* <p>
* MySQL [test_priv]> execute as test1 with no revert;
* Query OK, 0 rows affected (0.00 sec)
* MySQL [test_priv]> select * from test_table2;
@ -40,14 +44,69 @@ public class ExecuteAsExecutor {
Preconditions.checkArgument(!stmt.isAllowRevert());
LOG.info("{} EXEC AS {} from now on", ctx.getCurrentUserIdentity(), stmt.getToUser());
UserIdentity user = stmt.getToUser();
ctx.setCurrentUserIdentity(user);
ctx.setCurrentRoleIds(user);
UserIdentity userIdentity = stmt.getToUser();
ctx.setCurrentUserIdentity(userIdentity);
if (!user.isEphemeral()) {
// Refresh groups and roles for all users based on security integration
refreshGroupsAndRoles(ctx, userIdentity);
if (!userIdentity.isEphemeral()) {
UserProperty userProperty = ctx.getGlobalStateMgr().getAuthenticationMgr()
.getUserProperty(user.getUser());
.getUserProperty(userIdentity.getUser());
ctx.updateByUserProperty(userProperty);
}
}
/**
* Refresh groups and roles for user based on security integration
* This applies to all users (both external and native) to ensure proper permission refresh
*/
private static void refreshGroupsAndRoles(ConnectContext ctx, UserIdentity userIdentity) {
try {
// Get group provider list based on security integration
List<String> groupProviderList = getGroupProviderList(ctx);
// Query groups for the user
Set<String> groups = AuthenticationHandler.getGroups(userIdentity, userIdentity.getUser(), groupProviderList);
// Set groups to context
ctx.setGroups(groups);
// Refresh current role IDs based on user + groups
ctx.setCurrentRoleIds(userIdentity);
LOG.info("Refreshed groups {} and roles for user {}", groups, userIdentity);
} catch (Exception e) {
LOG.warn("Failed to refresh groups and roles for user {}: {}", userIdentity, e.getMessage());
// Continue execution even if group refresh fails
}
}
/**
* Get group provider list based on security integration
*/
private static List<String> getGroupProviderList(ConnectContext ctx) {
String securityIntegration = ctx.getSecurityIntegration();
// If no security integration is set, use default group provider
if (securityIntegration == null || securityIntegration.isEmpty() ||
securityIntegration.equals("native")) {
return List.of(Config.group_provider);
}
// Try to get group provider from security integration
try {
var authMgr = ctx.getGlobalStateMgr().getAuthenticationMgr();
var si = authMgr.getSecurityIntegration(securityIntegration);
if (si != null && si.getGroupProviderName() != null && !si.getGroupProviderName().isEmpty()) {
return si.getGroupProviderName();
}
} catch (Exception e) {
LOG.warn("Failed to get group provider from security integration {}: {}",
securityIntegration, e.getMessage());
}
// Fallback to default group provider
return List.of(Config.group_provider);
}
}

View File

@ -0,0 +1,129 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed 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
//
// https://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 com.starrocks.authentication;
import com.starrocks.common.Config;
import com.starrocks.common.DdlException;
import com.starrocks.persist.EditLog;
import com.starrocks.qe.ConnectContext;
import com.starrocks.server.GlobalStateMgr;
import com.starrocks.sql.analyzer.Analyzer;
import com.starrocks.sql.ast.CreateUserStmt;
import com.starrocks.sql.ast.UserAuthOption;
import com.starrocks.sql.ast.UserIdentity;
import com.starrocks.sql.parser.NodePosition;
import mockit.Mock;
import mockit.MockUp;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyShort;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.spy;
public class AuthenticationHandlerTest {
@BeforeEach
public void setUp() throws Exception {
// Mock EditLog
EditLog editLog = spy(new EditLog(null));
doNothing().when(editLog).logEdit(anyShort(), any());
GlobalStateMgr.getCurrentState().setEditLog(editLog);
new MockUp<LDAPGroupProvider>() {
@Mock
public void init() throws DdlException {
// do nothing
}
};
}
@Test
public void testLdapDNMappingGroup() throws Exception {
AuthenticationMgr authenticationMgr = new AuthenticationMgr();
GlobalStateMgr.getCurrentState().setAuthenticationMgr(authenticationMgr);
CreateUserStmt stmt = new CreateUserStmt(
new UserIdentity("ldap_user", "%"),
true,
new UserAuthOption("AUTHENTICATION_LDAP_SIMPLE",
"uid=ldap_user,ou=company,dc=example,dc=com",
false, NodePosition.ZERO),
List.of(), Map.of(), NodePosition.ZERO);
Analyzer.analyze(stmt, new ConnectContext());
authenticationMgr.createUser(stmt);
new MockUp<LDAPAuthProvider>() {
@Mock
private void checkPassword(String dn, String password) throws Exception {
// mock: always success
}
@Mock
private String findUserDNByRoot(String user) throws Exception {
return "uid=test,ou=People,dc=starrocks,dc=com";
}
};
Map<String, String> properties = new HashMap<>();
properties.put(GroupProvider.GROUP_PROVIDER_PROPERTY_TYPE_KEY, "ldap");
properties.put(LDAPGroupProvider.LDAP_USER_SEARCH_ATTR, "uid");
String groupName = "ldap_group_provider";
authenticationMgr.replayCreateGroupProvider(groupName, properties);
Config.group_provider = new String[] {groupName};
LDAPGroupProvider ldapGroupProvider = (LDAPGroupProvider) authenticationMgr.getGroupProvider(groupName);
Map<String, Set<String>> groups = new HashMap<>();
groups.put("ldap_user", Set.of("group1", "group2"));
groups.put("uid=ldap_user,ou=company,dc=example,dc=com", Set.of("group3", "group4"));
groups.put("u1", Set.of("group5"));
groups.put("u2", Set.of("group6"));
ldapGroupProvider.setUserToGroupCache(groups);
ConnectContext context = new ConnectContext();
AuthenticationHandler.authenticate(context, "ldap_user", "%", "\0".getBytes(StandardCharsets.UTF_8));
Assertions.assertEquals("ldap_user", context.getQualifiedUser());
Assertions.assertEquals("uid=ldap_user,ou=company,dc=example,dc=com", context.getDistinguishedName());
Assertions.assertEquals(Set.of("group1", "group2"),
ldapGroupProvider.getGroup(context.getCurrentUserIdentity(), context.getDistinguishedName()));
properties = new HashMap<>();
properties.put(GroupProvider.GROUP_PROVIDER_PROPERTY_TYPE_KEY, "ldap");
groupName = "ldap_group_provider2";
authenticationMgr.replayCreateGroupProvider(groupName, properties);
Config.group_provider = new String[] {groupName};
LDAPGroupProvider ldapGroupProvider2 = (LDAPGroupProvider) authenticationMgr.getGroupProvider(groupName);
Map<String, Set<String>> groups2 = new HashMap<>();
groups2.put("ldap_user", Set.of("group1", "group2"));
groups2.put("uid=ldap_user,ou=company,dc=example,dc=com", Set.of("group3", "group4"));
groups2.put("u1", Set.of("group5"));
groups2.put("u2", Set.of("group6"));
ldapGroupProvider2.setUserToGroupCache(groups2);
Assertions.assertEquals(Set.of("group3", "group4"),
ldapGroupProvider2.getGroup(context.getCurrentUserIdentity(), context.getDistinguishedName()));
}
}

View File

@ -0,0 +1,127 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed 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
//
// https://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 com.starrocks.authentication;
import com.starrocks.authorization.AuthorizationMgr;
import com.starrocks.authorization.DefaultAuthorizationProvider;
import com.starrocks.common.Config;
import com.starrocks.common.DdlException;
import com.starrocks.mysql.MysqlPassword;
import com.starrocks.persist.EditLog;
import com.starrocks.qe.ConnectContext;
import com.starrocks.qe.ExecuteAsExecutor;
import com.starrocks.server.GlobalStateMgr;
import com.starrocks.sql.analyzer.Analyzer;
import com.starrocks.sql.ast.CreateRoleStmt;
import com.starrocks.sql.ast.CreateUserStmt;
import com.starrocks.sql.ast.ExecuteAsStmt;
import com.starrocks.sql.ast.UserIdentity;
import com.starrocks.sql.parser.NodePosition;
import mockit.Mock;
import mockit.MockUp;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyShort;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.spy;
public class ExecuteAsExecutorTest {
private AuthenticationMgr authenticationMgr;
private AuthorizationMgr authorizationMgr;
@BeforeEach
public void setUp() throws Exception {
// Mock EditLog
EditLog editLog = spy(new EditLog(null));
doNothing().when(editLog).logEdit(anyShort(), any());
GlobalStateMgr.getCurrentState().setEditLog(editLog);
authenticationMgr = new AuthenticationMgr();
GlobalStateMgr.getCurrentState().setAuthenticationMgr(authenticationMgr);
authorizationMgr = new AuthorizationMgr(new DefaultAuthorizationProvider());
GlobalStateMgr.getCurrentState().setAuthorizationMgr(authorizationMgr);
new MockUp<LDAPGroupProvider>() {
@Mock
public void init() throws DdlException {
// do nothing
}
};
Map<String, String> properties = new HashMap<>();
properties.put(GroupProvider.GROUP_PROVIDER_PROPERTY_TYPE_KEY, "ldap");
properties.put(LDAPGroupProvider.LDAP_USER_SEARCH_ATTR, "uid");
String groupName = "ldap_group_provider";
authenticationMgr.replayCreateGroupProvider(groupName, properties);
Config.group_provider = new String[] {groupName};
LDAPGroupProvider ldapGroupProvider = (LDAPGroupProvider) authenticationMgr.getGroupProvider(groupName);
Map<String, Set<String>> groups = new HashMap<>();
groups.put("impersonate_user", Set.of("group1", "group2"));
groups.put("u1", Set.of("group3"));
groups.put("u2", Set.of("group4"));
ldapGroupProvider.setUserToGroupCache(groups);
}
@Test
public void testExecuteAs() throws Exception {
authorizationMgr.createRole(new CreateRoleStmt(List.of("r1"), true, ""));
authorizationMgr.createRole(new CreateRoleStmt(List.of("r2"), true, ""));
CreateUserStmt createUserStmt =
new CreateUserStmt(new UserIdentity("impersonate_user", "%"), true, null, List.of(), Map.of(), NodePosition.ZERO);
Analyzer.analyze(createUserStmt, new ConnectContext());
authenticationMgr.createUser(createUserStmt);
createUserStmt = new CreateUserStmt(new UserIdentity("u1", "%"), true, null, List.of("r1"), Map.of(), NodePosition.ZERO);
Analyzer.analyze(createUserStmt, new ConnectContext());
authenticationMgr.createUser(createUserStmt);
createUserStmt = new CreateUserStmt(new UserIdentity("u2", "%"), true, null, List.of("r2"), Map.of(), NodePosition.ZERO);
Analyzer.analyze(createUserStmt, new ConnectContext());
authenticationMgr.createUser(createUserStmt);
long roleId1 = authorizationMgr.getRoleIdByNameAllowNull("r1");
long roleId2 = authorizationMgr.getRoleIdByNameAllowNull("r2");
// login as impersonate_user
ConnectContext context = new ConnectContext();
AuthenticationHandler.authenticate(context, "impersonate_user", "%", MysqlPassword.EMPTY_PASSWORD);
Assertions.assertEquals("impersonate_user", context.getQualifiedUser());
Assertions.assertEquals(Set.of("group1", "group2"), context.getGroups());
ExecuteAsStmt executeAsStmt = new ExecuteAsStmt(new UserIdentity("u1", "%"), false);
ExecuteAsExecutor.execute(executeAsStmt, context);
Assertions.assertEquals(Set.of("group3"), context.getGroups());
Assertions.assertEquals(Set.of(roleId1), context.getCurrentRoleIds());
ExecuteAsStmt executeAsStmt2 = new ExecuteAsStmt(new UserIdentity("u2", "%"), false);
ExecuteAsExecutor.execute(executeAsStmt2, context);
Assertions.assertEquals(Set.of("group4"), context.getGroups());
Assertions.assertEquals(Set.of(roleId2), context.getCurrentRoleIds());
}
}

View File

@ -0,0 +1,56 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed 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
//
// https://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 com.starrocks.authentication;
import com.starrocks.common.Config;
import com.starrocks.common.DdlException;
import com.starrocks.server.GlobalStateMgr;
import com.starrocks.sql.ast.UserIdentity;
import mockit.Mock;
import mockit.MockUp;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Set;
public class FileGroupProviderTest {
@Test
public void testFileGroupProvider() throws DdlException {
new MockUp<FileGroupProvider>() {
@Mock
public InputStream getPath(String groupFileUrl) throws IOException {
String path = ClassLoader.getSystemClassLoader().getResource("auth").getPath() + "/" + "file_group";
return new FileInputStream(path);
}
};
AuthenticationMgr authenticationMgr = GlobalStateMgr.getCurrentState().getAuthenticationMgr();
String groupName = "file_group_provider";
Map<String, String> properties = Map.of(GroupProvider.GROUP_PROVIDER_PROPERTY_TYPE_KEY, "file",
FileGroupProvider.GROUP_FILE_URL, "file_group");
authenticationMgr.replayCreateGroupProvider(groupName, properties);
Config.group_provider = new String[] {groupName};
FileGroupProvider fileGroupProvider = (FileGroupProvider) authenticationMgr.getGroupProvider(groupName);
Set<String> groups = fileGroupProvider.getGroup(new UserIdentity("harbor", "%"), "harbor");
Assertions.assertTrue(groups.contains("group1"));
Assertions.assertTrue(groups.contains("group2"));
}
}

View File

@ -0,0 +1,124 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed 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
//
// https://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 com.starrocks.authentication;
import com.starrocks.common.Config;
import com.starrocks.persist.EditLog;
import com.starrocks.qe.ConnectContext;
import com.starrocks.server.GlobalStateMgr;
import com.starrocks.sql.ast.CreateUserStmt;
import com.starrocks.sql.ast.UserAuthOption;
import com.starrocks.sql.ast.UserIdentity;
import com.starrocks.sql.parser.NodePosition;
import mockit.Mock;
import mockit.MockUp;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyShort;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.spy;
class LDAPAuthProviderTest {
@BeforeAll
public static void setUp() throws Exception {
new MockUp<LDAPAuthProvider>() {
@Mock
private void checkPassword(String dn, String password) throws Exception {
// mock: always success
}
@Mock
private String findUserDNByRoot(String user) throws Exception {
return "uid=test,ou=People,dc=starrocks,dc=com";
}
};
// Mock EditLog
EditLog editLog = spy(new EditLog(null));
doNothing().when(editLog).logEdit(anyShort(), any());
GlobalStateMgr.getCurrentState().setEditLog(editLog);
AuthenticationMgr authenticationMgr = new AuthenticationMgr();
GlobalStateMgr.getCurrentState().setAuthenticationMgr(authenticationMgr);
authenticationMgr.createUser(new CreateUserStmt(
new UserIdentity("ldap_user", "%"),
true,
new UserAuthOption("AUTHENTICATION_LDAP_SIMPLE",
"uid=ldap_user,ou=company,dc=example,dc=com",
false, NodePosition.ZERO),
List.of(), Map.of(), NodePosition.ZERO));
Map<String, String> properties = new HashMap<>();
properties.put(GroupProvider.GROUP_PROVIDER_PROPERTY_TYPE_KEY, "file");
properties.put(FileGroupProvider.GROUP_FILE_URL, "file_group");
String groupName = "file_group_provider";
authenticationMgr.replayCreateGroupProvider(groupName, properties);
Config.group_provider = new String[] {groupName};
}
@AfterAll
public static void teardown() throws Exception {
}
@Test
void testAuthenticateSetsDNWhenLdapUserDNProvided() throws Exception {
String providedDN = "cn=test,ou=People,dc=starrocks,dc=com";
LDAPAuthProvider provider = new LDAPAuthProvider(
"localhost", 389, false,
null, null,
"cn=admin,dc=starrocks,dc=com", "secret",
"ou=People,dc=starrocks,dc=com", "uid",
providedDN);
ConnectContext context = new ConnectContext();
UserIdentity user = UserIdentity.createEphemeralUserIdent("ldap_user", "%");
byte[] authResponse = "password\0".getBytes(StandardCharsets.UTF_8);
provider.authenticate(context, user, authResponse);
Assertions.assertEquals(providedDN, context.getDistinguishedName());
}
@Test
void testAuthenticateSetsDNWhenFindByRoot() throws Exception {
String discoveredDN = "uid=test,ou=People,dc=starrocks,dc=com";
LDAPAuthProvider provider = new LDAPAuthProvider(
"localhost", 389, false,
null, null,
"cn=admin,dc=starrocks,dc=com", "secret",
"ou=People,dc=starrocks,dc=com", "uid",
/* ldapUserDN */ null
);
ConnectContext context = new ConnectContext();
UserIdentity user = UserIdentity.createEphemeralUserIdent("ldap_user", "%");
byte[] authResponse = "password\0".getBytes(StandardCharsets.UTF_8);
provider.authenticate(context, user, authResponse);
Assertions.assertEquals(discoveredDN, context.getDistinguishedName());
}
}

View File

@ -51,7 +51,6 @@ import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyShort;
@ -127,27 +126,6 @@ public class SecurityIntegrationTest {
return sb.toString();
}
@Test
public void testFileGroupProvider() throws DdlException, AuthenticationException, IOException, NoSuchMethodException {
new MockUp<FileGroupProvider>() {
@Mock
public InputStream getPath(String groupFileUrl) throws IOException {
String path = ClassLoader.getSystemClassLoader().getResource("auth").getPath() + "/" + "file_group";
return new FileInputStream(path);
}
};
String groupName = "g1";
Map<String, String> properties = new HashMap<>();
properties.put(FileGroupProvider.GROUP_FILE_URL, "file_group");
FileGroupProvider fileGroupProvider = new FileGroupProvider(groupName, properties);
fileGroupProvider.init();
Set<String> groups = fileGroupProvider.getGroup(new UserIdentity("harbor", "127.0.0.1"));
Assertions.assertTrue(groups.contains("group1"));
Assertions.assertTrue(groups.contains("group2"));
}
@Test
public void testGroupProvider() throws Exception {
GlobalStateMgr.getCurrentState().setJwkMgr(new MockTokenUtils.MockJwkMgr());
@ -261,7 +239,6 @@ public class SecurityIntegrationTest {
Assert.assertTrue(
resultSet.getResultRows().get(0).get(1).contains("\"authentication_ldap_simple_bind_root_pwd\" = \"***\""));
properties = new HashMap<>();
properties.put(SecurityIntegration.SECURITY_INTEGRATION_PROPERTY_TYPE_KEY, "authentication_oauth2");
properties.put(OAuth2AuthenticationProvider.OAUTH2_CLIENT_SECRET, "123");

View File

@ -1,2 +1,2 @@
group1:harbor,tina
group1:harbor,test
group2:harbor

View File

@ -0,0 +1,126 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed 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
//
// https://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 com.starrocks.authentication;
import com.starrocks.catalog.UserIdentity;
/**
* AuthenticationContext encapsulates authentication and authorization information for a connection session.
* This includes user identity, roles, groups, and authentication-related metadata.
*/
public class AuthenticationContext {
// `qualifiedUser` is the user used when the user establishes connection and authentication.
// It is the real user used for this connection.
// Different from the `currentUserIdentity` authentication user of execute as,
// `qualifiedUser` should not be changed during the entire session.
private String qualifiedUser;
// `currentUserIdentity` is the user used for authorization. Under normal circumstances,
// `currentUserIdentity` and `qualifiedUser` are the same user,
// but currentUserIdentity may be modified by execute as statement.
private UserIdentity currentUserIdentity;
// Distinguished name (DN) used for LDAP authentication and group resolution
// In LDAP context, this represents the unique identifier of a user in the directory
// For non-LDAP authentication, this typically defaults to the username
// Used by group providers to resolve user group memberships
protected String distinguishedName = "";
// The Token in the OpenIDConnect authentication method is obtained
// from the authentication logic and stored in the AuthenticationContext.
// If the downstream system needs it, it needs to be obtained from the AuthenticationContext.
private volatile String authToken = null;
// The security integration method used for authentication.
protected String securityIntegration = "native";
// The authentication provider used for this authentication.
private AuthenticationProvider authenticationProvider = null;
// After negotiate and switching with the client,
// the auth plugin type used for this authentication is finally determined.
private String authPlugin = null;
// Auth Data salt generated at mysql negotiate used for password salting
private byte[] authDataSalt = null;
public AuthenticationContext() {
// Default constructor
}
public String getQualifiedUser() {
return qualifiedUser;
}
public void setQualifiedUser(String qualifiedUser) {
this.qualifiedUser = qualifiedUser;
}
public UserIdentity getCurrentUserIdentity() {
return currentUserIdentity;
}
public void setCurrentUserIdentity(UserIdentity currentUserIdentity) {
this.currentUserIdentity = currentUserIdentity;
}
public void setDistinguishedName(String distinguishedName) {
this.distinguishedName = distinguishedName;
}
public String getDistinguishedName() {
return distinguishedName;
}
public String getAuthToken() {
return authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
public AuthenticationProvider getAuthenticationProvider() {
return authenticationProvider;
}
public void setAuthenticationProvider(AuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}
public String getAuthPlugin() {
return authPlugin;
}
public void setAuthPlugin(String authPlugin) {
this.authPlugin = authPlugin;
}
public byte[] getAuthDataSalt() {
return authDataSalt;
}
public void setAuthDataSalt(byte[] authDataSalt) {
this.authDataSalt = authDataSalt;
}
public String getSecurityIntegration() {
return securityIntegration;
}
public void setSecurityIntegration(String securityIntegration) {
this.securityIntegration = securityIntegration;
}
}