[enhance](auth)Abstract authentication interface (#33668) (#33961)

bp #33668

Co-authored-by: zhangdong <493738387@qq.com>
This commit is contained in:
Mingyu Chen
2024-04-22 14:47:59 +08:00
committed by yiguolei
parent 88b3d61eca
commit f6b6c13fb3
24 changed files with 913 additions and 468 deletions

View File

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

View File

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

View File

@ -183,6 +183,8 @@ import org.apache.doris.mtmv.MTMVRefreshPartitionSnapshot;
import org.apache.doris.mtmv.MTMVRelation;
import org.apache.doris.mtmv.MTMVService;
import org.apache.doris.mtmv.MTMVStatus;
import org.apache.doris.mysql.authenticate.AuthenticateType;
import org.apache.doris.mysql.authenticate.AuthenticatorManager;
import org.apache.doris.mysql.privilege.AccessControllerManager;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.mysql.privilege.PrivPredicate;
@ -454,6 +456,8 @@ public class Env {
private Auth auth;
private AccessControllerManager accessManager;
private AuthenticatorManager authenticatorManager;
private DomainResolver domainResolver;
private TabletSchedulerStat stat;
@ -701,6 +705,7 @@ public class Env {
this.auth = new Auth();
this.accessManager = new AccessControllerManager(auth);
this.authenticatorManager = new AuthenticatorManager(AuthenticateType.getAuthTypeConfig());
this.domainResolver = new DomainResolver(auth);
this.metaContext = new MetaContext();
@ -825,6 +830,10 @@ public class Env {
return accessManager;
}
public AuthenticatorManager getAuthenticatorManager() {
return authenticatorManager;
}
public MTMVService getMtmvService() {
return mtmvService;
}

View File

@ -23,7 +23,6 @@ import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.datasource.CatalogIf;
import org.apache.doris.mysql.authenticate.MysqlAuth;
import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Strings;
@ -194,7 +193,8 @@ public class MysqlProto {
}
// authenticate
if (!MysqlAuth.authenticate(context, qualifiedUser, channel, serializer, authPacket, handshakePacket)) {
if (!Env.getCurrentEnv().getAuthenticatorManager()
.authenticate(context, qualifiedUser, channel, serializer, authPacket, handshakePacket)) {
return false;
}

View File

@ -0,0 +1,44 @@
// 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.mysql.authenticate.password.Password;
public class AuthenticateRequest {
private String userName;
private Password password;
private String remoteIp;
public AuthenticateRequest(String userName, Password password, String remoteIp) {
this.userName = userName;
this.password = password;
this.remoteIp = remoteIp;
}
public String getUserName() {
return userName;
}
public Password getPassword() {
return password;
}
public String getRemoteIp() {
return remoteIp;
}
}

View File

@ -0,0 +1,76 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.mysql.authenticate;
import org.apache.doris.analysis.UserIdentity;
public class AuthenticateResponse {
public static AuthenticateResponse failedResponse = new AuthenticateResponse(false);
private boolean success;
private UserIdentity userIdentity;
private boolean isTemp = false;
public AuthenticateResponse(boolean success) {
this.success = success;
}
public AuthenticateResponse(boolean success, UserIdentity userIdentity) {
this.success = success;
this.userIdentity = userIdentity;
}
public AuthenticateResponse(boolean success, UserIdentity userIdentity, boolean isTemp) {
this.success = success;
this.userIdentity = userIdentity;
this.isTemp = isTemp;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public UserIdentity getUserIdentity() {
return userIdentity;
}
public void setUserIdentity(UserIdentity userIdentity) {
this.userIdentity = userIdentity;
}
public boolean isTemp() {
return isTemp;
}
public void setTemp(boolean temp) {
isTemp = temp;
}
@Override
public String toString() {
return "AuthenticateResponse{"
+ "success=" + success
+ ", userIdentity=" + userIdentity
+ ", isTemp=" + isTemp
+ '}';
}
}

View File

@ -19,11 +19,11 @@ package org.apache.doris.mysql.authenticate;
import org.apache.doris.common.Config;
public enum MysqlAuthType {
public enum AuthenticateType {
DEFAULT,
LDAP;
public static MysqlAuthType getAuthTypeConfig() {
public static AuthenticateType getAuthTypeConfig() {
switch (Config.authentication_type.toLowerCase()) {
case "default":
return DEFAULT;

View File

@ -0,0 +1,30 @@
// 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.mysql.authenticate.password.PasswordResolver;
import java.io.IOException;
public interface Authenticator {
AuthenticateResponse authenticate(AuthenticateRequest request) throws IOException;
boolean canDeal(String qualifiedUser);
PasswordResolver getPasswordResolver();
}

View File

@ -0,0 +1,83 @@
// 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.mysql.MysqlAuthPacket;
import org.apache.doris.mysql.MysqlChannel;
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.LdapAuthenticator;
import org.apache.doris.mysql.authenticate.password.Password;
import org.apache.doris.qe.ConnectContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.Optional;
public class AuthenticatorManager {
private static final Logger LOG = LogManager.getLogger(AuthenticatorManager.class);
private Authenticator defaultAuthenticator;
private Authenticator authTypeAuthenticator;
public AuthenticatorManager(AuthenticateType type) {
LOG.info("authenticate type: {}", type);
this.defaultAuthenticator = new DefaultAuthenticator();
switch (type) {
case LDAP:
this.authTypeAuthenticator = new LdapAuthenticator();
break;
case DEFAULT:
default:
this.authTypeAuthenticator = defaultAuthenticator;
break;
}
}
public boolean authenticate(ConnectContext context,
String userName,
MysqlChannel channel,
MysqlSerializer serializer,
MysqlAuthPacket authPacket,
MysqlHandshakePacket handshakePacket) throws IOException {
Authenticator authenticator = chooseAuthenticator(userName);
Optional<Password> password = authenticator.getPasswordResolver()
.resolvePassword(context, channel, serializer, authPacket, handshakePacket);
if (!password.isPresent()) {
return false;
}
String remoteIp = context.getMysqlChannel().getRemoteIp();
AuthenticateRequest request = new AuthenticateRequest(userName, password.get(), remoteIp);
AuthenticateResponse response = authenticator.authenticate(request);
if (!response.isSuccess()) {
MysqlProto.sendResponsePacket(context);
return false;
}
context.setCurrentUserIdentity(response.getUserIdentity());
context.setRemoteIP(remoteIp);
context.setIsTempUser(response.isTemp());
return true;
}
private Authenticator chooseAuthenticator(String userName) {
return authTypeAuthenticator.canDeal(userName) ? authTypeAuthenticator : defaultAuthenticator;
}
}

View File

@ -0,0 +1,75 @@
// 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.ErrorReport;
import org.apache.doris.mysql.authenticate.password.NativePassword;
import org.apache.doris.mysql.authenticate.password.NativePasswordResolver;
import org.apache.doris.mysql.authenticate.password.Password;
import org.apache.doris.mysql.authenticate.password.PasswordResolver;
import com.google.common.collect.Lists;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.List;
public class DefaultAuthenticator implements Authenticator {
private static final Logger LOG = LogManager.getLogger(DefaultAuthenticator.class);
private PasswordResolver passwordResolver;
public DefaultAuthenticator() {
this.passwordResolver = new NativePasswordResolver();
}
@Override
public AuthenticateResponse authenticate(AuthenticateRequest request) throws IOException {
if (LOG.isDebugEnabled()) {
LOG.debug("user:{} start to default authenticate.", request.getUserName());
}
Password password = request.getPassword();
if (!(password instanceof NativePassword)) {
return AuthenticateResponse.failedResponse;
}
NativePassword nativePassword = (NativePassword) password;
List<UserIdentity> currentUserIdentity = Lists.newArrayList();
try {
Env.getCurrentEnv().getAuth().checkPassword(request.getUserName(), request.getRemoteIp(),
nativePassword.getRemotePasswd(), nativePassword.getRandomString(), currentUserIdentity);
} catch (AuthenticationException e) {
ErrorReport.report(e.errorCode, e.msgs);
return AuthenticateResponse.failedResponse;
}
return new AuthenticateResponse(true, currentUserIdentity.get(0));
}
@Override
public boolean canDeal(String qualifiedUser) {
return true;
}
@Override
public PasswordResolver getPasswordResolver() {
return passwordResolver;
}
}

View File

@ -1,205 +0,0 @@
// 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

@ -22,12 +22,20 @@ import org.apache.doris.catalog.Env;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.mysql.authenticate.AuthenticateRequest;
import org.apache.doris.mysql.authenticate.AuthenticateResponse;
import org.apache.doris.mysql.authenticate.Authenticator;
import org.apache.doris.mysql.authenticate.password.ClearPassword;
import org.apache.doris.mysql.authenticate.password.ClearPasswordResolver;
import org.apache.doris.mysql.authenticate.password.Password;
import org.apache.doris.mysql.authenticate.password.PasswordResolver;
import org.apache.doris.mysql.privilege.Auth;
import com.google.common.base.Strings;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.List;
/**
@ -35,8 +43,43 @@ import java.util.List;
* 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);
public class LdapAuthenticator implements Authenticator {
private static final Logger LOG = LogManager.getLogger(LdapAuthenticator.class);
private PasswordResolver passwordResolver;
public LdapAuthenticator() {
this.passwordResolver = new ClearPasswordResolver();
}
/*
* ldap:
* server ---AuthSwitch---> client
* server <--- clear text password --- client
*/
@Override
public AuthenticateResponse authenticate(AuthenticateRequest request) throws IOException {
if (LOG.isDebugEnabled()) {
LOG.debug("user:{} start to ldap authenticate.", request.getUserName());
}
Password password = request.getPassword();
if (!(password instanceof ClearPassword)) {
return AuthenticateResponse.failedResponse;
}
ClearPassword clearPassword = (ClearPassword) password;
return internalAuthenticate(clearPassword.getPassword(), request.getUserName(), request.getRemoteIp());
}
@Override
public boolean canDeal(String qualifiedUser) {
if (qualifiedUser.equals(Auth.ROOT_USER) || qualifiedUser.equals(Auth.ADMIN_USER)) {
return false;
}
if (!Env.getCurrentEnv().getAuth().getLdapManager().doesUserExist(qualifiedUser)) {
return false;
}
return true;
}
/**
* The LDAP authentication process is as follows:
@ -45,7 +88,7 @@ public class LdapAuthenticate {
* 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) {
private AuthenticateResponse internalAuthenticate(String password, String qualifiedUser, String remoteIp) {
String usePasswd = (Strings.isNullOrEmpty(password)) ? "NO" : "YES";
String userName = ClusterNamespace.getNameFromFullName(qualifiedUser);
if (LOG.isDebugEnabled()) {
@ -56,35 +99,36 @@ public class LdapAuthenticate {
try {
if (!Env.getCurrentEnv().getAuth().getLdapManager().checkUserPasswd(qualifiedUser, password)) {
LOG.info("user:{} use check LDAP password failed.", userName);
ErrorReport.report(ErrorCode.ERR_ACCESS_DENIED_ERROR, qualifiedUser, context.getRemoteIP(), usePasswd);
return false;
ErrorReport.report(ErrorCode.ERR_ACCESS_DENIED_ERROR, qualifiedUser, remoteIp, usePasswd);
return AuthenticateResponse.failedResponse;
}
} catch (Exception e) {
LOG.error("Check ldap password error.", e);
return false;
return AuthenticateResponse.failedResponse;
}
String remoteIp = context.getMysqlChannel().getRemoteIp();
UserIdentity tempUserIdentity = UserIdentity.createAnalyzedUserIdentWithIp(qualifiedUser, remoteIp);
// Search the user in doris.
List<UserIdentity> userIdentities = Env.getCurrentEnv().getAuth()
.getUserIdentityForLdap(qualifiedUser, remoteIp);
UserIdentity userIdentity;
AuthenticateResponse response = new AuthenticateResponse(true);
if (userIdentities.isEmpty()) {
userIdentity = tempUserIdentity;
response.setUserIdentity(tempUserIdentity);
if (LOG.isDebugEnabled()) {
LOG.debug("User:{} does not exists in doris, login as temporary users.", userName);
}
context.setIsTempUser(true);
response.setTemp(true);
} else {
userIdentity = userIdentities.get(0);
response.setUserIdentity(userIdentities.get(0));
}
context.setCurrentUserIdentity(userIdentity);
context.setRemoteIP(remoteIp);
if (LOG.isDebugEnabled()) {
LOG.debug("ldap authentication success: identity:{}", context.getCurrentUserIdentity());
LOG.debug("ldap authentication success: identity:{}", response.getUserIdentity());
}
return true;
return response;
}
@Override
public PasswordResolver getPasswordResolver() {
return passwordResolver;
}
}

View File

@ -25,7 +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.authenticate.AuthenticateType;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.mysql.privilege.PrivBitSet;
import org.apache.doris.mysql.privilege.Privilege;
@ -103,7 +103,7 @@ public class LdapManager {
public boolean checkUserPasswd(String fullName, String passwd) {
String userName = ClusterNamespace.getNameFromFullName(fullName);
if (MysqlAuthType.getAuthTypeConfig() != MysqlAuthType.LDAP || Strings.isNullOrEmpty(userName)
if (AuthenticateType.getAuthTypeConfig() != AuthenticateType.LDAP || Strings.isNullOrEmpty(userName)
|| Objects.isNull(passwd)) {
return false;
}
@ -137,7 +137,7 @@ public class LdapManager {
}
private boolean checkParam(String fullName) {
return MysqlAuthType.getAuthTypeConfig() == MysqlAuthType.LDAP
return AuthenticateType.getAuthTypeConfig() == AuthenticateType.LDAP
&& !Strings.isNullOrEmpty(fullName)
&& !fullName.equalsIgnoreCase(Auth.ROOT_USER) && !fullName.equalsIgnoreCase(Auth.ADMIN_USER);
}

View File

@ -0,0 +1,30 @@
// 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.password;
public class ClearPassword extends Password {
private String password;
public ClearPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
}

View File

@ -0,0 +1,60 @@
// 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.password;
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.qe.ConnectContext;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Optional;
public class ClearPasswordResolver implements PasswordResolver {
@Override
public Optional<Password> resolvePassword(ConnectContext context, MysqlChannel channel, MysqlSerializer serializer,
MysqlAuthPacket authPacket,
MysqlHandshakePacket handshakePacket) throws IOException {
// 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 Optional.empty();
}
MysqlClearTextPacket clearTextPacket = new MysqlClearTextPacket();
if (!clearTextPacket.readFrom(authSwitchResponse)) {
ErrorReport.report(ErrorCode.ERR_NOT_SUPPORTED_AUTH_MODE);
MysqlProto.sendResponsePacket(context);
return Optional.empty();
}
return Optional.of(new ClearPassword(clearTextPacket.getPassword()));
}
}

View File

@ -0,0 +1,36 @@
// 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.password;
public class NativePassword extends Password {
private byte[] remotePasswd;
private byte[] randomString;
public NativePassword(byte[] remotePasswd, byte[] randomString) {
this.remotePasswd = remotePasswd;
this.randomString = randomString;
}
public byte[] getRemotePasswd() {
return remotePasswd;
}
public byte[] getRandomString() {
return randomString;
}
}

View File

@ -0,0 +1,71 @@
// 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.password;
import org.apache.doris.common.Config;
import org.apache.doris.mysql.MysqlAuthPacket;
import org.apache.doris.mysql.MysqlChannel;
import org.apache.doris.mysql.MysqlHandshakePacket;
import org.apache.doris.mysql.MysqlProto;
import org.apache.doris.mysql.MysqlSerializer;
import org.apache.doris.qe.ConnectContext;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Optional;
public class NativePasswordResolver implements PasswordResolver {
@Override
public Optional<Password> resolvePassword(ConnectContext context, 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 Optional.empty();
}
// 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();
}
return Optional.of(new NativePassword(authPacket.getAuthResponse(), randomString));
}
}

View File

@ -0,0 +1,22 @@
// 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.password;
public abstract class Password {
}

View File

@ -0,0 +1,34 @@
// 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.password;
import org.apache.doris.mysql.MysqlAuthPacket;
import org.apache.doris.mysql.MysqlChannel;
import org.apache.doris.mysql.MysqlHandshakePacket;
import org.apache.doris.mysql.MysqlSerializer;
import org.apache.doris.qe.ConnectContext;
import java.io.IOException;
import java.util.Optional;
public interface PasswordResolver {
Optional<Password> resolvePassword(ConnectContext context, MysqlChannel channel,
MysqlSerializer serializer,
MysqlAuthPacket authPacket,
MysqlHandshakePacket handshakePacket) throws IOException;
}

View File

@ -53,7 +53,7 @@ import org.apache.doris.common.UserException;
import org.apache.doris.common.io.Writable;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.mysql.MysqlPassword;
import org.apache.doris.mysql.authenticate.MysqlAuthType;
import org.apache.doris.mysql.authenticate.AuthenticateType;
import org.apache.doris.mysql.authenticate.ldap.LdapManager;
import org.apache.doris.mysql.authenticate.ldap.LdapUserInfo;
import org.apache.doris.persist.AlterUserOperationLog;
@ -419,7 +419,7 @@ public class Auth implements Writable {
// Check if LDAP authentication is enabled.
private boolean isLdapAuthEnabled() {
return MysqlAuthType.getAuthTypeConfig() == MysqlAuthType.LDAP;
return AuthenticateType.getAuthTypeConfig() == AuthenticateType.LDAP;
}
// create user