[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

View File

@ -20,12 +20,13 @@ package org.apache.doris.mysql;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Database;
import org.apache.doris.catalog.Env;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.AuthenticationException;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.mysql.authenticate.ldap.LdapAuthenticate;
import org.apache.doris.mysql.authenticate.AuthenticateRequest;
import org.apache.doris.mysql.authenticate.AuthenticatorManager;
import org.apache.doris.mysql.authenticate.ldap.LdapAuthenticator;
import org.apache.doris.mysql.authenticate.ldap.LdapManager;
import org.apache.doris.mysql.privilege.AccessControllerManager;
import org.apache.doris.mysql.privilege.Auth;
@ -65,14 +66,16 @@ public class MysqlProtoTest {
@Mocked
private LdapManager ldapManager;
@Mocked
private LdapAuthenticate ldapAuthenticate;
@Mocked
private MysqlClearTextPacket clearTextPacket;
@Mocked
StreamConnection streamConnection;
private StreamConnection streamConnection;
@Mocked
private LdapAuthenticator ldapAuthenticator;
@Mocked
private AuthenticatorManager authenticatorManager;
@Before
public void setUp() throws DdlException, AuthenticationException {
public void setUp() throws DdlException, AuthenticationException, IOException {
// mock auth
new Expectations() {
@ -95,6 +98,15 @@ public class MysqlProtoTest {
minTimes = 0;
result = catalog;
env.getAuthenticatorManager();
minTimes = 0;
result = authenticatorManager;
authenticatorManager.authenticate((ConnectContext) any, anyString, (MysqlChannel) any,
(MysqlSerializer) any, (MysqlAuthPacket) any, (MysqlHandshakePacket) any);
minTimes = 0;
result = true;
catalog.getDbNullable(anyString);
minTimes = 0;
result = new Database();
@ -215,19 +227,14 @@ public class MysqlProtoTest {
private void mockAccess() throws Exception {
}
private void mockLdap(String user, boolean userExist) {
private void mockLdap(boolean userExist) throws IOException {
Config.authentication_type = "ldap";
new Expectations() {
{
LdapAuthenticate.authenticate((ConnectContext) any, anyString, anyString);
ldapAuthenticator.authenticate((AuthenticateRequest) any);
minTimes = 0;
result = new Delegate() {
boolean fakeLdapAuthenticate(ConnectContext context, String password, String qualifiedUser) {
return password.equals(PASSWORD_CLEAR_TEXT)
&& ClusterNamespace.getNameFromFullName(qualifiedUser).equals(user);
}
};
result = true;
ldapManager.checkUserPasswd(anyString, anyString);
minTimes = 0;
@ -285,7 +292,7 @@ public class MysqlProtoTest {
mockPassword(true);
mockAccess();
mockMysqlClearTextPacket(PASSWORD_CLEAR_TEXT);
mockLdap("user", true);
mockLdap(true);
ConnectContext context = new ConnectContext(streamConnection);
context.setEnv(env);
context.setThreadLocalInfo();
@ -293,26 +300,12 @@ public class MysqlProtoTest {
Config.authentication_type = "default";
}
@Test
public void testNegotiateLdapInvalidPasswd() throws Exception {
mockChannel("user", true);
mockPassword(true);
mockAccess();
mockMysqlClearTextPacket("654321");
mockLdap("user", true);
ConnectContext context = new ConnectContext(streamConnection);
context.setEnv(env);
context.setThreadLocalInfo();
Assert.assertFalse(MysqlProto.negotiate(context));
Config.authentication_type = "default";
}
@Test
public void testNegotiateLdapRoot() throws Exception {
mockChannel("root", true);
mockPassword(true);
mockAccess();
mockLdap("root", false);
mockLdap(false);
mockMysqlClearTextPacket("654321");
ConnectContext context = new ConnectContext(streamConnection);
context.setEnv(env);

View File

@ -0,0 +1,100 @@
// 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.common.AuthenticationException;
import org.apache.doris.common.DdlException;
import org.apache.doris.mysql.authenticate.password.NativePassword;
import org.apache.doris.mysql.authenticate.password.NativePasswordResolver;
import org.apache.doris.mysql.privilege.Auth;
import mockit.Delegate;
import mockit.Expectations;
import mockit.Mocked;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
public class DefaultAuthenticatorTest {
private static final String USER_NAME = "user";
private static final String IP = "192.168.1.1";
@Mocked
private Auth auth;
private DefaultAuthenticator defaultAuthenticator = new DefaultAuthenticator();
private AuthenticateRequest request = new AuthenticateRequest(USER_NAME,
new NativePassword(new byte[2], new byte[2]), IP);
@Before
public void setUp() throws DdlException, AuthenticationException, IOException {
// mock auth
new Expectations() {
{
auth.checkPassword(anyString, anyString, (byte[]) any, (byte[]) any, (List<UserIdentity>) any);
minTimes = 0;
result = new Delegate() {
void fakeCheckPassword(String remoteUser, String remoteHost, byte[] remotePasswd,
byte[] randomString, List<UserIdentity> currentUser) {
UserIdentity userIdentity = new UserIdentity(USER_NAME, IP);
currentUser.add(userIdentity);
}
};
}
};
}
@Test
public void testAuthenticate() throws IOException {
AuthenticateResponse response = defaultAuthenticator.authenticate(request);
Assert.assertTrue(response.isSuccess());
Assert.assertFalse(response.isTemp());
Assert.assertEquals("'user'@'192.168.1.1'", response.getUserIdentity().toString());
}
@Test
public void testAuthenticateFailed() throws IOException, AuthenticationException {
new Expectations() {
{
auth.checkPassword(anyString, anyString, (byte[]) any, (byte[]) any, (List<UserIdentity>) any);
minTimes = 0;
result = new AuthenticationException("exception");
}
};
AuthenticateResponse response = defaultAuthenticator.authenticate(request);
Assert.assertFalse(response.isSuccess());
}
@Test
public void testCanDeal() {
Assert.assertTrue(defaultAuthenticator.canDeal("ss"));
}
@Test
public void testGetPasswordResolver() {
Assert.assertTrue(defaultAuthenticator.getPasswordResolver() instanceof NativePasswordResolver);
}
}

View File

@ -1,203 +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.ldap;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.common.DdlException;
import org.apache.doris.mysql.privilege.AccessControllerManager;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.mysql.privilege.Role;
import org.apache.doris.qe.ConnectContext;
import com.google.common.collect.Sets;
import mockit.Delegate;
import mockit.Expectations;
import mockit.Mocked;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
public class LdapAuthenticateTest {
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 Role ldapGroupsPrivs;
@Mocked
private LdapManager ldapManager;
@Mocked
private Env env;
@Mocked
private Auth auth;
@Mocked
private AccessControllerManager accessManager;
@Before
public void setUp() throws DdlException {
new Expectations() {
{
auth.doesRoleExist(anyString);
minTimes = 0;
result = true;
auth.mergeRolesNoCheckName((List<String>) any, (Role) any);
minTimes = 0;
result = new Delegate() {
void fakeMergeRolesNoCheckName(List<String> roles, Role savedRole) {
ldapGroupsPrivs = savedRole;
}
};
env.getAccessManager();
minTimes = 0;
result = accessManager;
env.getAuth();
minTimes = 0;
result = auth;
Env.getCurrentEnv();
minTimes = 0;
result = env;
}
};
}
private void setCheckPassword(boolean res) {
new Expectations() {
{
ldapManager.checkUserPasswd(anyString, anyString);
minTimes = 0;
result = res;
}
};
}
private void setCheckPasswordException() {
new Expectations() {
{
ldapManager.checkUserPasswd(anyString, anyString);
minTimes = 0;
result = new RuntimeException("exception");
}
};
}
private void setGetUserInfo(boolean res) {
new Expectations() {
{
if (res) {
ldapManager.getUserInfo(anyString);
minTimes = 0;
result = new Delegate() {
LdapUserInfo fakeGetGroups(String user) {
return new LdapUserInfo(anyString, false, "", Sets.newHashSet(new Role(anyString)));
}
};
} else {
ldapManager.getUserInfo(anyString);
minTimes = 0;
result = null;
}
}
};
}
private void setGetCurrentUserIdentity(boolean res) {
new Expectations() {
{
if (res) {
auth.getCurrentUserIdentity((UserIdentity) any);
minTimes = 0;
result = new UserIdentity(USER_NAME, IP);
} else {
auth.getCurrentUserIdentity((UserIdentity) any);
minTimes = 0;
result = null;
}
}
};
}
private ConnectContext getContext() {
ConnectContext context = new ConnectContext();
context.setEnv(env);
context.setThreadLocalInfo();
return context;
}
@Test
public void testAuthenticate() {
ConnectContext context = getContext();
setCheckPassword(true);
setGetUserInfo(true);
setGetCurrentUserIdentity(true);
String qualifiedUser = USER_NAME;
Assert.assertTrue(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertTrue(context.getIsTempUser());
}
@Test
public void testAuthenticateWithWrongPassword() {
ConnectContext context = getContext();
setCheckPassword(false);
setGetUserInfo(true);
setGetCurrentUserIdentity(true);
String qualifiedUser = USER_NAME;
Assert.assertFalse(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertFalse(context.getIsTempUser());
}
@Test
public void testAuthenticateWithCheckPasswordException() {
ConnectContext context = getContext();
setCheckPasswordException();
setGetUserInfo(true);
setGetCurrentUserIdentity(true);
String qualifiedUser = USER_NAME;
Assert.assertFalse(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertFalse(context.getIsTempUser());
}
@Test
public void testAuthenticateGetGroupsNull() {
ConnectContext context = getContext();
setCheckPassword(true);
setGetUserInfo(false);
setGetCurrentUserIdentity(true);
String qualifiedUser = USER_NAME;
Assert.assertTrue(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertTrue(context.getIsTempUser());
}
@Test
public void testAuthenticateUserNotExistInDoris() {
ConnectContext context = getContext();
setCheckPassword(true);
setGetUserInfo(true);
setGetCurrentUserIdentity(false);
String qualifiedUser = USER_NAME;
Assert.assertTrue(LdapAuthenticate.authenticate(context, "123", qualifiedUser));
Assert.assertTrue(context.getIsTempUser());
}
}

View File

@ -0,0 +1,146 @@
// 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.ldap;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.mysql.authenticate.AuthenticateRequest;
import org.apache.doris.mysql.authenticate.AuthenticateResponse;
import org.apache.doris.mysql.authenticate.password.ClearPassword;
import org.apache.doris.mysql.authenticate.password.ClearPasswordResolver;
import org.apache.doris.mysql.privilege.Auth;
import com.google.common.collect.Lists;
import mockit.Expectations;
import mockit.Mocked;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
public class LdapAuthenticatorTest {
private static final String USER_NAME = "user";
private static final String IP = "192.168.1.1";
@Mocked
private LdapManager ldapManager;
@Mocked
private Auth auth;
private LdapAuthenticator ldapAuthenticator = new LdapAuthenticator();
private AuthenticateRequest request = new AuthenticateRequest(USER_NAME, new ClearPassword("123"), IP);
private void setCheckPassword(boolean res) {
new Expectations() {
{
ldapManager.checkUserPasswd(anyString, anyString);
minTimes = 0;
result = res;
}
};
}
private void setCheckPasswordException() {
new Expectations() {
{
ldapManager.checkUserPasswd(anyString, anyString);
minTimes = 0;
result = new RuntimeException("exception");
}
};
}
private void setGetUserInDoris(boolean res) {
new Expectations() {
{
if (res) {
List<UserIdentity> list = Lists.newArrayList(new UserIdentity(USER_NAME, IP));
auth.getUserIdentityForLdap(anyString, anyString);
minTimes = 0;
result = list;
} else {
auth.getCurrentUserIdentity((UserIdentity) any);
minTimes = 0;
result = null;
}
}
};
}
private void setLdapUserExist(boolean res) {
new Expectations() {
{
ldapManager.doesUserExist(anyString);
minTimes = 0;
result = res;
}
};
}
@Test
public void testAuthenticate() throws IOException {
setCheckPassword(true);
setGetUserInDoris(true);
AuthenticateResponse response = ldapAuthenticator.authenticate(request);
Assert.assertTrue(response.isSuccess());
Assert.assertFalse(response.isTemp());
Assert.assertEquals("'user'@'192.168.1.1'", response.getUserIdentity().toString());
}
@Test
public void testAuthenticateWithWrongPassword() throws IOException {
setCheckPassword(false);
setGetUserInDoris(true);
AuthenticateResponse response = ldapAuthenticator.authenticate(request);
Assert.assertFalse(response.isSuccess());
}
@Test
public void testAuthenticateWithCheckPasswordException() throws IOException {
setCheckPasswordException();
setGetUserInDoris(true);
AuthenticateResponse response = ldapAuthenticator.authenticate(request);
Assert.assertFalse(response.isSuccess());
}
@Test
public void testAuthenticateUserNotExistInDoris() throws IOException {
setCheckPassword(true);
setGetUserInDoris(false);
AuthenticateResponse response = ldapAuthenticator.authenticate(request);
Assert.assertTrue(response.isSuccess());
Assert.assertTrue(response.isTemp());
Assert.assertEquals("'user'@'192.168.1.1'", response.getUserIdentity().toString());
}
@Test
public void testCanDeal() {
setLdapUserExist(true);
Assert.assertFalse(ldapAuthenticator.canDeal(Auth.ROOT_USER));
Assert.assertFalse(ldapAuthenticator.canDeal(Auth.ADMIN_USER));
Assert.assertTrue(ldapAuthenticator.canDeal("ss"));
setLdapUserExist(false);
Assert.assertFalse(ldapAuthenticator.canDeal("ss"));
}
@Test
public void testGetPasswordResolver() {
Assert.assertTrue(ldapAuthenticator.getPasswordResolver() instanceof ClearPasswordResolver);
}
}