LdapManager.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.TablePattern;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.InfoSchemaDb;
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.AuthenticateType;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.mysql.privilege.PrivBitSet;
import org.apache.doris.mysql.privilege.Privilege;
import org.apache.doris.mysql.privilege.Role;

import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Encapsulates LDAP service interfaces and caches user LDAP information.
 */
public class LdapManager {
    private static final Logger LOG = LogManager.getLogger(LdapManager.class);

    public static final String LDAP_DEFAULT_ROLE = "ldapDefaultRole";

    private final LdapClient ldapClient = new LdapClient();

    private final Map<String, LdapUserInfo> ldapUserInfoCache = Maps.newHashMap();

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private void readLock() {
        lock.readLock().lock();
    }

    private void readUnlock() {
        lock.readLock().unlock();
    }

    private void writeLock() {
        lock.writeLock().lock();
    }

    private void writeUnlock() {
        lock.writeLock().unlock();
    }

    private volatile long lastTimestamp = System.currentTimeMillis();

    // If the user exists in LDAP, the LDAP information of the user is returned; otherwise, null is returned.
    public LdapUserInfo getUserInfo(String fullName) {
        if (!checkParam(fullName)) {
            return null;
        }
        LdapUserInfo ldapUserInfo = getUserInfoFromCache(fullName);
        if (ldapUserInfo != null && !ldapUserInfo.checkTimeout()) {
            return ldapUserInfo;
        }
        try {
            return getUserInfoAndUpdateCache(fullName);
        } catch (DdlException e) {
            LOG.warn("getUserInfo for {} failed", fullName, e);
            return null;
        }
    }

    public boolean doesUserExist(String fullName) {
        if (!checkParam(fullName)) {
            return false;
        }
        LdapUserInfo info = getUserInfo(fullName);
        return !Objects.isNull(info) && info.isExists();
    }

    public boolean checkUserPasswd(String fullName, String passwd) {
        String userName = ClusterNamespace.getNameFromFullName(fullName);
        if (AuthenticateType.getAuthTypeConfig() != AuthenticateType.LDAP || Strings.isNullOrEmpty(userName)
                || Objects.isNull(passwd)) {
            return false;
        }
        LdapUserInfo ldapUserInfo = getUserInfo(fullName);
        if (Objects.isNull(ldapUserInfo) || !ldapUserInfo.isExists()) {
            return false;
        }

        if (ldapUserInfo.isSetPasswd() && ldapUserInfo.getPasswd().equals(passwd)) {
            return true;
        }
        boolean isRightPasswd = ldapClient.checkPassword(userName, passwd);
        if (!isRightPasswd) {
            return false;
        }
        updatePasswd(ldapUserInfo, passwd);
        return true;
    }

    public boolean checkUserPasswd(String fullName, String passwd, String remoteIp, List<UserIdentity> currentUser) {
        if (checkUserPasswd(fullName, passwd)) {
            currentUser.addAll(Env.getCurrentEnv().getAuth().getUserIdentityForLdap(fullName, remoteIp));
            return true;
        }
        return false;
    }

    public Set<Role> getUserRoles(String fullName) {
        LdapUserInfo info = getUserInfo(fullName);
        return info == null ? Collections.emptySet() : info.getRoles();
    }

    private boolean checkParam(String fullName) {
        return AuthenticateType.getAuthTypeConfig() == AuthenticateType.LDAP
                && !Strings.isNullOrEmpty(fullName)
                && !fullName.equalsIgnoreCase(Auth.ROOT_USER) && !fullName.equalsIgnoreCase(Auth.ADMIN_USER);
    }

    private LdapUserInfo getUserInfoAndUpdateCache(String fulName) throws DdlException {
        String userName = ClusterNamespace.getNameFromFullName(fulName);
        if (Strings.isNullOrEmpty(userName)) {
            return null;
        } else if (!ldapClient.doesUserExist(userName)) {
            return makeUserNotExists(fulName);
        }
        checkTimeoutCleanCache();

        LdapUserInfo ldapUserInfo = new LdapUserInfo(fulName, false, "", getLdapGroupsRoles(userName));
        writeLock();
        try {
            ldapUserInfoCache.put(ldapUserInfo.getUserName(), ldapUserInfo);
        } finally {
            writeUnlock();
        }
        return ldapUserInfo;
    }

    private void updatePasswd(LdapUserInfo ldapUserInfo, String passwd) {
        LdapUserInfo newLdapUserInfo = ldapUserInfo.cloneWithPasswd(passwd);
        writeLock();
        try {
            ldapUserInfoCache.put(newLdapUserInfo.getUserName(), newLdapUserInfo);
        } finally {
            writeUnlock();
        }
    }

    private LdapUserInfo makeUserNotExists(String fullName) {
        writeLock();
        try {
            return ldapUserInfoCache.put(fullName, new LdapUserInfo(fullName));
        } finally {
            writeUnlock();
        }
    }

    private void checkTimeoutCleanCache() {
        long tempTimestamp = System.currentTimeMillis() - LdapConfig.ldap_cache_timeout_day * 24 * 60 * 60 * 1000;
        if (lastTimestamp < tempTimestamp) {
            writeLock();
            try {
                if (lastTimestamp < tempTimestamp) {
                    ldapUserInfoCache.clear();
                    lastTimestamp = System.currentTimeMillis();
                }
            } finally {
                writeUnlock();
            }
        }
    }

    private LdapUserInfo getUserInfoFromCache(String fullName) {
        readLock();
        try {
            return ldapUserInfoCache.get(fullName);
        } finally {
            readUnlock();
        }
    }

    /**
     * Step1: get ldap groups from ldap server;
     * Step2: get roles by ldap groups;
     * Step3: generate default role;
     */
    private Set<Role> getLdapGroupsRoles(String userName) throws DdlException {
        // get user ldap group. the ldap group name should be the same as the doris role name
        List<String> ldapGroups = ldapClient.getGroups(userName);
        Set<Role> roles = Sets.newHashSet();
        for (String group : ldapGroups) {
            String qualifiedRole = group;
            if (Env.getCurrentEnv().getAuth().doesRoleExist(qualifiedRole)) {
                roles.add(Env.getCurrentEnv().getAuth().getRoleByName(qualifiedRole));
            }
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("get user:{} ldap groups:{} and doris roles:{}", userName, ldapGroups, roles);
        }

        Role ldapGroupsPrivs = new Role(LDAP_DEFAULT_ROLE);
        grantDefaultPrivToTempUser(ldapGroupsPrivs);
        roles.add(ldapGroupsPrivs);
        return roles;
    }

    public void refresh(boolean isAll, String fullName) {
        writeLock();
        try {
            if (isAll) {
                ldapUserInfoCache.clear();
                lastTimestamp = System.currentTimeMillis();
                LOG.info("refreshed all ldap info.");
            } else {
                ldapUserInfoCache.remove(fullName);
                LOG.info("refreshed ldap info for " + fullName);
            }
        } finally {
            writeUnlock();
        }
    }

    // Temporary user has information_schema 'Select_priv' priv by default.
    private static void grantDefaultPrivToTempUser(Role role) throws DdlException {
        TablePattern tblPattern = new TablePattern(InfoSchemaDb.DATABASE_NAME, "*");
        try {
            tblPattern.analyze();
        } catch (AnalysisException e) {
            LOG.warn("should not happen.", e);
        }
        Role newRole = new Role(role.getRoleName(), tblPattern, PrivBitSet.of(Privilege.SELECT_PRIV));
        role.merge(newRole);
    }
}