This is an automated email from the ASF dual-hosted git repository. kirs pushed a commit to branch branch-2.1-authentication-plugin in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-2.1-authentication-plugin by this push: new 428588c7ea2 Branch 2.1 authentication plugin commits (#44156) 428588c7ea2 is described below commit 428588c7ea246d34ea71c4cc60c8737eb11e338d Author: Calvin Kirs <guoqi...@selectdb.com> AuthorDate: Mon Nov 18 14:36:57 2024 +0800 Branch 2.1 authentication plugin commits (#44156) --- .../main/java/org/apache/doris/common/Config.java | 20 +++ .../main/java/org/apache/doris/catalog/Env.java | 2 +- .../hive/RangerHiveAccessControllerFactory.java | 6 + .../doris/common/util/ChildFirstClassLoader.java | 151 +++++++++++++++++++++ .../apache/doris/common/util/ClassLoaderUtils.java | 126 +++++++++++++++++ .../apache/doris/common/util/PropertyAnalyzer.java | 11 -- .../doris/mysql/authenticate/AuthenticateType.java | 18 +++ .../AuthenticatorFactory.java} | 20 ++- .../mysql/authenticate/AuthenticatorManager.java | 76 ++++++++--- .../DefaultAuthenticatorFactory.java} | 15 +- .../mysql/authenticate/ldap/LdapAuthenticator.java | 9 +- .../ldap/LdapAuthenticatorFactory.java} | 20 ++- .../mysql/privilege/AccessControllerFactory.java | 8 ++ .../mysql/privilege/AccessControllerManager.java | 100 ++++++++++---- ...ava => RangerDorisAccessControllerFactory.java} | 13 +- .../org/apache/doris/plugin/PropertiesUtils.java | 68 ++++++++++ ...e.doris.mysql.authenticate.AuthenticatorFactory | 19 +++ ...e.doris.mysql.privilege.AccessControllerFactory | 19 +++ .../privileges/CustomAccessControllerFactory.java} | 11 +- .../nereids/privileges/TestCheckPrivileges.java | 23 ++-- ...e.doris.mysql.privilege.AccessControllerFactory | 18 +++ 21 files changed, 667 insertions(+), 86 deletions(-) diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java index 59daae8f8d3..08a207eeae8 100644 --- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java +++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java @@ -2878,6 +2878,26 @@ public class Config extends ConfigBase { "设置为 true,如果查询无法选择到健康副本时,会打印出该tablet所有副本的详细信息,"}) public static boolean sql_block_rule_ignore_admin = false; + @ConfField(description = {"认证插件目录", + "Authentication plugin directory"}) + public static String authentication_plugins_dir = EnvUtils.getDorisHome() + "/plugins/authentication"; + + @ConfField(description = {"鉴权插件目录", + "Authorization plugin directory"}) + public static String authorization_plugins_dir = EnvUtils.getDorisHome() + "/plugins/authorization"; + + @ConfField(description = { + "鉴权插件配置文件路径,需在 DORIS_HOME 下,默认为 conf/authorization.conf", + "Authorization plugin configuration file path, need to be in DORIS_HOME," + + "default is conf/authorization.conf"}) + public static String authorization_config_file_path = "/conf/authorization.conf"; + + @ConfField(description = { + "认证插件配置文件路径,需在 DORIS_HOME 下,默认为 conf/authentication.conf", + "Authentication plugin configuration file path, need to be in DORIS_HOME," + + "default is conf/authentication.conf"}) + public static String authentication_config_file_path = "/conf/authentication.conf"; + @ConfField(description = {"用于测试,强制将所有的查询forward到master以验证forward query的行为", "For testing purposes, all queries are forcibly forwarded to the master to verify" + "the behavior of forwarding queries."}) diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java index 03679d64330..1d4ea604bfc 100755 --- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java +++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java @@ -729,7 +729,7 @@ public class Env { this.auth = new Auth(); this.accessManager = new AccessControllerManager(auth); - this.authenticatorManager = new AuthenticatorManager(AuthenticateType.getAuthTypeConfig()); + this.authenticatorManager = new AuthenticatorManager(AuthenticateType.getAuthTypeConfigString()); this.domainResolver = new DomainResolver(auth); this.metaContext = new MetaContext(); diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessControllerFactory.java b/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessControllerFactory.java index 545e7a26836..33e3f4a64c1 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessControllerFactory.java +++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessControllerFactory.java @@ -23,6 +23,12 @@ import org.apache.doris.mysql.privilege.CatalogAccessController; import java.util.Map; public class RangerHiveAccessControllerFactory implements AccessControllerFactory { + + @Override + public String factoryIdentifier() { + return "ranger-hive"; + } + @Override public CatalogAccessController createAccessController(Map<String, String> prop) { return new RangerCacheHiveAccessController(prop); diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/util/ChildFirstClassLoader.java b/fe/fe-core/src/main/java/org/apache/doris/common/util/ChildFirstClassLoader.java new file mode 100644 index 00000000000..ad3b0dfe77d --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/common/util/ChildFirstClassLoader.java @@ -0,0 +1,151 @@ +// 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.common.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * ChildFirstClassLoader is a custom class loader designed to load classes from + * plugin JAR files. It uses a child-first class loading strategy, where the loader + * first attempts to load classes from its own URLs (plugin JARs), and if the class + * is not found, it delegates the loading to its parent class loader. + * <p> + * This class is intended for plugin-based systems where classes defined in plugins + * might override or replace standard library classes. + * <p> + * Key features: + * - Child-First loading mechanism. + * - Support for loading classes from multiple JAR files. + * - Efficient caching of JAR file resources to avoid repeated file access. + */ +public class ChildFirstClassLoader extends URLClassLoader { + + // A list of URLs pointing to JAR files + private final List<URL> jarURLs; + + /** + * Constructs a new ChildFirstClassLoader with the given URLs and parent class loader. + * This constructor stores the URLs for class loading. + * + * @param urls The URLs pointing to the plugin JAR files. + * @param parent The parent class loader to use for delegation if class is not found. + * @throws IOException If there is an error opening the JAR files. + * @throws URISyntaxException If there is an error converting the URL to URI. + */ + public ChildFirstClassLoader(URL[] urls, ClassLoader parent) throws IOException, URISyntaxException { + super(urls, parent); + this.jarURLs = new ArrayList<>(); + for (URL url : urls) { + if ("file".equals(url.getProtocol())) { + this.jarURLs.add(url); + } + } + } + + /** + * Attempts to load the class with the specified name. + * This method first tries to find the class using the current class loader (child-first strategy), + * and if the class is not found, it delegates the loading to the parent class loader. + * + * @param name The fully qualified name of the class to be loaded. + * @param resolve If true, the class will be resolved after being loaded. + * @return The resulting Class object. + * @throws ClassNotFoundException If the class cannot be found by either the child or parent loader. + */ + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + // Child-First mechanism: try to find the class locally first + try { + return findClass(name); + } catch (ClassNotFoundException e) { + // If the class is not found locally, delegate to the parent class loader + return super.loadClass(name, resolve); + } + } + + /** + * Searches for the class in the loaded plugin JAR files. + * If the class is found in one of the JAR files, it will be defined and returned. + * + * @param name The fully qualified name of the class to find. + * @return The resulting Class object. + * @throws ClassNotFoundException If the class cannot be found in the JAR files. + */ + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + String classFile = name.replace('.', '/') + ".class"; // Convert class name to path + + // Iterate over all the JAR URLs to find the class + for (URL jarURL : jarURLs) { + try (JarFile jarFile = new JarFile(Paths.get(jarURL.toURI()).toFile())) { + JarEntry entry = jarFile.getJarEntry(classFile); + if (entry != null) { + try (InputStream inputStream = jarFile.getInputStream(entry)) { + byte[] classData = readAllBytes(inputStream); + // Define the class from the byte array + return defineClass(name, classData, 0, classData.length); + } + } + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + } + // If the class was not found in any JAR file, throw ClassNotFoundException + throw new ClassNotFoundException(name); + } + + /** + * Reads all bytes from the given InputStream. + * This method reads the entire content of the InputStream and returns it as a byte array. + * + * @param inputStream The InputStream to read from. + * @return A byte array containing the data from the InputStream. + * @throws IOException If an I/O error occurs while reading the stream. + */ + private byte[] readAllBytes(InputStream inputStream) throws IOException { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + return outputStream.toByteArray(); + } + } + + /** + * Closes all open JAR files and releases any resources held by this class loader. + * This method should be called when the class loader is no longer needed to avoid resource leaks. + * + * @throws IOException If an I/O error occurs while closing the JAR files. + */ + @Override + public void close() throws IOException { + super.close(); // Call the superclass close method + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/util/ClassLoaderUtils.java b/fe/fe-core/src/main/java/org/apache/doris/common/util/ClassLoaderUtils.java new file mode 100644 index 00000000000..c82858c7d01 --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/common/util/ClassLoaderUtils.java @@ -0,0 +1,126 @@ +// 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.common.util; + +import org.apache.doris.common.Config; +import org.apache.doris.mysql.authenticate.AuthenticatorFactory; +import org.apache.doris.mysql.privilege.AccessControllerFactory; + +import org.apache.commons.collections.map.HashedMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; + +/** + * Utility class for loading service implementations from external JAR files in specific plugin directories. + * <p> + * This class provides a mechanism to dynamically load service implementations from JAR files located in + * plugin directories, which are mapped by the service type's simple name. It uses a child-first class loading + * strategy to ensure that plugins in the JAR files are prioritized over classes loaded by the parent class loader. + * <p> + * It is particularly useful in scenarios where the system needs to support modular or pluggable architectures, + * such as dynamically loading authenticators, access controllers, or other pluggable services from external + * directories without requiring the services to be bundled with the core application. + * <p> + * Plugin directory mappings are maintained in a static map where the key is the simple name of the service class, + * and the value is the relative path to the directory containing the plugin JARs. + * <p> + * Example usage: + * <pre> + * {@code + * List<AuthenticatorFactory> authenticators = ClassLoaderUtils.loadServicesFromDirectory(AuthenticatorFactory.class); + * } + * </pre> + * + * @see ServiceLoader + * @see ChildFirstClassLoader + */ +public class ClassLoaderUtils { + private static final Logger LOG = LogManager.getLogger(ClassLoaderUtils.class); + // A mapping of service class simple names to their respective plugin directories. + private static final Map<String, String> pluginDirMapping = new HashedMap(); + + static { + pluginDirMapping.put(AuthenticatorFactory.class.getSimpleName(), Config.authentication_plugins_dir); + pluginDirMapping.put(AccessControllerFactory.class.getSimpleName(), Config.authorization_plugins_dir); + } + + /** + * Loads service implementations from JAR files in the specified plugin directory. + * <p> + * The method first looks up the directory for the given service class type from the {@code pluginDirMapping}. + * If a directory exists and contains JAR files, it will load the service implementations from those JAR files + * using a child-first class loader to prioritize the plugin classes. + * <p> + * If no directory is found for the service type, or the directory is invalid, an exception is thrown. If the + * directory does not contain any JAR files, an empty list is returned. + * + * @param serviceClass The class type of the service to load. This should be the interface or + * base class of the service. + * @param <T> The type of the service. + * @return A list of service instances loaded from JAR files. If no services are found, an empty list is returned. + * @throws IOException If there is an error reading the JAR files or the directory is invalid. + * @throws RuntimeException If there is a problem with the directory mapping or JAR file URL creation. + */ + public static <T> List<T> loadServicesFromDirectory(Class<T> serviceClass) throws IOException { + String pluginDirKey = serviceClass.getSimpleName(); + String pluginDir = pluginDirMapping.get(pluginDirKey); + if (pluginDir == null) { + throw new RuntimeException("No mapping found for plugin directory key: " + pluginDirKey); + } + File jarDir = new File(pluginDir); + // If the directory does not exist, return an empty list. + if (!jarDir.exists()) { + return new ArrayList<>(); + } + if (!jarDir.isDirectory()) { + throw new IOException("The specified path is not a directory: " + pluginDir); + } + + File[] jarFiles = jarDir.listFiles((dir, name) -> name.endsWith(".jar")); + if (jarFiles == null || jarFiles.length == 0) { + LOG.info("No JAR files found in the plugin directory: {}", pluginDir); + return new ArrayList<>(); + } + + List<T> services = new ArrayList<>(); + for (File jarFile : jarFiles) { + URL[] jarURLs; + jarURLs = new URL[]{jarFile.toURI().toURL()}; + + try (ChildFirstClassLoader urlClassLoader = new ChildFirstClassLoader(jarURLs, + Thread.currentThread().getContextClassLoader())) { + ServiceLoader<T> serviceLoader = ServiceLoader.load(serviceClass, urlClassLoader); + for (T service : serviceLoader) { + services.add(service); + } + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + return services; + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java b/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java index 553b322076a..da163cc6fcf 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java +++ b/fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java @@ -34,7 +34,6 @@ import org.apache.doris.common.AnalysisException; import org.apache.doris.common.Config; import org.apache.doris.common.DdlException; import org.apache.doris.datasource.CatalogIf; -import org.apache.doris.datasource.CatalogMgr; import org.apache.doris.datasource.ExternalCatalog; import org.apache.doris.policy.Policy; import org.apache.doris.policy.StoragePolicy; @@ -1400,16 +1399,6 @@ public class PropertyAnalyzer { // "access_controller.properties.prop2" = "yyy", // ) // 1. get access controller class - String acClass = properties.getOrDefault(CatalogMgr.ACCESS_CONTROLLER_CLASS_PROP, ""); - if (!Strings.isNullOrEmpty(acClass)) { - // 2. check if class exists - try { - Class.forName(acClass); - } catch (ClassNotFoundException e) { - throw new AnalysisException("failed to find class " + acClass, e); - } - } - if (isAlter) { // The 'use_meta_cache' property can not be modified if (properties.containsKey(ExternalCatalog.USE_META_CACHE)) { diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticateType.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticateType.java index 4281c19bba6..1f16c1f541b 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticateType.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticateType.java @@ -40,4 +40,22 @@ public enum AuthenticateType { return DEFAULT; } } + + public static String getAuthTypeConfigString() { + String authType = Config.authentication_type.toLowerCase(); + + if (LdapConfig.ldap_authentication_enabled) { + return LDAP.name(); + } + + switch (authType) { + case "default": + return DEFAULT.toString(); + case "ldap": + return LDAP.toString(); + default: + return authType; + } + } + } diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticatorFactory.java similarity index 64% copy from fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java copy to fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticatorFactory.java index d4e0400c9eb..25ac87de4e7 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticatorFactory.java @@ -15,11 +15,23 @@ // specific language governing permissions and limitations // under the License. -package org.apache.doris.mysql.privilege; +package org.apache.doris.mysql.authenticate; -import java.util.Map; +import java.util.Properties; -public interface AccessControllerFactory { +public interface AuthenticatorFactory { + /** + * Creates a new instance of Authenticator. + * + * @return an instance of Authenticator + */ + Authenticator create(Properties initProps); - CatalogAccessController createAccessController(Map<String, String> prop); + /** + * Returns the identifier for the factory, such as "ldap" or "default". + * + * @return the factory identifier + */ + String factoryIdentifier(); } + diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticatorManager.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticatorManager.java index c00828f82fa..8ba711e6655 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticatorManager.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticatorManager.java @@ -17,47 +17,85 @@ package org.apache.doris.mysql.authenticate; +import org.apache.doris.common.util.ClassLoaderUtils; 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.plugin.PropertiesUtils; 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.List; import java.util.Optional; +import java.util.Properties; +import java.util.ServiceLoader; public class AuthenticatorManager { private static final Logger LOG = LogManager.getLogger(AuthenticatorManager.class); - private Authenticator defaultAuthenticator; - private Authenticator authTypeAuthenticator; + private static volatile Authenticator defaultAuthenticator = null; + private static volatile Authenticator authTypeAuthenticator = null; - 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 AuthenticatorManager(String type) { + LOG.info("Authenticate type: {}", type); + defaultAuthenticator = new DefaultAuthenticator(); + if (authTypeAuthenticator == null) { + synchronized (AuthenticatorManager.class) { + if (authTypeAuthenticator == null) { + try { + authTypeAuthenticator = loadFactoriesByName(type); + } catch (Exception e) { + LOG.warn("Failed to load authenticator by name: {}, using default authenticator", type, e); + authTypeAuthenticator = defaultAuthenticator; + } + } + } } } + + private Authenticator loadFactoriesByName(String identifier) throws Exception { + ServiceLoader<AuthenticatorFactory> loader = ServiceLoader.load(AuthenticatorFactory.class); + for (AuthenticatorFactory factory : loader) { + LOG.info("Found Authenticator Plugin Factory: {}", factory.factoryIdentifier()); + if (factory.factoryIdentifier().equalsIgnoreCase(identifier)) { + Properties properties = PropertiesUtils.loadAuthenticationConfigFile(); + return factory.create(properties); + } + } + return loadCustomerFactories(identifier); + + } + + private Authenticator loadCustomerFactories(String identifier) throws Exception { + List<AuthenticatorFactory> factories = ClassLoaderUtils.loadServicesFromDirectory(AuthenticatorFactory.class); + if (factories.isEmpty()) { + LOG.info("No customer authenticator found, using default authenticator"); + return defaultAuthenticator; + } + for (AuthenticatorFactory factory : factories) { + LOG.info("Found Customer Authenticator Plugin Factory: {}", factory.factoryIdentifier()); + if (factory.factoryIdentifier().equalsIgnoreCase(identifier)) { + Properties properties = PropertiesUtils.loadAuthenticationConfigFile(); + return factory.create(properties); + } + } + + throw new RuntimeException("No AuthenticatorFactory found for identifier: " + identifier); + } + public boolean authenticate(ConnectContext context, - String userName, - MysqlChannel channel, - MysqlSerializer serializer, - MysqlAuthPacket authPacket, - MysqlHandshakePacket handshakePacket) throws IOException { + 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); diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/DefaultAuthenticatorFactory.java similarity index 69% copy from fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java copy to fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/DefaultAuthenticatorFactory.java index d4e0400c9eb..5d073a8296a 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/DefaultAuthenticatorFactory.java @@ -15,11 +15,18 @@ // specific language governing permissions and limitations // under the License. -package org.apache.doris.mysql.privilege; +package org.apache.doris.mysql.authenticate; -import java.util.Map; +import java.util.Properties; -public interface AccessControllerFactory { +public class DefaultAuthenticatorFactory implements AuthenticatorFactory { + @Override + public DefaultAuthenticator create(Properties initProps) { + return new DefaultAuthenticator(); + } - CatalogAccessController createAccessController(Map<String, String> prop); + @Override + public String factoryIdentifier() { + return "default"; + } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticator.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticator.java index e37112372ce..cd9cef469d2 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticator.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticator.java @@ -75,10 +75,11 @@ public class LdapAuthenticator implements Authenticator { if (qualifiedUser.equals(Auth.ROOT_USER) || qualifiedUser.equals(Auth.ADMIN_USER)) { return false; } - if (!Env.getCurrentEnv().getAuth().getLdapManager().doesUserExist(qualifiedUser)) { - return false; - } - return true; + // 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); } /** diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticatorFactory.java similarity index 65% copy from fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java copy to fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticatorFactory.java index d4e0400c9eb..fba5c350d39 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticatorFactory.java @@ -15,11 +15,23 @@ // specific language governing permissions and limitations // under the License. -package org.apache.doris.mysql.privilege; +package org.apache.doris.mysql.authenticate.ldap; -import java.util.Map; +import org.apache.doris.mysql.authenticate.AuthenticatorFactory; -public interface AccessControllerFactory { +import java.util.Properties; + +public class LdapAuthenticatorFactory implements AuthenticatorFactory { + + + @Override + public LdapAuthenticator create(Properties initProps) { + return new LdapAuthenticator(); + } + + @Override + public String factoryIdentifier() { + return "ldap"; + } - CatalogAccessController createAccessController(Map<String, String> prop); } diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java index d4e0400c9eb..8d1481aa070 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java @@ -20,6 +20,14 @@ package org.apache.doris.mysql.privilege; import java.util.Map; public interface AccessControllerFactory { + /** + * Returns the identifier for the factory, such as "range-doris". + * + * @return the factory identifier + */ + default String factoryIdentifier() { + return this.getClass().getSimpleName(); + } CatalogAccessController createAccessController(Map<String, String> prop); } diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java index 42fa769d033..67d98837720 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerManager.java @@ -21,12 +21,13 @@ import org.apache.doris.analysis.TableName; import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.AuthorizationInfo; import org.apache.doris.catalog.Env; -import org.apache.doris.catalog.authorizer.ranger.doris.RangerCacheDorisAccessController; import org.apache.doris.common.Config; import org.apache.doris.common.UserException; +import org.apache.doris.common.util.ClassLoaderUtils; import org.apache.doris.datasource.CatalogIf; import org.apache.doris.datasource.ExternalCatalog; import org.apache.doris.datasource.InternalCatalog; +import org.apache.doris.plugin.PropertiesUtils; import org.apache.doris.qe.ConnectContext; import com.google.common.base.Preconditions; @@ -34,11 +35,14 @@ import com.google.common.collect.Maps; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.ServiceLoader; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** * AccessControllerManager is the entry point of privilege authentication. @@ -51,19 +55,62 @@ public class AccessControllerManager { private static final Logger LOG = LogManager.getLogger(AccessControllerManager.class); private Auth auth; + // Default access controller instance used for handling cases where no specific controller is specified private CatalogAccessController defaultAccessController; + // Map that stores the mapping between catalogs and their corresponding access controllers private Map<String, CatalogAccessController> ctlToCtlAccessController = Maps.newConcurrentMap(); + // Cache of loaded access controller factories for quick creation of new access controllers + private ConcurrentHashMap<String, AccessControllerFactory> accessControllerFactoriesCache + = new ConcurrentHashMap<>(); + // Mapping between access controller class names and their identifiers for easy lookup of factory identifiers + private ConcurrentHashMap<String, String> accessControllerClassNameMapping = new ConcurrentHashMap<>(); public AccessControllerManager(Auth auth) { this.auth = auth; - if (Config.access_controller_type.equalsIgnoreCase("ranger-doris")) { - defaultAccessController = new RangerCacheDorisAccessController("doris"); - } else { - defaultAccessController = new InternalAccessController(auth); - } + loadAccessControllerPlugins(); + String accessControllerName = Config.access_controller_type; + this.defaultAccessController = loadAccessControllerOrThrow(accessControllerName); ctlToCtlAccessController.put(InternalCatalog.INTERNAL_CATALOG_NAME, defaultAccessController); } + private CatalogAccessController loadAccessControllerOrThrow(String accessControllerName) { + if (accessControllerName.equalsIgnoreCase("default")) { + return new InternalAccessController(auth); + } + if (accessControllerFactoriesCache.containsKey(accessControllerName)) { + Map<String, String> prop; + try { + prop = PropertiesUtils.loadAccessControllerPropertiesOrNull(); + } catch (IOException e) { + throw new RuntimeException("Failed to load authorization properties." + + "Please check the configuration file, authorization name is " + accessControllerName, e); + } + return accessControllerFactoriesCache.get(accessControllerName).createAccessController(prop); + } + throw new RuntimeException("No authorization plugin factory found for " + accessControllerName + + ". Please confirm that your plugin is placed in the correct location."); + } + + private void loadAccessControllerPlugins() { + ServiceLoader<AccessControllerFactory> loaderFromClasspath = ServiceLoader.load(AccessControllerFactory.class); + for (AccessControllerFactory factory : loaderFromClasspath) { + LOG.info("Found Authentication Plugin Factories: {} from class path.", factory.factoryIdentifier()); + accessControllerFactoriesCache.put(factory.factoryIdentifier(), factory); + accessControllerClassNameMapping.put(factory.getClass().getName(), factory.factoryIdentifier()); + } + List<AccessControllerFactory> loader = null; + try { + loader = ClassLoaderUtils.loadServicesFromDirectory(AccessControllerFactory.class); + } catch (IOException e) { + throw new RuntimeException("Failed to load Authentication Plugin Factories", e); + } + for (AccessControllerFactory factory : loader) { + LOG.info("Found Access Controller Plugin Factory: {} from directory.", factory.factoryIdentifier()); + accessControllerFactoriesCache.put(factory.factoryIdentifier(), factory); + accessControllerClassNameMapping.put(factory.getClass().getName(), factory.factoryIdentifier()); + } + } + public CatalogAccessController getAccessControllerOrDefault(String ctl) { CatalogAccessController catalogAccessController = ctlToCtlAccessController.get(ctl); if (catalogAccessController != null) { @@ -93,23 +140,28 @@ public class AccessControllerManager { } public void createAccessController(String ctl, String acFactoryClassName, Map<String, String> prop, - boolean isDryRun) { - Class<?> factoryClazz = null; - try { - factoryClazz = Class.forName(acFactoryClassName); - AccessControllerFactory factory = (AccessControllerFactory) factoryClazz.newInstance(); - CatalogAccessController accessController = factory.createAccessController(prop); - if (!isDryRun) { - ctlToCtlAccessController.put(ctl, accessController); - LOG.info("create access controller {} for catalog {}", ctl, acFactoryClassName); - } - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } catch (InstantiationException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + boolean isDryRun) { + String pluginIdentifier = getPluginIdentifierForAccessController(acFactoryClassName); + CatalogAccessController accessController = accessControllerFactoriesCache.get(pluginIdentifier) + .createAccessController(prop); + if (!isDryRun) { + ctlToCtlAccessController.put(ctl, accessController); + LOG.info("create access controller {} for catalog {}", acFactoryClassName, ctl); + } + } + + private String getPluginIdentifierForAccessController(String acClassName) { + String pluginIdentifier = null; + if (accessControllerClassNameMapping.containsKey(acClassName)) { + pluginIdentifier = accessControllerClassNameMapping.get(acClassName); + } + if (accessControllerFactoriesCache.containsKey(acClassName)) { + pluginIdentifier = acClassName; } + if (null == pluginIdentifier || !accessControllerFactoriesCache.containsKey(pluginIdentifier)) { + throw new RuntimeException("Access Controller Plugin Factory not found for " + acClassName); + } + return pluginIdentifier; } public void removeAccessController(String ctl) { @@ -159,7 +211,7 @@ public class AccessControllerManager { } public boolean checkTblPriv(ConnectContext ctx, String qualifiedCtl, - String qualifiedDb, String tbl, PrivPredicate wanted) { + String qualifiedDb, String tbl, PrivPredicate wanted) { if (ctx.isSkipAuth()) { return true; } @@ -183,7 +235,7 @@ public class AccessControllerManager { public void checkColumnsPriv(UserIdentity currentUser, String ctl, String qualifiedDb, String tbl, Set<String> cols, - PrivPredicate wanted) throws UserException { + PrivPredicate wanted) throws UserException { boolean hasGlobal = checkGlobalPriv(currentUser, wanted); CatalogAccessController accessController = getAccessControllerOrDefault(ctl); accessController.checkColsPriv(hasGlobal, currentUser, ctl, qualifiedDb, diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RangerDorisAccessControllerFactory.java similarity index 66% copy from fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java copy to fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RangerDorisAccessControllerFactory.java index d4e0400c9eb..297fe5c708c 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/AccessControllerFactory.java +++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RangerDorisAccessControllerFactory.java @@ -17,9 +17,18 @@ package org.apache.doris.mysql.privilege; +import org.apache.doris.catalog.authorizer.ranger.doris.RangerCacheDorisAccessController; + import java.util.Map; -public interface AccessControllerFactory { +public class RangerDorisAccessControllerFactory implements AccessControllerFactory { + @Override + public String factoryIdentifier() { + return "ranger-doris"; + } - CatalogAccessController createAccessController(Map<String, String> prop); + @Override + public RangerCacheDorisAccessController createAccessController(Map<String, String> prop) { + return new RangerCacheDorisAccessController("doris"); + } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/plugin/PropertiesUtils.java b/fe/fe-core/src/main/java/org/apache/doris/plugin/PropertiesUtils.java new file mode 100644 index 00000000000..7318be7da61 --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/plugin/PropertiesUtils.java @@ -0,0 +1,68 @@ +// 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.plugin; + +import org.apache.doris.common.Config; +import org.apache.doris.common.EnvUtils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class PropertiesUtils { + + public static final Logger LOG = LogManager.getLogger(PropertiesUtils.class); + + public static Map<String, String> loadAccessControllerPropertiesOrNull() throws IOException { + String configFilePath = EnvUtils.getDorisHome() + Config.authorization_config_file_path; + if (new File(configFilePath).exists()) { + Properties properties = new Properties(); + properties.load(Files.newInputStream(Paths.get(configFilePath))); + return propertiesToMap(properties); + } + return null; + } + + public static Properties loadAuthenticationConfigFile() throws Exception { + String configFilePath = EnvUtils.getDorisHome() + Config.authentication_config_file_path; + if (new File(configFilePath).exists()) { + LOG.info("Loading authenticate configuration file: {}", configFilePath); + Properties properties = new Properties(); + properties.load(Files.newInputStream(Paths.get(configFilePath))); + return properties; + } + return new Properties(); + } + + public static Map<String, String> propertiesToMap(Properties properties) { + Map<String, String> map = new HashMap<>(); + for (Map.Entry<Object, Object> entry : properties.entrySet()) { + String key = String.valueOf(entry.getKey()); + String value = String.valueOf(entry.getValue()); + map.put(key, value); + } + return map; + } +} diff --git a/fe/fe-core/src/main/resources/META-INF/services/org.apache.doris.mysql.authenticate.AuthenticatorFactory b/fe/fe-core/src/main/resources/META-INF/services/org.apache.doris.mysql.authenticate.AuthenticatorFactory new file mode 100644 index 00000000000..3a013ff7f32 --- /dev/null +++ b/fe/fe-core/src/main/resources/META-INF/services/org.apache.doris.mysql.authenticate.AuthenticatorFactory @@ -0,0 +1,19 @@ +# +# 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. +# +# +org.apache.doris.mysql.authenticate.DefaultAuthenticatorFactory +org.apache.doris.mysql.authenticate.ldap.LdapAuthenticatorFactory \ No newline at end of file diff --git a/fe/fe-core/src/main/resources/META-INF/services/org.apache.doris.mysql.privilege.AccessControllerFactory b/fe/fe-core/src/main/resources/META-INF/services/org.apache.doris.mysql.privilege.AccessControllerFactory new file mode 100644 index 00000000000..e2100cb8b23 --- /dev/null +++ b/fe/fe-core/src/main/resources/META-INF/services/org.apache.doris.mysql.privilege.AccessControllerFactory @@ -0,0 +1,19 @@ +# +# 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. +# +# +org.apache.doris.mysql.privilege.RangerDorisAccessControllerFactory +org.apache.doris.catalog.authorizer.ranger.hive.RangerHiveAccessControllerFactory \ No newline at end of file diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessControllerFactory.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/CustomAccessControllerFactory.java similarity index 78% copy from fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessControllerFactory.java copy to fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/CustomAccessControllerFactory.java index 545e7a26836..f30ab8def4f 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessControllerFactory.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/CustomAccessControllerFactory.java @@ -15,16 +15,21 @@ // specific language governing permissions and limitations // under the License. -package org.apache.doris.catalog.authorizer.ranger.hive; +package org.apache.doris.nereids.privileges; import org.apache.doris.mysql.privilege.AccessControllerFactory; import org.apache.doris.mysql.privilege.CatalogAccessController; import java.util.Map; -public class RangerHiveAccessControllerFactory implements AccessControllerFactory { +public class CustomAccessControllerFactory implements AccessControllerFactory { + @Override + public String factoryIdentifier() { + return "CustomAccess"; + } + @Override public CatalogAccessController createAccessController(Map<String, String> prop) { - return new RangerCacheHiveAccessController(prop); + return new TestCheckPrivileges.SimpleCatalogAccessController(); } } diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/TestCheckPrivileges.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/TestCheckPrivileges.java index 07cbb002a64..24c2e656dca 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/TestCheckPrivileges.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/TestCheckPrivileges.java @@ -22,10 +22,10 @@ import org.apache.doris.catalog.Column; import org.apache.doris.catalog.Env; import org.apache.doris.catalog.PrimitiveType; import org.apache.doris.common.AuthorizationException; +import org.apache.doris.common.DdlException; import org.apache.doris.common.FeConstants; import org.apache.doris.datasource.CatalogMgr; import org.apache.doris.datasource.test.TestExternalCatalog.TestCatalogProvider; -import org.apache.doris.mysql.privilege.AccessControllerFactory; import org.apache.doris.mysql.privilege.AccessControllerManager; import org.apache.doris.mysql.privilege.CatalogAccessController; import org.apache.doris.mysql.privilege.DataMaskPolicy; @@ -91,10 +91,20 @@ public class TestCheckPrivileges extends TestWithFeService implements GeneratedM String catalogProvider = "org.apache.doris.nereids.privileges.TestCheckPrivileges$CustomCatalogProvider"; String accessControllerFactory - = "org.apache.doris.nereids.privileges.TestCheckPrivileges$CustomAccessControllerFactory"; - + = "org.apache.doris.nereids.privileges.CustomAccessControllerFactory"; String catalog = "custom_catalog"; String db = "test_db"; + String failedAccessControllerFactory + = "org.apache.doris.nereids.privileges.FailedAccessControllerFactory"; + //try to create catalog with failed access controller + Assertions.assertThrows(DdlException.class, () -> { + createCatalog("create catalog " + catalog + " properties(" + + " \"type\"=\"test\"," + + " \"catalog_provider.class\"=\"" + catalogProvider + "\"," + + " \"" + CatalogMgr.ACCESS_CONTROLLER_CLASS_PROP + "\"=\"" + failedAccessControllerFactory + "\"" + + ")"); + }, "Failed to init access controller"); + createCatalog("create catalog " + catalog + " properties(" + " \"type\"=\"test\"," + " \"catalog_provider.class\"=\"" + catalogProvider + "\"," @@ -313,13 +323,6 @@ public class TestCheckPrivileges extends TestWithFeService implements GeneratedM } } - public static class CustomAccessControllerFactory implements AccessControllerFactory { - @Override - public CatalogAccessController createAccessController(Map<String, String> prop) { - return new SimpleCatalogAccessController(); - } - } - public static class SimpleCatalogAccessController implements CatalogAccessController { private static ThreadLocal<List<TablePrivilege>> tablePrivileges = new ThreadLocal<>(); private static ThreadLocal<List<ColumnPrivilege>> columnPrivileges = new ThreadLocal<>(); diff --git a/fe/fe-core/src/test/resources/META-INF/services/org.apache.doris.mysql.privilege.AccessControllerFactory b/fe/fe-core/src/test/resources/META-INF/services/org.apache.doris.mysql.privilege.AccessControllerFactory new file mode 100644 index 00000000000..83924e7e0f6 --- /dev/null +++ b/fe/fe-core/src/test/resources/META-INF/services/org.apache.doris.mysql.privilege.AccessControllerFactory @@ -0,0 +1,18 @@ +# +# 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. +# +# +org.apache.doris.nereids.privileges.CustomAccessControllerFactory \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@doris.apache.org For additional commands, e-mail: commits-h...@doris.apache.org