LdapAuthenticator.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.ldap;

import org.apache.doris.analysis.UserIdentity;
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.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;

/**
 * This class is used for LDAP authentication login and LDAP group authorization.
 * 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 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;
        }
        // Fixme Note: LdapManager should be managed internally within the Ldap plugin
        // and not be placed inside the Env class. This ensures that Ldap-related
        // logic and dependencies are encapsulated within the plugin, promoting
        // better modularity and maintainability.
        return Env.getCurrentEnv().getAuth().getLdapManager().doesUserExist(qualifiedUser);
    }

    /**
     * The LDAP authentication process is as follows:
     * step1: Check the LDAP password.
     * step2: Get the LDAP groups privileges as a role, saved into ConnectContext.
     * step3: Set current userIdentity. If the user account does not exist in Doris, login as a temporary user.
     * Otherwise, login to the Doris account.
     */
    private AuthenticateResponse internalAuthenticate(String password, String qualifiedUser, String remoteIp) {
        String usePasswd = (Strings.isNullOrEmpty(password)) ? "NO" : "YES";
        String userName = ClusterNamespace.getNameFromFullName(qualifiedUser);
        if (LOG.isDebugEnabled()) {
            LOG.debug("user:{}", userName);
        }

        // check user password by ldap server.
        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, remoteIp, usePasswd);
                return AuthenticateResponse.failedResponse;
            }
        } catch (Exception e) {
            LOG.error("Check ldap password error.", e);
            return AuthenticateResponse.failedResponse;
        }

        UserIdentity tempUserIdentity = UserIdentity.createAnalyzedUserIdentWithIp(qualifiedUser, remoteIp);
        // Search the user in doris.
        List<UserIdentity> userIdentities = Env.getCurrentEnv().getAuth()
                .getUserIdentityForLdap(qualifiedUser, remoteIp);
        AuthenticateResponse response = new AuthenticateResponse(true);
        if (userIdentities.isEmpty()) {
            response.setUserIdentity(tempUserIdentity);
            if (LOG.isDebugEnabled()) {
                LOG.debug("User:{} does not exists in doris, login as temporary users.", userName);
            }
            response.setTemp(true);
        } else {
            response.setUserIdentity(userIdentities.get(0));
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("ldap authentication success: identity:{}", response.getUserIdentity());
        }
        return response;
    }

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