[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:
@ -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
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
145
fe/fe-core/src/main/java/org/apache/doris/common/LdapConfig.java
Normal file
145
fe/fe-core/src/main/java/org/apache/doris/common/LdapConfig.java
Normal 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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
182
fe/fe-core/src/main/java/org/apache/doris/ldap/LdapClient.java
Normal file
182
fe/fe-core/src/main/java/org/apache/doris/ldap/LdapClient.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@ -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)) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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++;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user