Skip to content

Commit

Permalink
[refactor](Mysql) Refactoring the process of using external component…
Browse files Browse the repository at this point in the history
…s to authenticate in MySQL connections (apache#32875)

Add a separate processing method for external permission verification, and LDAP is now just one of its many switch cases. It will be easier to add other external authentication systems in the future.
This change did not change the original execution logic of the code.
  • Loading branch information
LompleZ authored Apr 2, 2024
1 parent 9112ae0 commit e864841
Show file tree
Hide file tree
Showing 16 changed files with 286 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2513,6 +2513,11 @@ public class Config extends ConfigBase {
options = {"default", "ranger-doris"})
public static String access_controller_type = "default";

@ConfField(description = {"指定 mysql登录身份认证类型",
"Specifies the authentication type"},
options = {"default", "ldap"})
public static String authentication_type = "default";

@ConfField(mutable = true, masterOnly = false, description = {"指定 trino-connector catalog 的插件默认加载路径",
"Specify the default plugins loading path for the trino-connector catalog"})
public static String trino_connector_plugin_dir = EnvUtils.getDorisHome() + "/connectors";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@
* LDAP configuration
*/
public class LdapConfig extends ConfigBase {

/**
* Flag to enable LDAP authentication.
*/
@ConfigBase.ConfField
public static boolean ldap_authentication_enabled = false;

/**
* LDAP server ip.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
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;
Expand Down Expand Up @@ -146,7 +146,8 @@ public String getComment() {
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.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
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;

Expand Down Expand Up @@ -56,7 +56,8 @@ public UserIdentity getUserIdentity() {
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.");
}

Expand Down
117 changes: 3 additions & 114 deletions fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlProto.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,54 +17,28 @@

package org.apache.doris.mysql;

import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.cloud.catalog.CloudEnv;
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";
Expand Down Expand Up @@ -101,25 +75,10 @@ public static void sendResponsePacket(ConnectContext context) throws IOException
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:
Expand Down Expand Up @@ -235,81 +194,11 @@ public static boolean negotiate(ConnectContext context) throws IOException {
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)) {
Expand Down
Loading

0 comments on commit e864841

Please sign in to comment.