AuthenticationPluginAuthenticator.java

// 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.plugin;

import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.authentication.AuthenticationException;
import org.apache.doris.authentication.AuthenticationFailureType;
import org.apache.doris.authentication.AuthenticationIntegration;
import org.apache.doris.authentication.AuthenticationRequest;
import org.apache.doris.authentication.AuthenticationResult;
import org.apache.doris.authentication.CredentialType;
import org.apache.doris.authentication.handler.AuthenticationPluginManager;
import org.apache.doris.authentication.spi.AuthenticationPlugin;
import org.apache.doris.catalog.Env;
import org.apache.doris.common.Config;
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.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 org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.plugin.PropertiesUtils;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;

/**
 * Bridge authenticator that adapts an {@link org.apache.doris.authentication.spi.AuthenticationPluginFactory}
 * to the legacy MySQL {@link Authenticator} contract.
 */
public class AuthenticationPluginAuthenticator implements Authenticator {
    private static final Logger LOG = LogManager.getLogger(AuthenticationPluginAuthenticator.class);
    private static final String CONFIG_INTEGRATION_NAME_PREFIX = "__config_auth_type__:";
    private static final String MYSQL_RANDOM_STRING_PROPERTY = "mysql.random_string";

    private final AuthenticationIntegration integration;
    private final AuthenticationPlugin plugin;
    private final PasswordResolver passwordResolver;

    public AuthenticationPluginAuthenticator(String pluginType, Properties initProps) throws AuthenticationException {
        this(pluginType, PropertiesUtils.propertiesToMap(initProps), new AuthenticationPluginManager());
    }

    AuthenticationPluginAuthenticator(String pluginType, Map<String, String> initProps,
            AuthenticationPluginManager pluginManager) throws AuthenticationException {
        Objects.requireNonNull(pluginType, "pluginType");
        AuthenticationPluginManager resolvedPluginManager = Objects.requireNonNull(pluginManager, "pluginManager");
        ensurePluginFactoryLoaded(resolvedPluginManager, pluginType);
        integration = AuthenticationIntegration.builder()
                .name(CONFIG_INTEGRATION_NAME_PREFIX + pluginType)
                .type(pluginType)
                .properties(initProps == null ? Collections.emptyMap() : initProps)
                .build();
        plugin = resolvedPluginManager.createPlugin(integration);
        passwordResolver = plugin.requiresClearPassword() ? new ClearPasswordResolver() : new NativePasswordResolver();
    }

    @Override
    public AuthenticateResponse authenticate(AuthenticateRequest request) throws IOException {
        AuthenticationRequest pluginRequest = toPluginRequest(request);
        if (!plugin.supports(pluginRequest)) {
            return AuthenticateResponse.failedResponse;
        }

        AuthenticationResult result;
        try {
            result = plugin.authenticate(pluginRequest, integration);
        } catch (AuthenticationException e) {
            LOG.warn("Authentication plugin '{}' failed for user '{}': {}", integration.getType(),
                    request.getUserName(), e.getMessage(), e);
            return AuthenticateResponse.failedResponse;
        }

        if (result.isContinue()) {
            LOG.warn("Authentication plugin '{}' returned CONTINUE for user '{}', which is not supported",
                    integration.getType(), request.getUserName());
            return AuthenticateResponse.failedResponse;
        }
        if (!result.isSuccess()) {
            if (result.getException() != null) {
                LOG.info("Authentication plugin '{}' rejected user '{}': {}", integration.getType(),
                        request.getUserName(), result.getException().getMessage());
            }
            return AuthenticateResponse.failedResponse;
        }

        return mapSuccessfulAuthentication(request.getUserName(), request.getRemoteIp());
    }

    @Override
    public boolean canDeal(String qualifiedUser) {
        return !Auth.ROOT_USER.equals(qualifiedUser) && !Auth.ADMIN_USER.equals(qualifiedUser);
    }

    @Override
    public PasswordResolver getPasswordResolver() {
        return passwordResolver;
    }

    private AuthenticateResponse mapSuccessfulAuthentication(String qualifiedUser, String remoteIp) {
        List<UserIdentity> userIdentities =
                Env.getCurrentEnv().getAuth().getUserIdentityForExternalAuth(qualifiedUser, remoteIp);
        if (!userIdentities.isEmpty()) {
            return new AuthenticateResponse(true, userIdentities.get(0), false);
        }
        if (!Boolean.parseBoolean(integration.getProperty("enable_jit_user", "false"))) {
            LOG.info("Authentication plugin '{}' authenticated user '{}' but JIT is disabled",
                    integration.getType(), qualifiedUser);
            return AuthenticateResponse.failedResponse;
        }
        UserIdentity tempUserIdentity = UserIdentity.createAnalyzedUserIdentWithIp(qualifiedUser, remoteIp);
        return new AuthenticateResponse(true, tempUserIdentity, true);
    }

    private AuthenticationRequest toPluginRequest(AuthenticateRequest request) {
        AuthenticationRequest.Builder builder = AuthenticationRequest.builder()
                .username(request.getUserName())
                .remoteHost(request.getRemoteIp())
                .clientType("mysql");

        Password password = request.getPassword();
        if (password instanceof ClearPassword) {
            builder.credentialType(CredentialType.CLEAR_TEXT_PASSWORD)
                    .credential(((ClearPassword) password).getPassword().getBytes(StandardCharsets.UTF_8));
        } else if (password instanceof NativePassword) {
            NativePassword nativePassword = (NativePassword) password;
            builder.credentialType(CredentialType.MYSQL_NATIVE_PASSWORD)
                    .credential(nativePassword.getRemotePasswd())
                    .property(MYSQL_RANDOM_STRING_PROPERTY, nativePassword.getRandomString());
        } else {
            throw new IllegalArgumentException("Unsupported password type: "
                    + (password == null ? "null" : password.getClass().getName()));
        }

        return builder.build();
    }

    private void ensurePluginFactoryLoaded(AuthenticationPluginManager pluginManager, String pluginType)
            throws AuthenticationException {
        if (pluginManager.hasFactory(pluginType)) {
            return;
        }
        try {
            Path pluginRoot = Paths.get(Config.authentication_plugins_dir);
            pluginManager.loadAll(Collections.singletonList(pluginRoot), getClass().getClassLoader());
        } catch (AuthenticationException e) {
            throw new AuthenticationException(
                    "Failed to load authentication plugin for type '" + pluginType + "': " + e.getMessage(),
                    e,
                    AuthenticationFailureType.MISCONFIGURED);
        }
        if (!pluginManager.hasFactory(pluginType)) {
            throw new AuthenticationException(
                    "No AuthenticationPluginFactory found for plugin: " + pluginType,
                    AuthenticationFailureType.MISCONFIGURED);
        }
    }
}