[Feature][LDAP] Add LDAP authentication login and LDAP group authorization support. (#6333)

* [Feature][LDAP] Add LDAP authentication login and LDAP group authorization support.

* Update docs/.vuepress/sidebar/en.js

Co-authored-by: Mingyu Chen <morningman.cmy@gmail.com>

Co-authored-by: Mingyu Chen <morningman.cmy@gmail.com>
This commit is contained in:
luozenglin
2021-07-30 09:24:50 +08:00
committed by GitHub
parent cf1fcdd614
commit 9ca369aa58
36 changed files with 2533 additions and 119 deletions

View File

@ -254,7 +254,7 @@ terminal String KW_ADD, KW_ADMIN, KW_AFTER, KW_AGGREGATE, KW_ALL, KW_ALTER, KW_A
KW_MAP, KW_MATERIALIZED, KW_MAX, KW_MAX_VALUE, KW_MERGE, KW_MIN, KW_MINUTE, KW_MINUS, KW_MIGRATE, KW_MIGRATIONS, KW_MODIFY, KW_MONTH,
KW_NAME, KW_NAMED_STRUCT, KW_NAMES, KW_NEGATIVE, KW_NO, KW_NOT, KW_NULL, KW_NULLS,
KW_OBSERVER, KW_OFFSET, KW_ON, KW_ONLY, KW_OPEN, KW_OR, KW_ORDER, KW_OUTER, KW_OUTFILE, KW_OVER,
KW_PARTITION, KW_PARTITIONS, KW_PASSWORD, KW_PATH, KW_PAUSE, KW_PIPE, KW_PRECEDING,
KW_PARTITION, KW_PARTITIONS, KW_PASSWORD, KW_LDAP_ADMIN_PASSWORD, KW_PATH, KW_PAUSE, KW_PIPE, KW_PRECEDING,
KW_PLUGIN, KW_PLUGINS,
KW_PROC, KW_PROCEDURE, KW_PROCESSLIST, KW_PROFILE, KW_PROPERTIES, KW_PROPERTY,
KW_QUERY, KW_QUOTA,
@ -3352,6 +3352,10 @@ option_value_no_option_type ::=
{:
RESULT = new SetPassVar(userId, passwd);
:}
| KW_LDAP_ADMIN_PASSWORD equal text_or_password:passwd
{:
RESULT = new SetLdapPassVar(passwd);
:}
;
variable_name ::=
@ -5078,6 +5082,8 @@ keyword ::=
{: RESULT = id; :}
| KW_PASSWORD:id
{: RESULT = id; :}
| KW_LDAP_ADMIN_PASSWORD:id
{: RESULT = id; :}
| KW_PLUGIN:id
{: RESULT = id; :}
| KW_PLUGINS:id

View File

@ -20,6 +20,7 @@ package org.apache.doris;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.CommandLineOptions;
import org.apache.doris.common.Config;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.common.Log4jConfig;
import org.apache.doris.common.ThreadPoolManager;
import org.apache.doris.common.Version;
@ -33,9 +34,6 @@ import org.apache.doris.service.ExecuteEnv;
import org.apache.doris.service.FeServer;
import org.apache.doris.service.FrontendOptions;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
@ -44,6 +42,9 @@ import org.apache.commons.cli.ParseException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
@ -88,6 +89,11 @@ public class PaloFe {
// Because the path of custom config file is defined in fe.conf
config.initCustom(Config.custom_config_dir + "/fe_custom.conf");
LdapConfig ldapConfig = new LdapConfig();
if (new File(dorisHomeDir + "/conf/ldap.conf").exists()) {
ldapConfig.init(dorisHomeDir + "/conf/ldap.conf");
}
// check it after Config is initialized, otherwise the config 'check_java_version' won't work.
if (!JdkUtils.checkJavaVersion()) {
throw new IllegalArgumentException("Java version doesn't match");

View File

@ -0,0 +1,63 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.analysis;
import com.google.common.base.Strings;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.util.SymmetricEncryption;
import org.apache.doris.mysql.privilege.PaloAuth;
import org.apache.doris.qe.ConnectContext;
public class SetLdapPassVar extends SetVar {
private String passwd;
public SetLdapPassVar(String passwd) {
// Encrypted password
this.passwd = SymmetricEncryption.encrypt(passwd);
}
public String getLdapPassword() {
return passwd;
}
@Override
public void analyze(Analyzer analyzer) throws AnalysisException {
if (Strings.isNullOrEmpty(analyzer.getClusterName())) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_CLUSTER_NO_SELECT_CLUSTER);
}
if (!ConnectContext.get().getCurrentUserIdentity().getQualifiedUser().equals(PaloAuth.ROOT_USER)
&& !ConnectContext.get().getCurrentUserIdentity().getQualifiedUser().equals(PaloAuth.ADMIN_USER)) {
throw new AnalysisException("Only root and admin user can set ldap admin password.");
}
}
@Override
public String toString() {
return toSql();
}
@Override
public String toSql() {
StringBuilder sb = new StringBuilder("SET LDAP_ADMIN_PASSWORD");
sb.append(" = '*XXX'");
return sb.toString();
}
}

View File

@ -17,13 +17,13 @@
package org.apache.doris.common;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
@ -55,10 +55,23 @@ public class ConfigBase {
private static String customConfFile;
public static Class<? extends ConfigBase> confClass;
public void init(String confFile) throws Exception {
confClass = this.getClass();
this.confFile = confFile;
initConf(confFile);
private static String ldapConfFile;
private static String ldapCustomConfFile;
public static Class<? extends ConfigBase> ldapConfClass;
private boolean isLdapConfig = false;
public void init(String configFile) throws Exception {
this.isLdapConfig = (this instanceof LdapConfig);
if (!isLdapConfig) {
confClass = this.getClass();
confFile = configFile;
initConf(confFile);
} else {
ldapConfClass = this.getClass();
ldapConfFile = configFile;
initConf(ldapConfFile);
}
}
public void initCustom(String customConfFile) throws Exception {
@ -75,7 +88,7 @@ public class ConfigBase {
Properties props = new Properties();
props.load(new FileReader(confFile));
replacedByEnv(props);
setFields(props);
setFields(props, isLdapConfig);
}
public static HashMap<String, String> dump() throws Exception {
@ -137,8 +150,9 @@ public class ConfigBase {
}
}
private static void setFields(Properties props) throws Exception {
Field[] fields = confClass.getFields();
private static void setFields(Properties props, boolean isLdapConfig) throws Exception {
Class<? extends ConfigBase> theClass = isLdapConfig ? ldapConfClass : confClass;
Field[] fields = theClass.getFields();
for (Field f : fields) {
// ensure that field has "@ConfField" annotation
ConfField anno = f.getAnnotation(ConfField.class);
@ -159,7 +173,7 @@ public class ConfigBase {
if (confKey.equalsIgnoreCase("async_load_task_pool_size")) {
Config.async_loading_load_task_pool_size = Config.async_load_task_pool_size;
}
}
}
}
public static void setConfigField(Field f, String confVal) throws IllegalAccessException, Exception {

View File

@ -243,7 +243,11 @@ public enum ErrorCode {
ERROR_DYNAMIC_PARTITION_CREATE_HISTORY_PARTITION(5074, new byte[]{'4', '2', '0', '0', '0'},
"Invalid dynamic partition create_history_partition: %s. Expected true or false"),
ERROR_DYNAMIC_PARTITION_HISTORY_PARTITION_NUM_ZERO(5075, new byte[] {'4', '2', '0', '0', '0'},
"Dynamic history partition num must greater than 0");
"Dynamic history partition num must greater than 0"),
ERROR_LDAP_CONFIGURATION_ERR(5080, new byte[] {'4', '2', '0', '0', '0'},
"LDAP configuration is incorrect or LDAP admin password is not set."),
ERROR_LDAP_USER_NOT_UNIQUE_ERR(5081, new byte[] {'4', '2', '0', '0', '0'},
"%s is not unique in LDAP server.");
ErrorCode(int code, byte[] sqlState, String errorMsg) {
this.code = code;

View File

@ -0,0 +1,145 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.common;
/**
* LDAP configuration
*/
public class LdapConfig extends ConfigBase {
/**
* Flag to enable LDAP authentication.
*/
@ConfigBase.ConfField
public static boolean ldap_authentication_enabled = false;
/**
* LDAP server ip.
*/
@ConfigBase.ConfField
public static String ldap_host = "";
/**
* LDAP server port.
*/
@ConfigBase.ConfField
public static int ldap_port = 389;
/**
* Search base for users.
* LDAP is a tree structure, and this specifies the base of the subtree in which the search is to be constrained.
*/
@ConfigBase.ConfField
public static String ldap_user_basedn = "";
/**
* The DN to bind as connection, this value will be used to lookup information about other users.
*/
@ConfigBase.ConfField
public static String ldap_admin_name = "";
/**
* User lookup filter, the placeholder {login} will be replaced by the user supplied login.
*/
@ConfigBase.ConfField
public static String ldap_user_filter = "(&(uid={login}))";
/**
* Search base for groups.
*/
@ConfigBase.ConfField
public static String ldap_group_basedn = "";
/**
* Maximum number of user connections. This value should be between 1 and 10000.
*/
@ConfigBase.ConfField
public static long user_max_connections = 100L;
/**
* LDAP pool configuration:
* https://docs.spring.io/spring-ldap/docs/2.3.3.RELEASE/reference/#pool-configuration
*/
/**
* The maximum number of active connections of each type (read-only or read-write) that can be allocated
* from this pool at the same time. You can use a non-positive number for no limit.
*/
@ConfigBase.ConfField
public static int max_active = 8;
/**
* The overall maximum number of active connections (for all types) that can be allocated from this pool
* at the same time. You can use a non-positive number for no limit.
*/
@ConfigBase.ConfField
public static int max_total = -1;
/**
* The maximum number of active connections of each type (read-only or read-write) that can remain idle
* in the pool without extra connections being released. You can use a non-positive number for no limit.
*/
@ConfigBase.ConfField
public static int max_idle = 8;
/**
* The minimum number of active connections of each type (read-only or read-write) that can remain idle
* in the pool without extra connections being created. You can use zero (the default) to create none.
*/
@ConfigBase.ConfField
public static int min_idle = 0;
/**
* The maximum number of milliseconds that the pool waits (when no connections are available) for a connection
* to be returned before throwing an exception. You can use a non-positive number to wait indefinitely.
*/
@ConfigBase.ConfField
public static int max_wait = -1;
/**
* Specifies the behavior when the pool is exhausted.
*
* The '0' option throws NoSuchElementException when the pool is exhausted.
*
* The '1' option waits until a new object is available. If max-wait is positive and no new object is available
* after the max-wait time expires, NoSuchElementException is thrown.
*
* The '2' option creates and returns a new object (essentially making max-active meaningless).
*/
@ConfigBase.ConfField
public static byte when_exhausted = 1;
/**
* Whether objects are validated before being borrowed from the pool. If the object fails to validate,
* it is dropped from the pool, and an attempt to borrow another is made.
*/
@ConfigBase.ConfField
public static boolean test_on_borrow = false;
/**
* Whether objects are validated before being returned to the pool.
*/
@ConfigBase.ConfField
public static boolean test_on_return = false;
/**
* Whether objects are validated by the idle object evictor (if any). If an object fails to validate,
* it is dropped from the pool.
*/
@ConfigBase.ConfField
public static boolean test_while_idle = false;
}

View File

@ -0,0 +1,76 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.common.util;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* This is borrowed from apache kylin:
* https://github.com/apache/kylin/blob/master/core-common/src/main/java/org/apache/kylin/common/util/EncryptUtil.java
*/
public class SymmetricEncryption {
private static byte[] key = { 0x56, 0x73, 0x36, 0x68, 0x4b, 0x56, 0x27, 0x67, 0x24, 0x46, 0x77, 0x57, 0x75, 0x5a,
0x46, 0x74 };
private static final Cipher getCipher(int cipherMode) throws InvalidAlgorithmParameterException,
InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, UnsupportedEncodingException {
Cipher cipher = Cipher.getInstance("AES/CFB/PKCS5Padding");
final SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec("AAAAAAAAAAAAAAAA".getBytes("UTF-8"));
cipher.init(cipherMode, secretKey, ivSpec);
return cipher;
}
public static String encrypt(String strToEncrypt) {
if (strToEncrypt == null) {
return null;
}
try {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE);
final String encryptedString = Base64.encodeBase64String(cipher.doFinal(strToEncrypt.getBytes(
StandardCharsets.UTF_8)));
return encryptedString;
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static String decrypt(String strToDecrypt) {
if (strToDecrypt == null) {
return null;
}
try {
Cipher cipher = getCipher(Cipher.DECRYPT_MODE);
final String decryptedString = new String(cipher.doFinal(Base64.decodeBase64(strToDecrypt)), StandardCharsets.UTF_8);
return decryptedString;
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}

View File

@ -73,6 +73,7 @@ import org.apache.doris.persist.ModifyTablePropertyOperationLog;
import org.apache.doris.persist.OperationType;
import org.apache.doris.persist.PartitionPersistInfo;
import org.apache.doris.persist.PrivInfo;
import org.apache.doris.persist.LdapInfo;
import org.apache.doris.persist.RecoverInfo;
import org.apache.doris.persist.RefreshExternalTableInfo;
import org.apache.doris.persist.RemoveAlterJobV2OperationLog;
@ -348,6 +349,11 @@ public class JournalEntity implements Writable {
isRead = true;
break;
}
case OperationType.OP_SET_LDAP_PASSWORD: {
data = LdapInfo.read(in);
isRead = true;
break;
}
case OperationType.OP_UPDATE_USER_PROPERTY: {
data = UserPropertyInfo.read(in);
isRead = true;

View File

@ -0,0 +1,145 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.ldap;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.mysql.privilege.PaloRole;
import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import java.util.List;
/**
* This class is used for LDAP authentication login and LDAP group authorization.
* This means that users can log in to Doris with a user name and LDAP password,
* and the user will get the privileges of all roles corresponding to the LDAP group.
*/
public class LdapAuthenticate {
private static final Logger LOG = LogManager.getLogger(LdapAuthenticate.class);
private static final String LDAP_GROUPS_PRIVS_NAME = "ldapGroupsPrivs";
// Maximum number of the user LDAP authentication login connections.
private static long userMaxConn = 100;
{
if (LdapConfig.user_max_connections <= 0 || LdapConfig.user_max_connections > 10000) {
LOG.warn("Ldap config user_max_connections is invalid. It should be set between 1 and 10000. " +
"And now, it is set to the default value.");
} else {
userMaxConn = LdapConfig.user_max_connections;
}
}
/**
* The LDAP authentication process is as follows:
* step1: Check the LDAP password.
* step2: Get the LDAP groups privileges as a role, saved into ConnectContext.
* step3: Set current userIdentity. If the user account does not exist in Doris, login as a temporary user.
* Otherwise, login to the Doris account.
*/
public static boolean authenticate(ConnectContext context, String password, String qualifiedUser) {
String usePasswd = (Strings.isNullOrEmpty(password)) ? "NO" : "YES";
String userName = ClusterNamespace.getNameFromFullName(qualifiedUser);
String clusterName = ClusterNamespace.getClusterNameFromFullName(qualifiedUser);
LOG.debug("user:{}, cluster:{}", userName, clusterName);
// check user password by ldap server.
try {
if (!LdapClient.checkPassword(userName, password)) {
LOG.debug("user:{} use error LDAP password.", userName);
ErrorReport.report(ErrorCode.ERR_ACCESS_DENIED_ERROR, qualifiedUser, usePasswd);
return false;
}
} catch (Exception e) {
LOG.error("Check ldap password error.", e);
return false;
}
// Get the LDAP groups privileges as a role.
PaloRole ldapGroupsPrivs;
try {
ldapGroupsPrivs = getLdapGroupsPrivs(userName, clusterName);
} catch (Exception e) {
LOG.error("Get ldap groups error.", e);
return false;
}
String remoteIp = context.getMysqlChannel().getRemoteIp();
UserIdentity tempUserIdentity = UserIdentity.createAnalyzedUserIdentWithIp(qualifiedUser, remoteIp);
// Search the user in doris.
UserIdentity userIdentity = Catalog.getCurrentCatalog().getAuth().getCurrentUserIdentity(tempUserIdentity);
if (userIdentity == null) {
userIdentity = tempUserIdentity;
LOG.debug("User:{} does not exists in doris, login as temporary users.", userName);
context.setIsTempUser(true);
if (ldapGroupsPrivs == null) {
ldapGroupsPrivs = new PaloRole(LDAP_GROUPS_PRIVS_NAME);
}
LdapPrivsChecker.grantDefaultPrivToTempUser(ldapGroupsPrivs, clusterName);
}
context.setCurrentUserIdentity(userIdentity);
context.setRemoteIP(remoteIp);
context.setLdapGroupsPrivs(ldapGroupsPrivs);
LOG.debug("ldap authentication success: identity:{}, privs:{}",
context.getCurrentUserIdentity(), context.getLdapGroupsPrivs());
return true;
}
/**
* Step1: get ldap groups from ldap server;
* Step2: get roles by ldap groups;
* Step3: merge the roles;
*/
private static PaloRole getLdapGroupsPrivs(String userName, String clusterName) {
//get user ldap group. the ldap group name should be the same as the doris role name
List<String> ldapGroups = LdapClient.getGroups(userName);
List<String> rolesNames = Lists.newArrayList();
for (String group : ldapGroups) {
String qualifiedRole = ClusterNamespace.getFullName(clusterName, group);
if (Catalog.getCurrentCatalog().getAuth().doesRoleExist(qualifiedRole)) {
rolesNames.add(qualifiedRole);
}
}
LOG.debug("get user:{} ldap groups:{} and doris roles:{}", userName, ldapGroups, rolesNames);
// merge the roles
if (rolesNames.isEmpty()) {
return null;
} else {
PaloRole ldapGroupsPrivs = new PaloRole(LDAP_GROUPS_PRIVS_NAME);
Catalog.getCurrentCatalog().getAuth().mergeRolesNoCheckName(rolesNames, ldapGroupsPrivs);
return ldapGroupsPrivs;
}
}
public static long getMaxConn() {
return userMaxConn;
}
}

View File

@ -0,0 +1,182 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.ldap;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.common.util.SymmetricEncryption;
import com.google.common.collect.Lists;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.AbstractContextMapper;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.pool.factory.PoolingContextSource;
import org.springframework.ldap.pool.validation.DefaultDirContextValidator;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.ldap.transaction.compensating.manager.TransactionAwareContextSourceProxy;
import java.util.List;
// This class is used to connect to the LDAP service.
public class LdapClient {
private static final Logger LOG = LogManager.getLogger(LdapClient.class);
// Checking the user password requires creating a new connection with the user dn and password.
// Due to this, these connections cannot be pooled.
private volatile static LdapTemplate ldapTemplateNoPool;
// Use ldap connection pool, connect to bind ldap admin dn and admin password.
private volatile static LdapTemplate ldapTemplatePool;
public static void init(String ldapPassword) {
LOG.info("Init ldap client.");
setLdapTemplateNoPool(ldapPassword);
setLdapTemplatePool(ldapPassword);
}
private static void setLdapTemplateNoPool(String ldapPassword) {
LdapContextSource contextSource = new LdapContextSource();
String url = "ldap://" + LdapConfig.ldap_host + ":" + LdapConfig.ldap_port;
contextSource.setUrl(url);
contextSource.setUserDn(LdapConfig.ldap_admin_name);
contextSource.setPassword(SymmetricEncryption.decrypt(ldapPassword));
contextSource.afterPropertiesSet();
ldapTemplateNoPool = new LdapTemplate(contextSource);
}
private static void setLdapTemplatePool(String ldapPassword) {
LdapContextSource contextSource = new LdapContextSource();
String url = "ldap://" + LdapConfig.ldap_host + ":" + LdapConfig.ldap_port;
contextSource.setUrl(url);
contextSource.setUserDn(LdapConfig.ldap_admin_name);
contextSource.setPassword(SymmetricEncryption.decrypt(ldapPassword));
contextSource.setPooled(true);
contextSource.afterPropertiesSet();
PoolingContextSource poolingContextSource = new PoolingContextSource();
poolingContextSource.setDirContextValidator(new DefaultDirContextValidator());
poolingContextSource.setContextSource(contextSource);
poolingContextSource.setMaxActive(LdapConfig.max_active);
poolingContextSource.setMaxTotal(LdapConfig.max_total);
poolingContextSource.setMaxIdle(LdapConfig.max_idle);
poolingContextSource.setMaxWait(LdapConfig.max_wait);
poolingContextSource.setMinIdle(LdapConfig.min_idle);
poolingContextSource.setWhenExhaustedAction(LdapConfig.when_exhausted);
poolingContextSource.setTestOnBorrow(LdapConfig.test_on_borrow);
poolingContextSource.setTestOnReturn(LdapConfig.test_on_return);
poolingContextSource.setTestWhileIdle(LdapConfig.test_while_idle);
TransactionAwareContextSourceProxy proxy = new TransactionAwareContextSourceProxy(poolingContextSource);
ldapTemplatePool = new LdapTemplate(proxy);
}
public static boolean doesUserExist(String userName) {
String user = getUserDn(userName);
if (user == null) {
LOG.debug("User:{} does not exist in LDAP.", userName);
return false;
}
return true;
}
public static boolean checkPassword(String userName, String password) {
checkLdapTemplate();
try {
ldapTemplateNoPool.authenticate(query().base(LdapConfig.ldap_user_basedn)
.filter(getUserFilter(LdapConfig.ldap_user_filter, userName)), password);
return true;
} catch (Exception e) {
return false;
}
}
// Search group DNs by 'member' attribution.
public static List<String> getGroups(String userName) {
List<String> groups = Lists.newArrayList();
if (LdapConfig.ldap_group_basedn.isEmpty()){
return groups;
}
String userDn = getUserDn(userName);
if (userDn == null) {
return groups;
}
List<String> groupDns = getDn(query().base(LdapConfig.ldap_group_basedn)
.where("member").is(userDn));
if (groupDns == null) {
return groups;
}
// group dn like: 'cn=groupName,ou=groups,dc=example,dc=com', we only need the groupName.
for (String dn : groupDns) {
String[] strings = dn.split("[,=]", 3);
if (strings.length > 2) {
groups.add(strings[1]);
}
}
return groups;
}
private static String getUserDn(String userName) {
List<String> userDns = getDn(query().base(LdapConfig.ldap_user_basedn)
.filter(getUserFilter(LdapConfig.ldap_user_filter, userName)));
if (userDns == null || userDns.isEmpty()) {
return null;
}
if (userDns.size() > 1) {
LOG.error("{} not unique in LDAP server:{}", getUserFilter(LdapConfig.ldap_user_filter, userName), userDns);
ErrorReport.report(ErrorCode.ERROR_LDAP_USER_NOT_UNIQUE_ERR, userName);
throw new RuntimeException("User is not unique");
}
return userDns.get(0);
}
private static List<String> getDn(LdapQuery query) {
checkLdapTemplate();
try {
return ldapTemplatePool.search(query, new AbstractContextMapper() {
protected String doMapFromContext(DirContextOperations ctx) {
return ctx.getNameInNamespace();
}
});
} catch (Exception e) {
LOG.error("Get user dn fail.", e);
ErrorReport.report(ErrorCode.ERROR_LDAP_CONFIGURATION_ERR);
throw e;
}
}
private static String getUserFilter(String userFilter, String userName) {
return userFilter.replaceAll("\\{login}", userName);
}
private static boolean checkLdapTemplate() {
if (ldapTemplatePool == null || ldapTemplateNoPool == null) {
LOG.error("ldapTemplate is not initialized.");
ErrorReport.report(ErrorCode.ERROR_LDAP_CONFIGURATION_ERR);
throw new RuntimeException("ldapTemplate is not initialized");
}
return true;
}
}

View File

@ -0,0 +1,263 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.ldap;
import com.google.common.collect.Maps;
import org.apache.doris.analysis.ResourcePattern;
import org.apache.doris.analysis.TablePattern;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.InfoSchemaDb;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.mysql.privilege.PaloAuth;
import org.apache.doris.mysql.privilege.PaloPrivilege;
import org.apache.doris.mysql.privilege.PaloRole;
import org.apache.doris.mysql.privilege.PrivBitSet;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Preconditions;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Map;
/**
* If the user logs in with LDAP authentication, the user LDAP group privileges will be saved in 'ldapGroupsPrivs' of ConnectContext.
* When checking user privileges, Doris need to check both the privileges granted by Doris and LDAP group privileges.
* This class is used for checking current user LDAP group privileges.
*/
public class LdapPrivsChecker {
private static final Logger LOG = LogManager.getLogger(LdapPrivsChecker.class);
public static boolean hasGlobalPrivFromLdap(UserIdentity currentUser, PrivPredicate wanted) {
return hasTblPatternPrivs(currentUser, wanted, null, null, PaloAuth.PrivLevel.GLOBAL)
|| hasResourcePatternPrivs(currentUser, wanted, null, PaloAuth.PrivLevel.GLOBAL);
}
public static boolean hasDbPrivFromLdap(UserIdentity currentUser, String db, PrivPredicate wanted) {
return hasTblPatternPrivs(currentUser, wanted, db, null, PaloAuth.PrivLevel.DATABASE);
}
// Any database has wanted priv return true.
public static boolean hasDbPrivFromLdap(UserIdentity currentUser, PrivPredicate wanted) {
return hasPrivs(currentUser, wanted, PaloAuth.PrivLevel.DATABASE);
}
public static boolean hasTblPrivFromLdap(UserIdentity currentUser, String db, String tbl, PrivPredicate wanted) {
return hasTblPatternPrivs(currentUser, wanted, db, tbl, PaloAuth.PrivLevel.TABLE);
}
// Any table has wanted priv return true.
public static boolean hasTblPrivFromLdap(UserIdentity currentUser, PrivPredicate wanted) {
return hasPrivs(currentUser, wanted, PaloAuth.PrivLevel.TABLE);
}
public static boolean hasResourcePrivFromLdap(UserIdentity currentUser, String resourceName, PrivPredicate wanted) {
return hasResourcePatternPrivs(currentUser, wanted, resourceName, PaloAuth.PrivLevel.RESOURCE);
}
private static boolean hasTblPatternPrivs(UserIdentity currentUser, PrivPredicate wanted, String db, String tbl,
PaloAuth.PrivLevel level) {
PrivBitSet savedPrivs = PrivBitSet.of();
getCurrentUserTblPrivs(currentUser, db, tbl, savedPrivs, level);
return PaloPrivilege.satisfy(savedPrivs, wanted);
}
private static boolean hasResourcePatternPrivs(UserIdentity currentUser, PrivPredicate wanted, String resourceName,
PaloAuth.PrivLevel level) {
PrivBitSet savedPrivs = PrivBitSet.of();
getCurrentUserResourcePrivs(currentUser, resourceName, savedPrivs, level);
return PaloPrivilege.satisfy(savedPrivs, wanted);
}
public static PrivBitSet getGlobalPrivFromLdap(UserIdentity currentUser) {
PrivBitSet savedPrivs = PrivBitSet.of();
getCurrentUserTblPrivs(currentUser, null, null, savedPrivs, PaloAuth.PrivLevel.GLOBAL);
getCurrentUserResourcePrivs(currentUser, null, savedPrivs, PaloAuth.PrivLevel.GLOBAL);
return savedPrivs;
}
public static PrivBitSet getDbPrivFromLdap(UserIdentity currentUser, String db) {
PrivBitSet savedPrivs = PrivBitSet.of();
getCurrentUserTblPrivs(currentUser, db, null, savedPrivs, PaloAuth.PrivLevel.DATABASE);
return savedPrivs;
}
public static PrivBitSet getTblPrivFromLdap(UserIdentity currentUser, String db, String tbl) {
PrivBitSet savedPrivs = PrivBitSet.of();
getCurrentUserTblPrivs(currentUser, db, tbl, savedPrivs, PaloAuth.PrivLevel.TABLE);
return savedPrivs;
}
public static PrivBitSet getResourcePrivFromLdap(UserIdentity currentUser, String resourceName) {
PrivBitSet savedPrivs = PrivBitSet.of();
getCurrentUserResourcePrivs(currentUser, resourceName, savedPrivs, PaloAuth.PrivLevel.RESOURCE);
return savedPrivs;
}
private static void getCurrentUserTblPrivs(UserIdentity currentUser, String db, String tbl, PrivBitSet savedPrivs,
PaloAuth.PrivLevel level) {
if (!hasLdapPrivs(currentUser)) {
return;
}
PaloRole currentUserLdapPrivs = ConnectContext.get().getLdapGroupsPrivs();
for (Map.Entry<TablePattern, PrivBitSet> entry : currentUserLdapPrivs.getTblPatternToPrivs().entrySet()) {
switch (entry.getKey().getPrivLevel()) {
case GLOBAL:
if (level.equals(PaloAuth.PrivLevel.GLOBAL)) {
savedPrivs.or(entry.getValue());
return;
}
break;
case DATABASE:
if (level.equals(PaloAuth.PrivLevel.DATABASE) && db != null
&& entry.getKey().getQualifiedDb().equals(db)) {
savedPrivs.or(entry.getValue());
return;
}
break;
case TABLE:
if (level.equals(PaloAuth.PrivLevel.TABLE) && db != null && tbl != null
&& entry.getKey().getQualifiedDb().equals(db) && entry.getKey().getTbl().equals(tbl)) {
savedPrivs.or(entry.getValue());
return;
}
break;
default:
Preconditions.checkNotNull(null, entry.getKey().getPrivLevel());
}
}
}
private static void getCurrentUserResourcePrivs(UserIdentity currentUser, String resourceName, PrivBitSet savedPrivs,
PaloAuth.PrivLevel level) {
if (!hasLdapPrivs(currentUser)) {
return;
}
PaloRole currentUserLdapPrivs = ConnectContext.get().getLdapGroupsPrivs();
for (Map.Entry<ResourcePattern, PrivBitSet> entry : currentUserLdapPrivs.getResourcePatternToPrivs().entrySet()) {
switch (entry.getKey().getPrivLevel()) {
case GLOBAL:
if (level.equals(PaloAuth.PrivLevel.GLOBAL)) {
savedPrivs.or(entry.getValue());
return;
}
break;
case RESOURCE:
if (level.equals(PaloAuth.PrivLevel.RESOURCE) && resourceName != null
&& entry.getKey().getResourceName().equals(resourceName)) {
savedPrivs.or(entry.getValue());
return;
}
break;
default:
Preconditions.checkNotNull(null, entry.getKey().getPrivLevel());
}
}
}
private static boolean hasPrivs(UserIdentity currentUser, PrivPredicate wanted, PaloAuth.PrivLevel level) {
if (!hasLdapPrivs(currentUser)) {
return false;
}
PaloRole currentUserLdapPrivs = ConnectContext.get().getLdapGroupsPrivs();
for (Map.Entry<TablePattern, PrivBitSet> entry : currentUserLdapPrivs.getTblPatternToPrivs().entrySet()) {
if (entry.getKey().getPrivLevel().equals(level) && PaloPrivilege.satisfy(entry.getValue(), wanted)) {
return true;
}
}
return false;
}
// Check if user has any privs of tables in this database.
public static boolean hasPrivsOfDb(UserIdentity currentUser, String db) {
if (!hasLdapPrivs(currentUser)) {
return false;
}
PaloRole currentUserLdapPrivs = ConnectContext.get().getLdapGroupsPrivs();
for (Map.Entry<TablePattern, PrivBitSet> entry : currentUserLdapPrivs.getTblPatternToPrivs().entrySet()) {
if (entry.getKey().getPrivLevel().equals(PaloAuth.PrivLevel.TABLE) && entry.getKey().getQualifiedDb().equals(db)) {
return true;
}
}
return false;
}
public static boolean isCurrentUser(UserIdentity userIdent) {
ConnectContext context = ConnectContext.get();
if (context == null) {
return false;
}
UserIdentity currentUser = context.getCurrentUserIdentity();
return currentUser.getQualifiedUser().equals(userIdent.getQualifiedUser())
&& currentUser.getHost().equals(userIdent.getHost());
}
public static boolean hasLdapPrivs(UserIdentity userIdent) {
return LdapConfig.ldap_authentication_enabled && isCurrentUser(userIdent)
&& ConnectContext.get().getLdapGroupsPrivs() != null;
}
public static Map<TablePattern, PrivBitSet> getLdapAllDbPrivs(UserIdentity userIdentity) {
Map<TablePattern, PrivBitSet> ldapDbPrivs = Maps.newConcurrentMap();
if (!hasLdapPrivs(userIdentity)) return ldapDbPrivs;
for (Map.Entry<TablePattern, PrivBitSet> entry : ConnectContext.get().getLdapGroupsPrivs()
.getTblPatternToPrivs().entrySet()) {
if (entry.getKey().getPrivLevel().equals(PaloAuth.PrivLevel.DATABASE)) {
ldapDbPrivs.put(entry.getKey(), entry.getValue());
}
}
return ldapDbPrivs;
}
public static Map<TablePattern, PrivBitSet> getLdapAllTblPrivs(UserIdentity userIdentity) {
Map<TablePattern, PrivBitSet> ldapTblPrivs = Maps.newConcurrentMap();
if (!hasLdapPrivs(userIdentity)) return ldapTblPrivs;
for (Map.Entry<TablePattern, PrivBitSet> entry : ConnectContext.get().getLdapGroupsPrivs()
.getTblPatternToPrivs().entrySet()) {
if (entry.getKey().getPrivLevel().equals(PaloAuth.PrivLevel.TABLE)) {
ldapTblPrivs.put(entry.getKey(), entry.getValue());
}
}
return ldapTblPrivs;
}
public static Map<ResourcePattern, PrivBitSet> getLdapAllResourcePrivs(UserIdentity userIdentity) {
Map<ResourcePattern, PrivBitSet> ldapResourcePrivs = Maps.newConcurrentMap();
if (!hasLdapPrivs(userIdentity)) return ldapResourcePrivs;
for (Map.Entry<ResourcePattern, PrivBitSet> entry : ConnectContext.get().getLdapGroupsPrivs()
.getResourcePatternToPrivs().entrySet()) {
if (entry.getKey().getPrivLevel().equals(PaloAuth.PrivLevel.RESOURCE)) {
ldapResourcePrivs.put(entry.getKey(), entry.getValue());
}
}
return ldapResourcePrivs;
}
// Temporary user has information_schema 'Select_priv' priv by default.
public static void grantDefaultPrivToTempUser(PaloRole role, String clusterName) {
TablePattern tblPattern = new TablePattern(InfoSchemaDb.DATABASE_NAME, "*");
try {
tblPattern.analyze(clusterName);
} catch (AnalysisException e) {
LOG.warn("should not happen.", e);
}
PaloRole newRole = new PaloRole(role.getRoleName(), tblPattern, PrivBitSet.of(PaloPrivilege.SELECT_PRIV));
role.merge(newRole);
}
}

View File

@ -0,0 +1,31 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.mysql;
public class MysqlAuthSwitchPacket extends MysqlPacket {
private static final int STATUS = 0xfe;
private static final String AUTH_PLUGIN_NAME = "mysql_clear_password";
private static final String DATA = "";
@Override
public void writeTo(MysqlSerializer serializer) {
serializer.writeInt1(STATUS);
serializer.writeNulTerminateString(AUTH_PLUGIN_NAME);
serializer.writeNulTerminateString(DATA);
}
}

View File

@ -0,0 +1,40 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.mysql;
import java.nio.ByteBuffer;
public class MysqlClearTextPacket extends MysqlPacket {
private String password = "";
public String getPassword() {
return password;
}
@Override
public boolean readFrom(ByteBuffer buffer) {
password = new String(MysqlProto.readNulTerminateString(buffer));
return true;
}
@Override
public void writeTo(MysqlSerializer serializer) {
}
}

View File

@ -24,6 +24,10 @@ import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.ldap.LdapAuthenticate;
import org.apache.doris.ldap.LdapClient;
import org.apache.doris.mysql.privilege.PaloAuth;
import org.apache.doris.mysql.privilege.UserResource;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.system.SystemInfoService;
@ -45,13 +49,29 @@ public class MysqlProto {
// scramble: data receive from server.
// randomString: data send by server in plug-in data field
// user_name#HIGH@cluster_name
private static boolean authenticate(ConnectContext context, byte[] scramble, byte[] randomString, String user) {
private static boolean authenticate(ConnectContext context, byte[] scramble, byte[] randomString, String qualifiedUser) {
String usePasswd = scramble.length == 0 ? "NO" : "YES";
String remoteIp = context.getMysqlChannel().getRemoteIp();
List<UserIdentity> currentUserIdentity = Lists.newArrayList();
if (!Catalog.getCurrentCatalog().getAuth().checkPassword(qualifiedUser, remoteIp,
scramble, randomString, currentUserIdentity)) {
ErrorReport.report(ErrorCode.ERR_ACCESS_DENIED_ERROR, qualifiedUser, usePasswd);
return false;
}
context.setCurrentUserIdentity(currentUserIdentity.get(0));
context.setRemoteIP(remoteIp);
return true;
}
private static String parseUser(ConnectContext context, byte[] scramble, String user) {
String usePasswd = scramble.length == 0 ? "NO" : "YES";
String tmpUser = user;
if (tmpUser == null || tmpUser.isEmpty()) {
ErrorReport.report(ErrorCode.ERR_ACCESS_DENIED_ERROR, "", usePasswd);
return false;
return null;
}
// check cluster, user name may contains cluster name or cluster id.
@ -64,14 +84,14 @@ public class MysqlProto {
clusterName = strList[1];
try {
// if cluster does not exist and it is not a valid cluster id, authenticate failed
if (Catalog.getCurrentCatalog().getCluster(clusterName) == null
if (Catalog.getCurrentCatalog().getCluster(clusterName) == null
&& Integer.valueOf(strList[1]) != context.getCatalog().getClusterId()) {
ErrorReport.report(ErrorCode.ERR_UNKNOWN_CLUSTER_ID, strList[1]);
return false;
return null;
}
} catch (Throwable e) {
ErrorReport.report(ErrorCode.ERR_UNKNOWN_CLUSTER_ID, strList[1]);
return false;
return null;
}
}
if (Strings.isNullOrEmpty(clusterName)) {
@ -90,22 +110,11 @@ public class MysqlProto {
context.getSessionVariable().setResourceGroup(strList[1]);
}
}
LOG.debug("parse cluster: {}", clusterName);
String qualifiedUser = ClusterNamespace.getFullName(clusterName, tmpUser);
String remoteIp = context.getMysqlChannel().getRemoteIp();
List<UserIdentity> currentUserIdentity = Lists.newArrayList();
if (!Catalog.getCurrentCatalog().getAuth().checkPassword(qualifiedUser, remoteIp,
scramble, randomString, currentUserIdentity)) {
ErrorReport.report(ErrorCode.ERR_ACCESS_DENIED_ERROR, qualifiedUser, usePasswd);
return false;
}
context.setCurrentUserIdentity(currentUserIdentity.get(0));
context.setQualifiedUser(qualifiedUser);
context.setRemoteIP(remoteIp);
return true;
return qualifiedUser;
}
// send response packet(OK/EOF/ERR).
@ -121,10 +130,28 @@ public class MysqlProto {
channel.sendAndFlush(serializer.toByteBuffer());
}
private static boolean useLdapAuthenticate(String qualifiedUser) {
// The root and admin are used to set the ldap admin password and cannot use ldap authentication.
if (qualifiedUser.equals(PaloAuth.ROOT_USER) || qualifiedUser.equals(PaloAuth.ADMIN_USER)) {
return false;
}
// If LDAP authentication is enabled and the user exists in LDAP, use LDAP authentication,
// otherwise use Doris authentication.
if (LdapConfig.ldap_authentication_enabled
&& LdapClient.doesUserExist(ClusterNamespace.getNameFromFullName(qualifiedUser))) {
return true;
}
return false;
}
/**
* negotiate with client, use MySQL protocol
* server ---handshake---> client
* server <--- authenticate --- client
* if enable ldap: {
* server ---AuthSwitch---> client
* server <--- clear text password --- client
* }
* server --- response(OK/ERR) ---> client
* Exception:
* IOException:
@ -165,48 +192,89 @@ public class MysqlProto {
return false;
}
// Starting with MySQL 8.0.4, MySQL changed the default authentication plugin for MySQL client
// from mysql_native_password to caching_sha2_password.
// ref: https://mysqlserverteam.com/mysql-8-0-4-new-default-authentication-plugin-caching_sha2_password/
// So, User use mysql client or ODBC Driver after 8.0.4 have problem to connect to Doris
// with password.
// So Doris support the Protocol::AuthSwitchRequest to tell client to keep the default password plugin
// which Doris is using now.
// Note: Check the authPacket whether support plugin auth firstly, before we check AuthPlugin between doris and client
// to compatible with older version: like mysql 5.1
if (authPacket.getCapability().isPluginAuth() &&
!handshakePacket.checkAuthPluginSameAsDoris(authPacket.getPluginName())) {
// 1. clear the serializer
serializer.reset();
// 2. build the auth switch request and send to the client
handshakePacket.buildAuthSwitchRequest(serializer);
channel.sendAndFlush(serializer.toByteBuffer());
// Server receive auth switch response packet from client.
ByteBuffer authSwitchResponse = channel.fetchOnePacket();
if (authSwitchResponse == null) {
// receive response failed.
return false;
}
// 3. the client use default password plugin of Doris to dispose
// password
authPacket.setAuthResponse(readEofString(authSwitchResponse));
}
// change the capability of serializer
context.setCapability(context.getServerCapability());
serializer.setCapability(context.getCapability());
// NOTE: when we behind proxy, we need random string sent by proxy.
byte[] randomString = handshakePacket.getAuthPluginData();
if (Config.proxy_auth_enable && authPacket.getRandomString() != null) {
randomString = authPacket.getRandomString();
}
// check authenticate
if (!authenticate(context, authPacket.getAuthResponse(), randomString, authPacket.getUser())) {
String qualifiedUser = parseUser(context, authPacket.getAuthResponse(), authPacket.getUser());
if (qualifiedUser == null) {
sendResponsePacket(context);
return false;
}
boolean useLdapAuthenticate;
try {
useLdapAuthenticate = useLdapAuthenticate(qualifiedUser);
} catch (Exception e) {
LOG.debug("Check if user exists in ldap error.", e);
sendResponsePacket(context);
return false;
}
if (useLdapAuthenticate) {
LOG.debug("user:{} start to ldap authenticate.", qualifiedUser);
// server send authentication switch packet to request password clear text.
// https://dev.mysql.com/doc/internals/en/authentication-method-change.html
serializer.reset();
MysqlAuthSwitchPacket mysqlAuthSwitchPacket = new MysqlAuthSwitchPacket();
mysqlAuthSwitchPacket.writeTo(serializer);
channel.sendAndFlush(serializer.toByteBuffer());
// Server receive password clear text.
ByteBuffer authSwitchResponse = channel.fetchOnePacket();
if (authSwitchResponse == null) {
return false;
}
MysqlClearTextPacket clearTextPacket = new MysqlClearTextPacket();
if (!clearTextPacket.readFrom(authSwitchResponse)) {
ErrorReport.report(ErrorCode.ERR_NOT_SUPPORTED_AUTH_MODE);
sendResponsePacket(context);
return false;
}
if (!LdapAuthenticate.authenticate(context, clearTextPacket.getPassword(), qualifiedUser)) {
sendResponsePacket(context);
return false;
}
} else {
// Starting with MySQL 8.0.4, MySQL changed the default authentication plugin for MySQL client
// from mysql_native_password to caching_sha2_password.
// ref: https://mysqlserverteam.com/mysql-8-0-4-new-default-authentication-plugin-caching_sha2_password/
// So, User use mysql client or ODBC Driver after 8.0.4 have problem to connect to Doris
// with password.
// So Doris support the Protocol::AuthSwitchRequest to tell client to keep the default password plugin
// which Doris is using now.
// Note: Check the authPacket whether support plugin auth firstly, before we check AuthPlugin between doris and client
// to compatible with older version: like mysql 5.1
if (authPacket.getCapability().isPluginAuth() &&
!handshakePacket.checkAuthPluginSameAsDoris(authPacket.getPluginName())) {
// 1. clear the serializer
serializer.reset();
// 2. build the auth switch request and send to the client
handshakePacket.buildAuthSwitchRequest(serializer);
channel.sendAndFlush(serializer.toByteBuffer());
// Server receive auth switch response packet from client.
ByteBuffer authSwitchResponse = channel.fetchOnePacket();
if (authSwitchResponse == null) {
// receive response failed.
return false;
}
// 3. the client use default password plugin of Doris to dispose
// password
authPacket.setAuthResponse(readEofString(authSwitchResponse));
}
// NOTE: when we behind proxy, we need random string sent by proxy.
byte[] randomString = handshakePacket.getAuthPluginData();
if (Config.proxy_auth_enable && authPacket.getRandomString() != null) {
randomString = authPacket.getRandomString();
}
// check authenticate
if (!authenticate(context, authPacket.getAuthResponse(), randomString, qualifiedUser)) {
sendResponsePacket(context);
return false;
}
}
// set database
String db = authPacket.getDb();
if (!Strings.isNullOrEmpty(db)) {

View File

@ -25,6 +25,7 @@ import org.apache.doris.analysis.GrantStmt;
import org.apache.doris.analysis.ResourcePattern;
import org.apache.doris.analysis.RevokeStmt;
import org.apache.doris.analysis.SetPassVar;
import org.apache.doris.analysis.SetLdapPassVar;
import org.apache.doris.analysis.SetUserPropertyStmt;
import org.apache.doris.analysis.TablePattern;
import org.apache.doris.analysis.UserIdentity;
@ -37,9 +38,13 @@ import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.FeConstants;
import org.apache.doris.common.FeMetaVersion;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.common.Pair;
import org.apache.doris.common.io.Writable;
import org.apache.doris.ldap.LdapClient;
import org.apache.doris.ldap.LdapPrivsChecker;
import org.apache.doris.load.DppConfig;
import org.apache.doris.persist.LdapInfo;
import org.apache.doris.persist.PrivInfo;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.thrift.TFetchResourceResult;
@ -230,6 +235,24 @@ public class PaloAuth implements Writable {
resourcePrivTable.revoke(entry, errOnNonExist, true /* delete entry when empty */);
}
public boolean doesRoleExist(String qualifiedRole) {
return roleManager.getRole(qualifiedRole) != null;
}
public void mergeRolesNoCheckName(List<String> roles, PaloRole savedRole) {
readLock();
try {
for (String roleName : roles) {
if (doesRoleExist(roleName)) {
PaloRole role = roleManager.getRole(roleName);
savedRole.mergeNotCheck(role);
}
}
} finally {
readUnlock();
}
}
/*
* check password, if matched, save the userIdentity in matched entry.
* the following auth checking should use userIdentity saved in currentUser.
@ -328,7 +351,8 @@ public class PaloAuth implements Writable {
private boolean checkTblWithDb(UserIdentity currentUser, String db) {
readLock();
try {
return tablePrivTable.hasPrivsOfDb(currentUser, db);
return (isLdapAuthEnabled() && LdapPrivsChecker.hasPrivsOfDb(currentUser, db))
|| tablePrivTable.hasPrivsOfDb(currentUser, db);
} finally {
readUnlock();
}
@ -401,35 +425,43 @@ public class PaloAuth implements Writable {
* This method will check the given privilege levels
*/
public boolean checkHasPriv(ConnectContext ctx, PrivPredicate priv, PrivLevel... levels) {
return checkHasPrivInternal(ctx.getRemoteIP(), ctx.getQualifiedUser(), priv, levels);
return checkHasPrivInternal(ctx.getCurrentUserIdentity(), ctx.getRemoteIP(), ctx.getQualifiedUser(), priv, levels);
}
private boolean checkHasPrivInternal(String host, String user, PrivPredicate priv, PrivLevel... levels) {
private boolean checkHasPrivInternal(UserIdentity currentUser, String host, String user, PrivPredicate priv,
PrivLevel... levels) {
for (PrivLevel privLevel : levels) {
switch (privLevel) {
case GLOBAL:
if (userPrivTable.hasPriv(host, user, priv)) {
return true;
}
break;
case DATABASE:
if (dbPrivTable.hasPriv(host, user, priv)) {
return true;
}
break;
case TABLE:
if (tablePrivTable.hasPriv(host, user, priv)) {
return true;
}
break;
default:
break;
case GLOBAL:
if ((isLdapAuthEnabled() && LdapPrivsChecker.hasGlobalPrivFromLdap(currentUser, priv))
|| userPrivTable.hasPriv(host, user, priv)) {
return true;
}
break;
case DATABASE:
if ((isLdapAuthEnabled() && LdapPrivsChecker.hasDbPrivFromLdap(currentUser, priv))
|| dbPrivTable.hasPriv(host, user, priv)) {
return true;
}
break;
case TABLE:
if ((isLdapAuthEnabled() && LdapPrivsChecker.hasTblPrivFromLdap(currentUser, priv))
|| tablePrivTable.hasPriv(host, user, priv)) {
return true;
}
break;
default:
break;
}
}
return false;
}
private boolean checkGlobalInternal(UserIdentity currentUser, PrivPredicate wanted, PrivBitSet savedPrivs) {
if (isLdapAuthEnabled() && LdapPrivsChecker.hasGlobalPrivFromLdap(currentUser, wanted)) {
return true;
}
readLock();
try {
userPrivTable.getPrivs(currentUser, savedPrivs);
@ -443,7 +475,11 @@ public class PaloAuth implements Writable {
}
private boolean checkDbInternal(UserIdentity currentUser, String db, PrivPredicate wanted,
PrivBitSet savedPrivs) {
PrivBitSet savedPrivs) {
if (isLdapAuthEnabled() && LdapPrivsChecker.hasDbPrivFromLdap(currentUser, db, wanted)) {
return true;
}
readLock();
try {
dbPrivTable.getPrivs(currentUser, db, savedPrivs);
@ -457,7 +493,11 @@ public class PaloAuth implements Writable {
}
private boolean checkTblInternal(UserIdentity currentUser, String db, String tbl,
PrivPredicate wanted, PrivBitSet savedPrivs) {
PrivPredicate wanted, PrivBitSet savedPrivs) {
if (isLdapAuthEnabled() && LdapPrivsChecker.hasTblPrivFromLdap(currentUser, db, tbl, wanted)) {
return true;
}
readLock();
try {
tablePrivTable.getPrivs(currentUser, db, tbl, savedPrivs);
@ -472,6 +512,10 @@ public class PaloAuth implements Writable {
private boolean checkResourceInternal(UserIdentity currentUser, String resourceName,
PrivPredicate wanted, PrivBitSet savedPrivs) {
if (isLdapAuthEnabled() && LdapPrivsChecker.hasResourcePrivFromLdap(currentUser, resourceName, wanted)) {
return true;
}
readLock();
try {
resourcePrivTable.getPrivs(currentUser, resourceName, savedPrivs);
@ -484,6 +528,11 @@ public class PaloAuth implements Writable {
}
}
// Check if LDAP authentication is enabled.
private boolean isLdapAuthEnabled(){
return LdapConfig.ldap_authentication_enabled;
}
// for test only
public void clear() {
userPrivTable.clear();
@ -535,23 +584,7 @@ public class PaloAuth implements Writable {
false /* set by resolver */, true /* is replay */);
// 4. grant privs of role to user
if (role != null) {
for (Map.Entry<TablePattern, PrivBitSet> entry : role.getTblPatternToPrivs().entrySet()) {
// use PrivBitSet copy to avoid same object being changed synchronously
grantInternal(userIdent, null /* role */, entry.getKey(), entry.getValue().copy(),
false /* err on non exist */, true /* is replay */);
}
for (Map.Entry<ResourcePattern, PrivBitSet> entry : role.getResourcePatternToPrivs().entrySet()) {
// use PrivBitSet copy to avoid same object being changed synchronously
grantInternal(userIdent, null /* role */, entry.getKey(), entry.getValue().copy(),
false /* err on non exist */, true /* is replay */);
}
}
if (role != null) {
// add user to this role
role.addUser(userIdent);
}
grantPrivsByRole(userIdent, role);
// other user properties
propertyMgr.addUserResource(userIdent.getQualifiedUser(), false /* not system user */);
@ -578,6 +611,28 @@ public class PaloAuth implements Writable {
}
}
private void grantPrivsByRole(UserIdentity userIdent, PaloRole role) throws DdlException {
writeLock();
try {
if (role != null) {
for (Map.Entry<TablePattern, PrivBitSet> entry : role.getTblPatternToPrivs().entrySet()) {
// use PrivBitSet copy to avoid same object being changed synchronously
grantInternal(userIdent, null /* role */, entry.getKey(), entry.getValue().copy(),
false /* err on non exist */, true /* is replay */);
}
for (Map.Entry<ResourcePattern, PrivBitSet> entry : role.getResourcePatternToPrivs().entrySet()) {
// use PrivBitSet copy to avoid same object being changed synchronously
grantInternal(userIdent, null /* role */, entry.getKey(), entry.getValue().copy(),
false /* err on non exist */, true /* is replay */);
}
// add user to this role
role.addUser(userIdent);
}
} finally {
writeUnlock();
}
}
// drop user
public void dropUser(DropUserStmt stmt) throws DdlException {
dropUserInternal(stmt.getUserIdentity(), false);
@ -788,6 +843,16 @@ public class PaloAuth implements Writable {
}
}
// Check whether the user exists. If the user exists, return UserIdentity, otherwise return null.
public UserIdentity getCurrentUserIdentity(UserIdentity userIdent) {
readLock();
try {
return userPrivTable.getCurrentUserIdentity(userIdent);
} finally {
readUnlock();
}
}
// revoke
public void revoke(RevokeStmt stmt) throws DdlException {
PrivBitSet privs = PrivBitSet.of(stmt.getPrivileges());
@ -957,6 +1022,18 @@ public class PaloAuth implements Writable {
LOG.info("finished to set password for {}. is replay: {}", userIdent, isReplay);
}
// set ldap admin password.
public void setLdapPassword(SetLdapPassVar stmt) {
LdapClient.init(stmt.getLdapPassword());
LdapInfo info = new LdapInfo(stmt.getLdapPassword());
Catalog.getCurrentCatalog().getEditLog().logSetLdapPassword(info);
}
public void replaySetLdapPassword(LdapInfo info) {
LdapClient.init(info.getLdapPasswd());
LOG.debug("finish replaying ldap admin password.");
}
// create role
public void createRole(CreateRoleStmt stmt) throws DdlException {
createRoleInternal(stmt.getQualifiedRole(), false);
@ -1108,6 +1185,8 @@ public class PaloAuth implements Writable {
List<String> userAuthInfo = Lists.newArrayList();
// global
// ldap global privs.
PrivBitSet ldapGlobalPrivs = LdapPrivsChecker.getGlobalPrivFromLdap(userIdent);
for (PrivEntry entry : userPrivTable.entries) {
if (!entry.match(userIdent, true /* exact match */)) {
continue;
@ -1120,23 +1199,27 @@ public class PaloAuth implements Writable {
} else {
userAuthInfo.add((gEntry.getPassword() == null || gEntry.getPassword().length == 0) ? "No" : "Yes");
}
userAuthInfo.add(gEntry.getPrivSet().toString() + " (" + gEntry.isSetByDomainResolver() + ")");
PrivBitSet savedPrivs = gEntry.getPrivSet().copy();
savedPrivs.or(ldapGlobalPrivs);
userAuthInfo.add(savedPrivs.toString() + " (" + gEntry.isSetByDomainResolver() + ")");
break;
}
if (userAuthInfo.isEmpty()) {
if (!userIdent.isDomain()) {
userAuthInfo.add(userIdent.toString());
if (LdapPrivsChecker.hasLdapPrivs(userIdent)) {
userAuthInfo.add("No");
userAuthInfo.add(ldapGlobalPrivs.toString() + " (false)");
} else if (!userIdent.isDomain()) {
// If this is not a domain user identity, it must have global priv entry.
// TODO(cmy): I don't know why previous comment said:
// This may happen when we grant non global privs to a non exist user via GRANT stmt.
LOG.warn("user identity does not have global priv entry: {}", userIdent);
userAuthInfo.add(userIdent.toString());
userAuthInfo.add(FeConstants.null_string);
userAuthInfo.add(FeConstants.null_string);
} else {
// this is a domain user identity and fall in here, which means this user identity does not
// have global priv, we need to check user property to see if it has password.
userAuthInfo.add(userIdent.toString());
userAuthInfo.add(propertyMgr.doesUserHasPassword(userIdent) ? "No" : "Yes");
userAuthInfo.add(FeConstants.null_string);
}
@ -1144,14 +1227,32 @@ public class PaloAuth implements Writable {
// db
List<String> dbPrivs = Lists.newArrayList();
Set<String> addedDbs = Sets.newHashSet();
for (PrivEntry entry : dbPrivTable.entries) {
if (!entry.match(userIdent, true /* exact match */)) {
continue;
}
DbPrivEntry dEntry = (DbPrivEntry) entry;
dbPrivs.add(dEntry.getOrigDb() + ": " + dEntry.getPrivSet().toString()
/**
* Doris and Ldap may have different privs on one database.
* Merge these privs and add.
*/
PrivBitSet savedPrivs = dEntry.getPrivSet().copy();
savedPrivs.or(LdapPrivsChecker.getDbPrivFromLdap(userIdent, dEntry.getOrigDb()));
addedDbs.add(dEntry.getOrigDb());
dbPrivs.add(dEntry.getOrigDb() + ": " + savedPrivs.toString()
+ " (" + entry.isSetByDomainResolver() + ")");
}
// Add privs from ldap groups that have not been added in Doris.
if (LdapPrivsChecker.hasLdapPrivs(userIdent)) {
Map<TablePattern, PrivBitSet> ldapDbPrivs = LdapPrivsChecker.getLdapAllDbPrivs(userIdent);
for (Map.Entry<TablePattern, PrivBitSet> entry : ldapDbPrivs.entrySet()) {
if (!addedDbs.contains(entry.getKey().getQualifiedDb())) {
dbPrivs.add(entry.getKey().getQualifiedDb() + ": " + entry.getValue().toString() + " (" + false + ")");
}
}
}
if (dbPrivs.isEmpty()) {
userAuthInfo.add(FeConstants.null_string);
} else {
@ -1160,15 +1261,34 @@ public class PaloAuth implements Writable {
// tbl
List<String> tblPrivs = Lists.newArrayList();
Set<String> addedtbls = Sets.newHashSet();
for (PrivEntry entry : tablePrivTable.entries) {
if (!entry.match(userIdent, true /* exact match */)) {
continue;
}
TablePrivEntry tEntry = (TablePrivEntry) entry;
/**
* Doris and Ldap may have different privs on one table.
* Merge these privs and add.
*/
PrivBitSet savedPrivs = tEntry.getPrivSet().copy();
savedPrivs.or(LdapPrivsChecker.getTblPrivFromLdap(userIdent, tEntry.getOrigDb(), tEntry.getOrigTbl()));
addedtbls.add(tEntry.getOrigDb().concat(".").concat(tEntry.getOrigTbl()));
tblPrivs.add(tEntry.getOrigDb() + "." + tEntry.getOrigTbl() + ": "
+ tEntry.getPrivSet().toString()
+ savedPrivs.toString()
+ " (" + entry.isSetByDomainResolver() + ")");
}
// Add privs from ldap groups that have not been added in Doris.
if (LdapPrivsChecker.hasLdapPrivs(userIdent)) {
Map<TablePattern, PrivBitSet> ldapTblPrivs = LdapPrivsChecker.getLdapAllTblPrivs(userIdent);
for (Map.Entry<TablePattern, PrivBitSet> entry : ldapTblPrivs.entrySet()) {
if (!addedtbls.contains(entry.getKey().getQualifiedDb().concat(".").concat(entry.getKey().getTbl()))) {
tblPrivs.add(entry.getKey().getQualifiedDb().concat(".").concat(entry.getKey().getTbl())
.concat(": ").concat(entry.getValue().toString()).concat(" (false)"));
}
}
}
if (tblPrivs.isEmpty()) {
userAuthInfo.add(FeConstants.null_string);
} else {
@ -1177,14 +1297,33 @@ public class PaloAuth implements Writable {
// resource
List<String> resourcePrivs = Lists.newArrayList();
Set<String> addedResources = Sets.newHashSet();
for (PrivEntry entry : resourcePrivTable.entries) {
if (!entry.match(userIdent, true /* exact match */)) {
continue;
}
ResourcePrivEntry rEntry = (ResourcePrivEntry) entry;
resourcePrivs.add(rEntry.getOrigResource() + ": " + rEntry.getPrivSet().toString()
/**
* Doris and Ldap may have different privs on one resource.
* Merge these privs and add.
*/
PrivBitSet savedPrivs = rEntry.getPrivSet().copy();
savedPrivs.or(LdapPrivsChecker.getResourcePrivFromLdap(userIdent, rEntry.getOrigResource()));
addedResources.add(rEntry.getOrigResource());
resourcePrivs.add(rEntry.getOrigResource() + ": " + savedPrivs.toString()
+ " (" + entry.isSetByDomainResolver() + ")");
}
// Add privs from ldap groups that have not been added in Doris.
if (LdapPrivsChecker.hasLdapPrivs(userIdent)) {
Map<ResourcePattern, PrivBitSet> ldapResourcePrivs = LdapPrivsChecker.getLdapAllResourcePrivs(userIdent);
for (Map.Entry<ResourcePattern, PrivBitSet> entry : ldapResourcePrivs.entrySet()) {
if (!addedResources.contains(entry.getKey().getResourceName())) {
tblPrivs.add(entry.getKey().getResourceName().concat(": ").concat(entry.getValue().toString())
.concat(" (false)"));
}
}
}
if (resourcePrivs.isEmpty()) {
userAuthInfo.add(FeConstants.null_string);
} else {

View File

@ -96,8 +96,8 @@ public class PaloRole implements Writable {
return users;
}
public void merge(PaloRole other) {
Preconditions.checkState(roleName.equalsIgnoreCase(other.getRoleName()));
// merge role not check role name.
public void mergeNotCheck(PaloRole other) {
for (Map.Entry<TablePattern, PrivBitSet> entry : other.getTblPatternToPrivs().entrySet()) {
if (tblPatternToPrivs.containsKey(entry.getKey())) {
PrivBitSet existPrivs = tblPatternToPrivs.get(entry.getKey());
@ -116,6 +116,11 @@ public class PaloRole implements Writable {
}
}
public void merge(PaloRole other) {
Preconditions.checkState(roleName.equalsIgnoreCase(other.getRoleName()));
mergeNotCheck(other);
}
public void addUser(UserIdentity userIdent) {
users.add(userIdent);
}

View File

@ -175,6 +175,17 @@ public class UserPrivTable extends PrivTable {
return false;
}
// Check whether the user exists and return the UserIdentity.
public UserIdentity getCurrentUserIdentity(UserIdentity userIdent) {
for (PrivEntry privEntry : entries) {
GlobalPrivEntry globalPrivEntry = (GlobalPrivEntry) privEntry;
if (globalPrivEntry.match(userIdent, false)) {
return globalPrivEntry.getDomainUserIdent();
}
}
return null;
}
@Override
public void write(DataOutput out) throws IOException {
if (!isClassNameWrote) {

View File

@ -485,6 +485,11 @@ public class EditLog {
catalog.getAuth().replaySetPassword(privInfo);
break;
}
case OperationType.OP_SET_LDAP_PASSWORD: {
LdapInfo ldapInfo = (LdapInfo) journal.getData();
catalog.getAuth().replaySetLdapPassword(ldapInfo);
break;
}
case OperationType.OP_CREATE_ROLE: {
PrivInfo privInfo = (PrivInfo) journal.getData();
catalog.getAuth().replayCreateRole(privInfo);
@ -1128,6 +1133,10 @@ public class EditLog {
logEdit(OperationType.OP_SET_PASSWORD, info);
}
public void logSetLdapPassword(LdapInfo info) {
logEdit(OperationType.OP_SET_LDAP_PASSWORD, info);
}
public void logCreateRole(PrivInfo info) {
logEdit(OperationType.OP_CREATE_ROLE, info);
}

View File

@ -0,0 +1,51 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.persist;
import com.google.gson.annotations.SerializedName;
import org.apache.doris.common.io.Text;
import org.apache.doris.common.io.Writable;
import org.apache.doris.persist.gson.GsonUtils;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class LdapInfo implements Writable {
@SerializedName(value = "ldapPasswd")
private String ldapPasswd;
public LdapInfo(String ldapPasswd) {
this.ldapPasswd = ldapPasswd;
}
public String getLdapPasswd() {
return ldapPasswd;
}
@Override
public void write(DataOutput out) throws IOException {
String json = GsonUtils.GSON.toJson(this);
Text.writeString(out, json);
}
public static LdapInfo read(DataInput in) throws IOException {
String json = Text.readString(in);
return GsonUtils.GSON.fromJson(json, LdapInfo.class);
}
}

View File

@ -202,6 +202,8 @@ public class OperationType {
// alter external table
public static final short OP_ALTER_EXTERNAL_TABLE_SCHEMA = 280;
public static final short OP_SET_LDAP_PASSWORD = 290;
// get opcode name by op codeStri
public static String getOpName(short opCode) {
try {

View File

@ -26,6 +26,7 @@ import org.apache.doris.mysql.MysqlCapability;
import org.apache.doris.mysql.MysqlChannel;
import org.apache.doris.mysql.MysqlCommand;
import org.apache.doris.mysql.MysqlSerializer;
import org.apache.doris.mysql.privilege.PaloRole;
import org.apache.doris.plugin.AuditEvent.AuditEventBuilder;
import org.apache.doris.thrift.TResourceInfo;
import org.apache.doris.thrift.TUniqueId;
@ -74,6 +75,10 @@ public class ConnectContext {
protected volatile String clusterName = "";
// username@host of current login user
protected volatile String qualifiedUser;
// LDAP authenticated but the Doris account does not exist, set the flag, and the user login Doris as Temporary user.
protected volatile boolean isTempUser = false;
// Save the privs from the ldap groups.
protected volatile PaloRole ldapGroupsPrivs = null;
// username@host combination for the Doris account
// that the server used to authenticate the current client.
// In other word, currentUserIdentity is the entry that matched in Doris auth table.
@ -261,6 +266,16 @@ public class ConnectContext {
this.qualifiedUser = qualifiedUser;
}
public boolean getIsTempUser() { return isTempUser;}
public void setIsTempUser(boolean isTempUser) { this.isTempUser = isTempUser;}
public PaloRole getLdapGroupsPrivs() { return ldapGroupsPrivs; }
public void setLdapGroupsPrivs(PaloRole ldapGroupsPrivs) {
this.ldapGroupsPrivs = ldapGroupsPrivs;
}
// for USER() function
public UserIdentity getUserIdentity() {
return new UserIdentity(qualifiedUser, remoteIP);

View File

@ -20,6 +20,7 @@ package org.apache.doris.qe;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.Config;
import org.apache.doris.common.ThreadPoolManager;
import org.apache.doris.ldap.LdapAuthenticate;
import org.apache.doris.mysql.MysqlProto;
import org.apache.doris.mysql.nio.NConnectContext;
import org.apache.doris.mysql.privilege.PrivPredicate;
@ -103,7 +104,11 @@ public class ConnectScheduler {
connByUser.put(ctx.getQualifiedUser(), new AtomicInteger(0));
}
int conns = connByUser.get(ctx.getQualifiedUser()).get();
if (conns >= ctx.getCatalog().getAuth().getMaxConn(ctx.getQualifiedUser())) {
if (ctx.getIsTempUser()) {
if (conns >= LdapAuthenticate.getMaxConn()) {
return false;
}
} else if (conns >= ctx.getCatalog().getAuth().getMaxConn(ctx.getQualifiedUser())) {
return false;
}
numberConnection++;

View File

@ -19,6 +19,7 @@ package org.apache.doris.qe;
import org.apache.doris.analysis.SetNamesVar;
import org.apache.doris.analysis.SetPassVar;
import org.apache.doris.analysis.SetLdapPassVar;
import org.apache.doris.analysis.SetStmt;
import org.apache.doris.analysis.SetTransaction;
import org.apache.doris.analysis.SetVar;
@ -44,6 +45,9 @@ public class SetExecutor {
// Set password
SetPassVar setPassVar = (SetPassVar) var;
ctx.getCatalog().getAuth().setPassword(setPassVar);
} else if(var instanceof SetLdapPassVar){
SetLdapPassVar setLdapPassVar = (SetLdapPassVar) var;
ctx.getCatalog().getAuth().setLdapPassword(setLdapPassVar);
} else if (var instanceof SetNamesVar) {
// do nothing
return;

View File

@ -284,6 +284,7 @@ import org.apache.doris.qe.SqlModeHelper;
keywordMap.put("partition", new Integer(SqlParserSymbols.KW_PARTITION));
keywordMap.put("partitions", new Integer(SqlParserSymbols.KW_PARTITIONS));
keywordMap.put("password", new Integer(SqlParserSymbols.KW_PASSWORD));
keywordMap.put("ldap_admin_password", new Integer(SqlParserSymbols.KW_LDAP_ADMIN_PASSWORD));
keywordMap.put("path", new Integer(SqlParserSymbols.KW_PATH));
keywordMap.put("pause", new Integer(SqlParserSymbols.KW_PAUSE));
keywordMap.put("plugin", new Integer(SqlParserSymbols.KW_PLUGIN));

View File

@ -0,0 +1,212 @@
package org.apache.doris.ldap;
import com.google.common.collect.Lists;
import mockit.Delegate;
import mockit.Expectations;
import mockit.Mocked;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.DdlException;
import org.apache.doris.mysql.privilege.PaloAuth;
import org.apache.doris.mysql.privilege.PaloRole;
import org.apache.doris.qe.ConnectContext;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class LdapAuthenticateTest {
private static final String DEFAULT_CLUSTER = "default_cluster";
private static final String USER_NAME = "user";
private static final String IP = "192.168.1.1";
private static final String TABLE_RD = "palo_rd";
private PaloRole ldapGroupsPrivs;
@Mocked
private LdapClient ldapClient;
@Mocked
private LdapPrivsChecker ldapPrivsChecker;
@Mocked
private Catalog catalog;
@Mocked
private PaloAuth auth;
@Before
public void setUp() throws DdlException {
new Expectations() {
{
auth.doesRoleExist(anyString);
minTimes = 0;
result = true;
auth.mergeRolesNoCheckName((List<String>) any, (PaloRole) any);
minTimes = 0;
result = new Delegate() {
void fakeMergeRolesNoCheckName(List<String> roles, PaloRole savedRole) {
ldapGroupsPrivs = savedRole;
}
};
catalog.getAuth();
minTimes = 0;
result = auth;
Catalog.getCurrentCatalog();
minTimes = 0;
result = catalog;
}
};
}
private void setCheckPassword(boolean res) {
new Expectations() {
{
LdapClient.checkPassword(anyString, anyString);
minTimes = 0;
result = res;
}
};
}
private void setCheckPasswordException() {
new Expectations() {
{
LdapClient.checkPassword(anyString, anyString);
minTimes = 0;
result = new RuntimeException("exception");
}
};
}
private void setGetGroups(boolean res) {
new Expectations() {
{
if (res) {
LdapClient.getGroups(anyString);
minTimes = 0;
result = new Delegate() {
List<String> fakeGetGroups(String user) {
List<String> list = new ArrayList<>();
list.add(TABLE_RD);
return list;
}
};
} else {
LdapClient.getGroups(anyString);
minTimes = 0;
result = Lists.newArrayList();
}
}
};
}
private void setGetGroupsException() {
new Expectations() {
{
LdapClient.getGroups(anyString);
minTimes = 0;
result = new RuntimeException("exception");
}
};
}
private void setGetCurrentUserIdentity(boolean res) {
new Expectations() {
{
if (res) {
auth.getCurrentUserIdentity((UserIdentity) any);
minTimes = 0;
result = new UserIdentity(ClusterNamespace.getFullName(DEFAULT_CLUSTER, USER_NAME), IP);
} else {
auth.getCurrentUserIdentity((UserIdentity) any);
minTimes = 0;
result = null;
}
}
};
}
private ConnectContext getContext() {
ConnectContext context = new ConnectContext(null);
context.setCatalog(catalog);
context.setThreadLocalInfo();
return context;
}
@Test
public void testAuthenticate() {
ConnectContext context = getContext();
setCheckPassword(true);
setGetGroups(true);
setGetCurrentUserIdentity(true);
String qualifiedUser = ClusterNamespace.getFullName(DEFAULT_CLUSTER, USER_NAME);
Assert.assertTrue(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertFalse(context.getIsTempUser());
Assert.assertSame(ldapGroupsPrivs, context.getLdapGroupsPrivs());
}
@Test
public void testAuthenticateWithWrongPassword() {
ConnectContext context = getContext();
setCheckPassword(false);
setGetGroups(true);
setGetCurrentUserIdentity(true);
String qualifiedUser = ClusterNamespace.getFullName(DEFAULT_CLUSTER, USER_NAME);
Assert.assertFalse(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertFalse(context.getIsTempUser());
Assert.assertNull(context.getLdapGroupsPrivs());
}
@Test
public void testAuthenticateWithCheckPasswordException() {
ConnectContext context = getContext();
setCheckPasswordException();
setGetGroups(true);
setGetCurrentUserIdentity(true);
String qualifiedUser = ClusterNamespace.getFullName(DEFAULT_CLUSTER, USER_NAME);
Assert.assertFalse(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertFalse(context.getIsTempUser());
Assert.assertNull(context.getLdapGroupsPrivs());
}
@Test
public void testAuthenticateGetGroupsNull() {
ConnectContext context = getContext();
setCheckPassword(true);
setGetGroups(false);
setGetCurrentUserIdentity(true);
String qualifiedUser = ClusterNamespace.getFullName(DEFAULT_CLUSTER, USER_NAME);
Assert.assertTrue(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertFalse(context.getIsTempUser());
Assert.assertNull(context.getLdapGroupsPrivs());
}
@Test
public void testAuthenticateGetGroupsException() {
ConnectContext context = getContext();
setCheckPassword(true);
setGetGroupsException();
setGetCurrentUserIdentity(true);
String qualifiedUser = ClusterNamespace.getFullName(DEFAULT_CLUSTER, USER_NAME);
Assert.assertFalse(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertFalse(context.getIsTempUser());
Assert.assertNull(context.getLdapGroupsPrivs());
}
@Test
public void testAuthenticateUserNotExistInDoris() {
ConnectContext context = getContext();
setCheckPassword(true);
setGetGroups(true);
setGetCurrentUserIdentity(false);
String qualifiedUser = ClusterNamespace.getFullName(DEFAULT_CLUSTER, USER_NAME);
Assert.assertTrue(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertTrue(context.getIsTempUser());
Assert.assertSame(ldapGroupsPrivs, context.getLdapGroupsPrivs());
}
}

View File

@ -0,0 +1,102 @@
package org.apache.doris.ldap;
import com.clearspring.analytics.util.Lists;
import mockit.Delegate;
import mockit.Expectations;
import mockit.Mocked;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.common.util.SymmetricEncryption;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.AbstractContextMapper;
import org.springframework.ldap.query.LdapQuery;
import java.util.List;
public class LdapClientTest {
private static final String ADMIN_PASSWORD = "admin";
@Mocked
private LdapTemplate ldapTemplate;
@Before
public void setUp() {
LdapConfig.ldap_authentication_enabled = true;
LdapConfig.ldap_host = "127.0.0.1";
LdapConfig.ldap_port = 389;
LdapConfig.ldap_admin_name = "cn=admin,dc=baidu,dc=com";
LdapConfig.ldap_user_basedn = "dc=baidu,dc=com";
LdapConfig.ldap_group_basedn = "ou=group,dc=baidu,dc=com";
LdapConfig.ldap_user_filter = "(&(uid={login}))";
LdapClient.init(SymmetricEncryption.encrypt(ADMIN_PASSWORD));
}
private void mockLdapTemplateSearch(List list) {
new Expectations() {
{
ldapTemplate.search((LdapQuery) any, (AbstractContextMapper) any);
minTimes = 0;
result = list;
}
};
}
private void mockLdapTemplateAuthenticate(String password) {
new Expectations() {
{
ldapTemplate.authenticate((LdapQuery) any, anyString);
minTimes = 0;
result = new Delegate() {
void fakeAuthenticate(LdapQuery query, String passwd) {
if (passwd.equals(password)) {
return;
} else {
throw new RuntimeException("exception");
}
}
};
}
};
}
@Test
public void testDoesUserExist() {
List<String> list = Lists.newArrayList();
list.add("zhangsan");
mockLdapTemplateSearch(list);
Assert.assertTrue(LdapClient.doesUserExist("zhangsan"));
}
@Test
public void testDoesUserExistFail() {
mockLdapTemplateSearch(null);
Assert.assertFalse(LdapClient.doesUserExist("zhangsan"));
}
@Test(expected = RuntimeException.class)
public void testDoesUserExistException() {
List<String> list = Lists.newArrayList();
list.add("zhangsan");
list.add("zhangsan");
mockLdapTemplateSearch(list);
Assert.assertTrue(LdapClient.doesUserExist("zhangsan"));
Assert.fail("No Exception throws.");
}
@Test
public void testCheckPassword() {
mockLdapTemplateAuthenticate(ADMIN_PASSWORD);
Assert.assertTrue(LdapClient.checkPassword("zhangsan", ADMIN_PASSWORD));
Assert.assertFalse(LdapClient.checkPassword("zhangsan", "123"));
}
@Test
public void testGetGroups() {
List<String> list = Lists.newArrayList();
list.add("cn=groupName,ou=groups,dc=example,dc=com");
mockLdapTemplateSearch(list);
Assert.assertEquals(1, LdapClient.getGroups("zhangsan").size());
}
}

View File

@ -0,0 +1,189 @@
package org.apache.doris.ldap;
import mockit.Expectations;
import mockit.Mocked;
import org.apache.doris.analysis.ResourcePattern;
import org.apache.doris.analysis.TablePattern;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.mysql.privilege.PaloPrivilege;
import org.apache.doris.mysql.privilege.PaloRole;
import org.apache.doris.mysql.privilege.PrivBitSet;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.Map;
public class LdapPrivsCheckerTest {
private static final String CLUSTER = "default_cluster";
private static final String DB = "palodb";
private static final String TABLE_DB = "tabledb";
private static final String TABLE1 = "table1";
private static final String TABLE2 = "table2";
private static final String RESOURCE1 = "spark_resource";
private static final String RESOURCE2 = "resource";
private static final String USER = "default_cluster:zhangsan";
private static final String IP = "192.168.0.1";
private UserIdentity userIdent = UserIdentity.createAnalyzedUserIdentWithIp(USER, IP);
@Mocked
private ConnectContext context;
@Before
public void setUp() {
LdapConfig.ldap_authentication_enabled = true;
new Expectations() {
{
ConnectContext.get();
minTimes = 0;
result = context;
PaloRole role = new PaloRole("");
Map<TablePattern, PrivBitSet> tblPatternToPrivs = role.getTblPatternToPrivs();
TablePattern global = new TablePattern("*", "*");
tblPatternToPrivs.put(global, PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.CREATE_PRIV));
TablePattern db = new TablePattern(DB, "*");
tblPatternToPrivs.put(db, PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.LOAD_PRIV));
TablePattern tbl1 = new TablePattern(TABLE_DB, TABLE1);
tblPatternToPrivs.put(tbl1, PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.ALTER_PRIV));
TablePattern tbl2 = new TablePattern(TABLE_DB, TABLE2);
tblPatternToPrivs.put(tbl2, PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.DROP_PRIV));
Map<ResourcePattern, PrivBitSet> resourcePatternToPrivs = role.getResourcePatternToPrivs();
ResourcePattern globalResource = new ResourcePattern("*");
resourcePatternToPrivs.put(globalResource, PrivBitSet.of(PaloPrivilege.USAGE_PRIV));
ResourcePattern resource1 = new ResourcePattern(RESOURCE1);
resourcePatternToPrivs.put(resource1, PrivBitSet.of(PaloPrivilege.USAGE_PRIV));
ResourcePattern resource2 = new ResourcePattern(RESOURCE1);
resourcePatternToPrivs.put(resource2, PrivBitSet.of(PaloPrivilege.USAGE_PRIV));
try {
global.analyze(CLUSTER);
db.analyze(CLUSTER);
tbl1.analyze(CLUSTER);
tbl2.analyze(CLUSTER);
resource1.analyze();
resource2.analyze();
} catch (AnalysisException e) {
e.printStackTrace();
}
context.getLdapGroupsPrivs();
minTimes = 0;
result = role;
context.getCurrentUserIdentity();
minTimes = 0;
result = UserIdentity.createAnalyzedUserIdentWithIp(USER, IP);
}
};
}
@Test
public void testHasGlobalPrivFromLdap() {
Assert.assertTrue(LdapPrivsChecker.hasGlobalPrivFromLdap(userIdent, PrivPredicate.CREATE));
Assert.assertTrue(LdapPrivsChecker.hasGlobalPrivFromLdap(userIdent, PrivPredicate.USAGE));
Assert.assertFalse(LdapPrivsChecker.hasGlobalPrivFromLdap(userIdent, PrivPredicate.DROP));
}
@Test
public void testHasDbPrivFromLdap() {
Assert.assertTrue(LdapPrivsChecker.hasDbPrivFromLdap(userIdent, CLUSTER + ":" + DB, PrivPredicate.LOAD));
Assert.assertFalse(LdapPrivsChecker.hasDbPrivFromLdap(userIdent, CLUSTER + ":" + DB, PrivPredicate.DROP));
Assert.assertTrue(LdapPrivsChecker.hasDbPrivFromLdap(userIdent, PrivPredicate.LOAD));
Assert.assertFalse(LdapPrivsChecker.hasDbPrivFromLdap(userIdent, PrivPredicate.DROP));
}
@Test
public void testHasTblPrivFromLdap() {
Assert.assertTrue(LdapPrivsChecker.hasTblPrivFromLdap(userIdent, CLUSTER + ":" + TABLE_DB, TABLE1,
PrivPredicate.ALTER));
Assert.assertFalse(LdapPrivsChecker.hasTblPrivFromLdap(userIdent, CLUSTER + ":" + TABLE_DB, TABLE1,
PrivPredicate.DROP));
Assert.assertTrue(LdapPrivsChecker.hasTblPrivFromLdap(userIdent, CLUSTER + ":" + TABLE_DB, TABLE2,
PrivPredicate.DROP));
Assert.assertFalse(LdapPrivsChecker.hasTblPrivFromLdap(userIdent, CLUSTER + ":" + TABLE_DB, TABLE2,
PrivPredicate.CREATE));
Assert.assertTrue(LdapPrivsChecker.hasTblPrivFromLdap(userIdent, PrivPredicate.ALTER));
Assert.assertFalse(LdapPrivsChecker.hasTblPrivFromLdap(userIdent, PrivPredicate.LOAD));
}
@Test
public void testHasResourcePrivFromLdap() {
Assert.assertTrue(LdapPrivsChecker.hasResourcePrivFromLdap(userIdent, RESOURCE1, PrivPredicate.USAGE));
Assert.assertFalse(LdapPrivsChecker.hasResourcePrivFromLdap(userIdent, "resource",
PrivPredicate.USAGE));
}
@Test
public void testGetGlobalPrivFromLdap() {
Assert.assertEquals(PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.CREATE_PRIV, PaloPrivilege.USAGE_PRIV).toString(),
LdapPrivsChecker.getGlobalPrivFromLdap(userIdent).toString());
}
@Test
public void testGetDbPrivFromLdap() {
Assert.assertEquals(PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.LOAD_PRIV).toString(),
LdapPrivsChecker.getDbPrivFromLdap(userIdent, CLUSTER + ":" + DB).toString());
}
@Test
public void testGetTblPrivFromLdap() {
Assert.assertEquals(PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.ALTER_PRIV).toString(),
LdapPrivsChecker.getTblPrivFromLdap(userIdent, CLUSTER + ":" + TABLE_DB, TABLE1).toString());
}
@Test
public void testGetResourcePrivFromLdap() {
Assert.assertEquals(PrivBitSet.of(PaloPrivilege.USAGE_PRIV).toString(),
LdapPrivsChecker.getResourcePrivFromLdap(userIdent, RESOURCE1).toString());
}
@Test
public void testHasPrivsOfDb() {
Assert.assertTrue(LdapPrivsChecker.hasPrivsOfDb(userIdent, CLUSTER + ":" + TABLE_DB));
}
@Test
public void testIsCurrentUser() {
Assert.assertTrue(LdapPrivsChecker.isCurrentUser(userIdent));
Assert.assertFalse(LdapPrivsChecker.isCurrentUser(UserIdentity.
createAnalyzedUserIdentWithIp("default_cluster:lisi", IP)));
Assert.assertFalse(LdapPrivsChecker.isCurrentUser(UserIdentity.
createAnalyzedUserIdentWithIp(USER, "127.0.0.1")));
}
@Test
public void testGetLdapAllDbPrivs() throws AnalysisException {
Map<TablePattern, PrivBitSet> allDb = LdapPrivsChecker.getLdapAllDbPrivs(userIdent);
TablePattern db = new TablePattern(DB, "*");
db.analyze(CLUSTER);
Assert.assertEquals(PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.LOAD_PRIV).toString(),
allDb.get(db).toString());
}
@Test
public void testGetLdapAllTblPrivs() throws AnalysisException {
Map<TablePattern, PrivBitSet> allTbl = LdapPrivsChecker.getLdapAllTblPrivs(userIdent);
TablePattern tbl1 = new TablePattern(TABLE_DB, TABLE1);
TablePattern tbl2 = new TablePattern(TABLE_DB, TABLE2);
tbl1.analyze(CLUSTER);
tbl2.analyze(CLUSTER);
Assert.assertEquals(PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.ALTER_PRIV).toString(),
allTbl.get(tbl1).toString());
Assert.assertEquals(PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.DROP_PRIV).toString(),
allTbl.get(tbl2).toString());
}
@Test
public void testGetLdapAllResourcePrivs() {
Map<ResourcePattern, PrivBitSet> allResource = LdapPrivsChecker.getLdapAllResourcePrivs(userIdent);
ResourcePattern resource1 = new ResourcePattern(RESOURCE1);
ResourcePattern resource2 = new ResourcePattern(RESOURCE1);
Assert.assertEquals(PrivBitSet.of(PaloPrivilege.USAGE_PRIV).toString(), allResource.get(resource1).toString());
Assert.assertEquals(PrivBitSet.of(PaloPrivilege.USAGE_PRIV).toString(), allResource.get(resource2).toString());
}
}

View File

@ -23,7 +23,11 @@ import mockit.Mocked;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.catalog.Database;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.ldap.LdapAuthenticate;
import org.apache.doris.ldap.LdapClient;
import org.apache.doris.mysql.privilege.PaloAuth;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
@ -40,6 +44,8 @@ import java.util.List;
public class MysqlProtoTest {
private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(MysqlProtoTest.class);
private static final String PASSWORD_CLEAR_TEXT = "123456";
@Mocked
private MysqlChannel channel;
@Mocked
@ -48,6 +54,12 @@ public class MysqlProtoTest {
private Catalog catalog;
@Mocked
private PaloAuth auth;
@Mocked
private LdapClient ldapClient;
@Mocked
private LdapAuthenticate ldapAuthenticate;
@Mocked
private MysqlClearTextPacket clearTextPacket;
@Before
public void setUp() throws DdlException {
@ -64,7 +76,7 @@ public class MysqlProtoTest {
result = new Delegate() {
boolean fakeCheckPassword(String remoteUser, String remoteHost, byte[] remotePasswd, byte[] randomString,
List<UserIdentity> currentUser) {
UserIdentity userIdentity = new UserIdentity("defaut_cluster:user", "192.168.1.1");
UserIdentity userIdentity = new UserIdentity("default_cluster:user", "192.168.1.1");
currentUser.add(userIdentity);
return true;
}
@ -150,6 +162,20 @@ public class MysqlProtoTest {
};
}
private void mockMysqlClearTextPacket(String password) throws IOException {
new Expectations() {
{
clearTextPacket.getPassword();
minTimes = 0;
result = password;
clearTextPacket.readFrom((ByteBuffer) any);
minTimes = 0;
result = true;
}
};
}
private void mockPassword(boolean res) {
// mock password
new Expectations(password) {
@ -172,6 +198,27 @@ public class MysqlProtoTest {
private void mockAccess() throws Exception {
}
private void mockLdap(String user, boolean userExist) {
LdapConfig.ldap_authentication_enabled = true;
new Expectations() {
{
LdapAuthenticate.authenticate((ConnectContext) any, anyString, anyString);
minTimes = 0;
result = new Delegate() {
boolean fakeLdapAuthenticate(ConnectContext context, String password, String qualifiedUser) {
return password.equals(PASSWORD_CLEAR_TEXT)
&& ClusterNamespace.getNameFromFullName(qualifiedUser).equals(user);
}
};
LdapClient.doesUserExist(anyString);
minTimes = 0;
result = userExist;
}
};
}
@Test
public void testNegotiate() throws Exception {
mockChannel("user", true);
@ -211,6 +258,45 @@ public class MysqlProtoTest {
Assert.assertFalse(MysqlProto.negotiate(context));
}
@Test
public void testNegotiateLdap() throws Exception {
mockChannel("user", true);
mockPassword(true);
mockAccess();
mockMysqlClearTextPacket(PASSWORD_CLEAR_TEXT);
mockLdap("user", true);
ConnectContext context = new ConnectContext(null);
context.setCatalog(catalog);
context.setThreadLocalInfo();
Assert.assertTrue(MysqlProto.negotiate(context));
}
@Test
public void testNegotiateLdapInvalidPasswd() throws Exception {
mockChannel("user", true);
mockPassword(true);
mockAccess();
mockMysqlClearTextPacket("654321");
mockLdap("user", true);
ConnectContext context = new ConnectContext(null);
context.setCatalog(catalog);
context.setThreadLocalInfo();
Assert.assertFalse(MysqlProto.negotiate(context));
}
@Test
public void testNegotiateLdapRoot() throws Exception {
mockChannel("root", true);
mockPassword(true);
mockAccess();
mockLdap("root", false);
mockMysqlClearTextPacket("654321");
ConnectContext context = new ConnectContext(null);
context.setCatalog(catalog);
context.setThreadLocalInfo();
Assert.assertTrue(MysqlProto.negotiate(context));
}
@Test
public void testRead() throws UnsupportedEncodingException {
MysqlSerializer serializer = MysqlSerializer.newInstance();

View File

@ -0,0 +1,35 @@
package org.apache.doris.persist;
import org.junit.Assert;
import org.junit.Test;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class LdapInfoTest {
@Test
public void test() throws IOException {
LdapInfo ldapInfo = new LdapInfo("123456");
// 1. Write objects to file
File file = new File("./ldapInfo");
file.createNewFile();
DataOutputStream dos = new DataOutputStream(new FileOutputStream(file));
ldapInfo.write(dos);
dos.flush();
dos.close();
// 2. Read objects from file
DataInputStream dis = new DataInputStream(new FileInputStream(file));
Assert.assertEquals("123456", LdapInfo.read(dis).getLdapPasswd());
// 3. delete files
dis.close();
file.delete();
}
}