Branch 2.1 authentication plugin commits (#44156)
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 59daae8..08a207e 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 @@
"设置为 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 03679d6..1d4ea60 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 @@
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 545e7a2..33e3f4a 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 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 0000000..ad3b0df
--- /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 0000000..c82858c
--- /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 553b322..da163cc 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.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 @@
// "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 4281c19..1f16c1f 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 @@
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/authenticate/AuthenticatorFactory.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticatorFactory.java
new file mode 100644
index 0000000..25ac87d
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/AuthenticatorFactory.java
@@ -0,0 +1,37 @@
+// 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 java.util.Properties;
+
+public interface AuthenticatorFactory {
+ /**
+ * Creates a new instance of Authenticator.
+ *
+ * @return an instance of Authenticator
+ */
+ Authenticator create(Properties initProps);
+
+ /**
+ * 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 c00828f..8ba711e 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/authenticate/DefaultAuthenticatorFactory.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/DefaultAuthenticatorFactory.java
new file mode 100644
index 0000000..5d073a8
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/DefaultAuthenticatorFactory.java
@@ -0,0 +1,32 @@
+// 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 java.util.Properties;
+
+public class DefaultAuthenticatorFactory implements AuthenticatorFactory {
+ @Override
+ public DefaultAuthenticator create(Properties initProps) {
+ return new DefaultAuthenticator();
+ }
+
+ @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 e371123..cd9cef4 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 @@
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/authenticate/ldap/LdapAuthenticatorFactory.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticatorFactory.java
new file mode 100644
index 0000000..fba5c35
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapAuthenticatorFactory.java
@@ -0,0 +1,37 @@
+// 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.mysql.authenticate.AuthenticatorFactory;
+
+import java.util.Properties;
+
+public class LdapAuthenticatorFactory implements AuthenticatorFactory {
+
+
+ @Override
+ public LdapAuthenticator create(Properties initProps) {
+ return new LdapAuthenticator();
+ }
+
+ @Override
+ public String factoryIdentifier() {
+ return "ldap";
+ }
+
+}
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 d4e0400..8d1481a 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 @@
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 42fa769..67d98837 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.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 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 @@
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,25 +140,30 @@
}
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) {
ctlToCtlAccessController.remove(ctl);
LOG.info("remove access controller for catalog {}", ctl);
@@ -159,7 +211,7 @@
}
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 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/RangerDorisAccessControllerFactory.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RangerDorisAccessControllerFactory.java
new file mode 100644
index 0000000..297fe5c
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RangerDorisAccessControllerFactory.java
@@ -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.privilege;
+
+import org.apache.doris.catalog.authorizer.ranger.doris.RangerCacheDorisAccessController;
+
+import java.util.Map;
+
+public class RangerDorisAccessControllerFactory implements AccessControllerFactory {
+ @Override
+ public String factoryIdentifier() {
+ return "ranger-doris";
+ }
+
+ @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 0000000..7318be7
--- /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 0000000..3a013ff
--- /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 0000000..e2100cb
--- /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/test/java/org/apache/doris/nereids/privileges/CustomAccessControllerFactory.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/CustomAccessControllerFactory.java
new file mode 100644
index 0000000..f30ab8d
--- /dev/null
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/CustomAccessControllerFactory.java
@@ -0,0 +1,35 @@
+// 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.nereids.privileges;
+
+import org.apache.doris.mysql.privilege.AccessControllerFactory;
+import org.apache.doris.mysql.privilege.CatalogAccessController;
+
+import java.util.Map;
+
+public class CustomAccessControllerFactory implements AccessControllerFactory {
+ @Override
+ public String factoryIdentifier() {
+ return "CustomAccess";
+ }
+
+ @Override
+ public CatalogAccessController createAccessController(Map<String, String> 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 07cbb00..24c2e65 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.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 @@
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 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 0000000..83924e7
--- /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