[refactor](Mysql) Refactoring the process of using external components to authenticate in MySQL connections (#32875) (#33958)

bp #32875

Co-authored-by: LompleZ Liu <47652868+LompleZ@users.noreply.github.com>
This commit is contained in:
Mingyu Chen
2024-04-22 14:40:52 +08:00
committed by yiguolei
parent 71314595be
commit 88b3d61eca
16 changed files with 286 additions and 150 deletions

View File

@ -23,8 +23,8 @@ import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeNameFormat;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.common.UserException;
import org.apache.doris.mysql.authenticate.MysqlAuthType;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.mysql.privilege.Role;
import org.apache.doris.qe.ConnectContext;
@ -119,7 +119,8 @@ public class CreateUserStmt extends DdlStmt {
public void analyze(Analyzer analyzer) throws UserException {
super.analyze(analyzer);
if (Config.access_controller_type.equalsIgnoreCase("ranger-doris") && LdapConfig.ldap_authentication_enabled) {
if (Config.access_controller_type.equalsIgnoreCase("ranger-doris")
&& MysqlAuthType.getAuthTypeConfig() == MysqlAuthType.LDAP) {
throw new AnalysisException("Create user is prohibited when Ranger and LDAP are enabled at same time.");
}

View File

@ -22,8 +22,8 @@ import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.common.UserException;
import org.apache.doris.mysql.authenticate.MysqlAuthType;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
@ -56,7 +56,8 @@ public class DropUserStmt extends DdlStmt {
public void analyze(Analyzer analyzer) throws AnalysisException, UserException {
super.analyze(analyzer);
if (Config.access_controller_type.equalsIgnoreCase("ranger-doris") && LdapConfig.ldap_authentication_enabled) {
if (Config.access_controller_type.equalsIgnoreCase("ranger-doris")
&& MysqlAuthType.getAuthTypeConfig() == MysqlAuthType.LDAP) {
throw new AnalysisException("Drop user is prohibited when Ranger and LDAP are enabled at same time.");
}

View File

@ -17,53 +17,27 @@
package org.apache.doris.mysql;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.common.AuthenticationException;
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.datasource.CatalogIf;
import org.apache.doris.ldap.LdapAuthenticate;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.mysql.authenticate.MysqlAuth;
import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
// MySQL protocol util
public class MysqlProto {
private static final Logger LOG = LogManager.getLogger(MysqlProto.class);
public static final boolean SERVER_USE_SSL = Config.enable_ssl;
// 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 qualifiedUser) {
String remoteIp = context.getMysqlChannel().getRemoteIp();
List<UserIdentity> currentUserIdentity = Lists.newArrayList();
try {
Env.getCurrentEnv().getAuth().checkPassword(qualifiedUser, remoteIp,
scramble, randomString, currentUserIdentity);
} catch (AuthenticationException e) {
ErrorReport.report(e.errorCode, e.msgs);
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";
@ -100,25 +74,10 @@ 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(Auth.ROOT_USER) || qualifiedUser.equals(Auth.ADMIN_USER)) {
return false;
}
// If LDAP authentication is enabled and the user exists in LDAP, use LDAP authentication,
// otherwise use Doris authentication.
return LdapConfig.ldap_authentication_enabled && Env.getCurrentEnv().getAuth().getLdapManager()
.doesUserExist(qualifiedUser);
}
/**
* 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:
@ -234,81 +193,11 @@ public class MysqlProto {
return false;
}
boolean useLdapAuthenticate;
try {
useLdapAuthenticate = useLdapAuthenticate(qualifiedUser);
} catch (Exception e) {
LOG.warn("Check if user exists in ldap error.", e);
sendResponsePacket(context);
// authenticate
if (!MysqlAuth.authenticate(context, qualifiedUser, channel, serializer, authPacket, handshakePacket)) {
return false;
}
if (useLdapAuthenticate) {
if (LOG.isDebugEnabled()) {
LOG.debug("user:{} start to ldap authenticate.", qualifiedUser);
}
// server send authentication switch packet to request password clear text.
// https://dev.mysql.com/doc/internals/en/authentication-method-change.html
serializer.reset();
MysqlAuthSwitchPacket mysqlAuthSwitchPacket = new MysqlAuthSwitchPacket();
mysqlAuthSwitchPacket.writeTo(serializer);
channel.sendAndFlush(serializer.toByteBuffer());
// Server receive password clear text.
ByteBuffer authSwitchResponse = channel.fetchOnePacket();
if (authSwitchResponse == null) {
return false;
}
MysqlClearTextPacket clearTextPacket = new MysqlClearTextPacket();
if (!clearTextPacket.readFrom(authSwitchResponse)) {
ErrorReport.report(ErrorCode.ERR_NOT_SUPPORTED_AUTH_MODE);
sendResponsePacket(context);
return false;
}
if (!LdapAuthenticate.authenticate(context, clearTextPacket.getPassword(), qualifiedUser)) {
sendResponsePacket(context);
return false;
}
} else {
// Starting with MySQL 8.0.4, MySQL changed the default authentication plugin for MySQL client
// from mysql_native_password to caching_sha2_password.
// ref: https://mysqlserverteam.com/mysql-8-0-4-new-default-authentication-plugin-caching_sha2_password/
// So, User use mysql client or ODBC Driver after 8.0.4 have problem to connect to Doris
// with password.
// So Doris support the Protocol::AuthSwitchRequest to tell client to keep the default password plugin
// which Doris is using now.
// Note: Check the authPacket whether support plugin auth firstly,
// before we check AuthPlugin between doris and client to compatible with older version: like mysql 5.1
if (authPacket.getCapability().isPluginAuth()
&& !handshakePacket.checkAuthPluginSameAsDoris(authPacket.getPluginName())) {
// 1. clear the serializer
serializer.reset();
// 2. build the auth switch request and send to the client
handshakePacket.buildAuthSwitchRequest(serializer);
channel.sendAndFlush(serializer.toByteBuffer());
// Server receive auth switch response packet from client.
ByteBuffer authSwitchResponse = channel.fetchOnePacket();
if (authSwitchResponse == null) {
// receive response failed.
return false;
}
// 3. the client use default password plugin of Doris to dispose
// password
authPacket.setAuthResponse(readEofString(authSwitchResponse));
}
// NOTE: when we behind proxy, we need random string sent by proxy.
byte[] randomString = handshakePacket.getAuthPluginData();
if (Config.proxy_auth_enable && authPacket.getRandomString() != null) {
randomString = authPacket.getRandomString();
}
// check authenticate
if (!authenticate(context, authPacket.getAuthResponse(), randomString, qualifiedUser)) {
sendResponsePacket(context);
return false;
}
}
// set database
String db = authPacket.getDb();
if (!Strings.isNullOrEmpty(db)) {

View File

@ -0,0 +1,205 @@
// 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.authenticate;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.common.AuthenticationException;
import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.mysql.MysqlAuthPacket;
import org.apache.doris.mysql.MysqlAuthSwitchPacket;
import org.apache.doris.mysql.MysqlChannel;
import org.apache.doris.mysql.MysqlClearTextPacket;
import org.apache.doris.mysql.MysqlHandshakePacket;
import org.apache.doris.mysql.MysqlProto;
import org.apache.doris.mysql.MysqlSerializer;
import org.apache.doris.mysql.authenticate.ldap.LdapAuthenticate;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.qe.ConnectContext;
import com.google.common.collect.Lists;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
public class MysqlAuth {
private static final Logger LOG = LogManager.getLogger(MysqlAuth.class);
// scramble: data receive from server.
// randomString: data send by server in plugin data field
// user_name#HIGH@cluster_name
private static boolean internalAuthenticate(ConnectContext context, byte[] scramble,
byte[] randomString, String qualifiedUser) {
String remoteIp = context.getMysqlChannel().getRemoteIp();
List<UserIdentity> currentUserIdentity = Lists.newArrayList();
try {
Env.getCurrentEnv().getAuth().checkPassword(qualifiedUser, remoteIp,
scramble, randomString, currentUserIdentity);
} catch (AuthenticationException e) {
ErrorReport.report(e.errorCode, e.msgs);
return false;
}
context.setCurrentUserIdentity(currentUserIdentity.get(0));
context.setRemoteIP(remoteIp);
return true;
}
// Default auth uses doris internal user system to authenticate.
private static boolean defaultAuth(
ConnectContext context,
String qualifiedUser,
MysqlChannel channel,
MysqlSerializer serializer,
MysqlAuthPacket authPacket,
MysqlHandshakePacket handshakePacket) throws IOException {
// 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(MysqlProto.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 (!internalAuthenticate(context, authPacket.getAuthResponse(), randomString, qualifiedUser)) {
MysqlProto.sendResponsePacket(context);
return false;
}
return true;
}
/*
* ldap:
* server ---AuthSwitch---> client
* server <--- clear text password --- client
*/
private static boolean ldapAuth(
ConnectContext context,
String qualifiedUser,
MysqlChannel channel,
MysqlSerializer serializer) throws IOException {
if (LOG.isDebugEnabled()) {
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);
MysqlProto.sendResponsePacket(context);
return false;
}
if (!LdapAuthenticate.authenticate(context, clearTextPacket.getPassword(), qualifiedUser)) {
MysqlProto.sendResponsePacket(context);
return false;
}
return true;
}
// Based on FE configuration and some prerequisites, decide which authentication type to actually use
private static MysqlAuthType useWhichAuthType(ConnectContext context, String qualifiedUser) throws IOException {
MysqlAuthType typeConfig = MysqlAuthType.getAuthTypeConfig();
// Root and admin are internal users of the Doris.
// They are used to set the ldap admin password.
// Cannot use external authentication.
if (qualifiedUser.equals(Auth.ROOT_USER) || qualifiedUser.equals(Auth.ADMIN_USER)) {
return MysqlAuthType.DEFAULT;
}
// precondition
switch (typeConfig) {
case LDAP:
try {
// If LDAP authentication is enabled and the user exists in LDAP, use LDAP authentication,
// otherwise use Doris internal authentication.
if (!Env.getCurrentEnv().getAuth().getLdapManager().doesUserExist(qualifiedUser)) {
return MysqlAuthType.DEFAULT;
}
} catch (Exception e) {
// TODO: can we catch exception here?
LOG.warn("Check if user exists in ldap error.", e);
MysqlProto.sendResponsePacket(context);
return MysqlAuthType.DEFAULT;
}
break;
default:
}
return typeConfig;
}
public static boolean authenticate(
ConnectContext context,
String qualifiedUser,
MysqlChannel channel,
MysqlSerializer serializer,
MysqlAuthPacket authPacket,
MysqlHandshakePacket handshakePacket) throws IOException {
MysqlAuthType authType = useWhichAuthType(context, qualifiedUser);
switch (authType) {
case DEFAULT:
return defaultAuth(context, qualifiedUser, channel, serializer, authPacket, handshakePacket);
case LDAP:
return ldapAuth(context, qualifiedUser, channel, serializer);
default:
}
return false;
}
}

View File

@ -0,0 +1,38 @@
// 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.authenticate;
import org.apache.doris.common.Config;
public enum MysqlAuthType {
DEFAULT,
LDAP;
public static MysqlAuthType getAuthTypeConfig() {
switch (Config.authentication_type.toLowerCase()) {
case "default":
return DEFAULT;
case "ldap":
return LDAP;
// add other authentication system here
// case otherAuthType:
default:
return DEFAULT;
}
}
}

View File

@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.ldap;
package org.apache.doris.mysql.authenticate.ldap;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;

View File

@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.ldap;
package org.apache.doris.mysql.authenticate.ldap;
import org.apache.doris.catalog.Env;
import org.apache.doris.common.ErrorCode;

View File

@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.ldap;
package org.apache.doris.mysql.authenticate.ldap;
import org.apache.doris.analysis.TablePattern;
import org.apache.doris.analysis.UserIdentity;
@ -25,6 +25,7 @@ import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.mysql.authenticate.MysqlAuthType;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.mysql.privilege.PrivBitSet;
import org.apache.doris.mysql.privilege.Privilege;
@ -102,7 +103,8 @@ public class LdapManager {
public boolean checkUserPasswd(String fullName, String passwd) {
String userName = ClusterNamespace.getNameFromFullName(fullName);
if (!LdapConfig.ldap_authentication_enabled || Strings.isNullOrEmpty(userName) || Objects.isNull(passwd)) {
if (MysqlAuthType.getAuthTypeConfig() != MysqlAuthType.LDAP || Strings.isNullOrEmpty(userName)
|| Objects.isNull(passwd)) {
return false;
}
LdapUserInfo ldapUserInfo = getUserInfo(fullName);
@ -135,8 +137,9 @@ public class LdapManager {
}
private boolean checkParam(String fullName) {
return LdapConfig.ldap_authentication_enabled && !Strings.isNullOrEmpty(fullName) && !fullName.equalsIgnoreCase(
Auth.ROOT_USER) && !fullName.equalsIgnoreCase(Auth.ADMIN_USER);
return MysqlAuthType.getAuthTypeConfig() == MysqlAuthType.LDAP
&& !Strings.isNullOrEmpty(fullName)
&& !fullName.equalsIgnoreCase(Auth.ROOT_USER) && !fullName.equalsIgnoreCase(Auth.ADMIN_USER);
}
private LdapUserInfo getUserInfoAndUpdateCache(String fulName) throws DdlException {
@ -207,7 +210,7 @@ public class LdapManager {
* Step3: generate default role;
*/
private Set<Role> getLdapGroupsRoles(String userName) throws DdlException {
//get user ldap group. the ldap group name should be the same as the doris role name
// get user ldap group. the ldap group name should be the same as the doris role name
List<String> ldapGroups = ldapClient.getGroups(userName);
Set<Role> roles = Sets.newHashSet();
for (String group : ldapGroups) {

View File

@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.ldap;
package org.apache.doris.mysql.authenticate.ldap;
import org.apache.doris.common.LdapConfig;
import org.apache.doris.mysql.privilege.Role;

View File

@ -47,15 +47,15 @@ import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
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.PatternMatcherException;
import org.apache.doris.common.UserException;
import org.apache.doris.common.io.Writable;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.ldap.LdapManager;
import org.apache.doris.ldap.LdapUserInfo;
import org.apache.doris.mysql.MysqlPassword;
import org.apache.doris.mysql.authenticate.MysqlAuthType;
import org.apache.doris.mysql.authenticate.ldap.LdapManager;
import org.apache.doris.mysql.authenticate.ldap.LdapUserInfo;
import org.apache.doris.persist.AlterUserOperationLog;
import org.apache.doris.persist.LdapInfo;
import org.apache.doris.persist.PrivInfo;
@ -419,7 +419,7 @@ public class Auth implements Writable {
// Check if LDAP authentication is enabled.
private boolean isLdapAuthEnabled() {
return LdapConfig.ldap_authentication_enabled;
return MysqlAuthType.getAuthTypeConfig() == MysqlAuthType.LDAP;
}
// create user