This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git

commit fdf8b59464d8d0c2074798c3998a92ab1ea83ba6
Author: linrrarity <[email protected]>
AuthorDate: Mon Sep 21 15:36:14 2026 +0800

    branch-4.1: [Enhance](resource) Restrict AI resource creation by user 
identity (#68147) (#68265)
    
    pick: https://github.com/apache/doris/pull/68147
---
 .../main/java/org/apache/doris/common/Config.java  |  4 ++
 .../plans/commands/info/CreateResourceInfo.java    | 10 +++
 .../plans/commands/CreateResourceCommandTest.java  | 75 ++++++++++++++++++++++
 3 files changed, 89 insertions(+)

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 5de052e5389..963ce97a1a8 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
@@ -3100,6 +3100,10 @@ public class Config extends ConfigBase {
     })
     public static boolean enable_python_udf = true;
 
+    @ConfField(description = "The user identity allowed to create AI 
resources, in the form 'user'@'host'. "
+            + "The default value '*' allows any user that satisfies the 
existing privilege checks.")
+    public static String ai_resource_allowed_user = "*";
+
     @ConfField(description = {
             "是否忽略 Image 文件中未知的模块。如果为 true,不在 PersistMetaModules.MODULE_NAMES 
中的元数据模块将被忽略并跳过。"
                     + "默认为 false,如果 Image 文件中包含未知的模块,Doris 将会抛出异常。"
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateResourceInfo.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateResourceInfo.java
index 8365693ec69..4b1f6f1a26f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateResourceInfo.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateResourceInfo.java
@@ -18,9 +18,11 @@
 package org.apache.doris.nereids.trees.plans.commands.info;
 
 import org.apache.doris.analysis.ResourceTypeEnum;
+import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.catalog.Resource.ResourceType;
 import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.Config;
 import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.ErrorReport;
 import org.apache.doris.common.FeNameFormat;
@@ -75,6 +77,14 @@ public class CreateResourceInfo {
         }
 
         analyzeResourceType();
+
+        if (resourceType == ResourceType.AI && 
!"*".equals(Config.ai_resource_allowed_user)) {
+            UserIdentity allowedUser = 
UserIdentity.fromString(Config.ai_resource_allowed_user);
+            if 
(!ConnectContext.get().getCurrentUserIdentity().equals(allowedUser)) {
+                throw new AnalysisException("Current user does not have 
permission to create AI resources",
+                        ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR);
+            }
+        }
     }
 
     /**
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/CreateResourceCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/CreateResourceCommandTest.java
index 98afb4eb15f..8721355ace5 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/CreateResourceCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/CreateResourceCommandTest.java
@@ -17,8 +17,12 @@
 
 package org.apache.doris.nereids.trees.plans.commands;
 
+import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.jmockit.Deencapsulation;
 import org.apache.doris.mysql.privilege.AccessControllerManager;
 import org.apache.doris.mysql.privilege.PrivPredicate;
 import org.apache.doris.nereids.parser.NereidsParser;
@@ -33,6 +37,7 @@ import mockit.Expectations;
 import mockit.Mocked;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 public class CreateResourceCommandTest extends TestWithFeService {
     @Test
@@ -119,6 +124,76 @@ public class CreateResourceCommandTest extends 
TestWithFeService {
         Assertions.assertDoesNotThrow(() -> createResource(jdbc));
     }
 
+    @Test
+    public void testAiResourceCreationRequiresConfiguredUserIdentity() {
+        allowAdminPrivilege();
+        String originalAllowedUser = Config.ai_resource_allowed_user;
+        UserIdentity originalUser = connectContext.getCurrentUserIdentity();
+        try {
+            Config.ai_resource_allowed_user = "'root'@'%'";
+
+            connectContext.setCurrentUserIdentity(UserIdentity.ROOT);
+            Assertions.assertDoesNotThrow(() -> 
createResourceInfo("ai").validate());
+
+            connectContext.setCurrentUserIdentity(
+                    UserIdentity.createAnalyzedUserIdentWithIp("root", 
"10.0.%"));
+            AnalysisException exception = Assertions.assertThrows(
+                    AnalysisException.class, () -> 
createResourceInfo("ai").validate());
+            Assertions.assertEquals(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
+                    exception.getMysqlErrorCode());
+            Assertions.assertEquals("errCode = 2, detailMessage = "
+                            + "Current user does not have permission to create 
AI resources",
+                    exception.getMessage());
+        } finally {
+            Config.ai_resource_allowed_user = originalAllowedUser;
+            connectContext.setCurrentUserIdentity(originalUser);
+        }
+    }
+
+    @Test
+    public void testAiResourceCreationAllowsAnyAdminForWildcard() {
+        allowAdminPrivilege();
+        String originalAllowedUser = Config.ai_resource_allowed_user;
+        UserIdentity originalUser = connectContext.getCurrentUserIdentity();
+        try {
+            Config.ai_resource_allowed_user = "*";
+            connectContext.setCurrentUserIdentity(UserIdentity.ADMIN);
+
+            Assertions.assertDoesNotThrow(() -> 
createResourceInfo("ai").validate());
+        } finally {
+            Config.ai_resource_allowed_user = originalAllowedUser;
+            connectContext.setCurrentUserIdentity(originalUser);
+        }
+    }
+
+    @Test
+    public void testAllowedUserDoesNotRestrictNonAiResourceCreation() {
+        allowAdminPrivilege();
+        String originalAllowedUser = Config.ai_resource_allowed_user;
+        UserIdentity originalUser = connectContext.getCurrentUserIdentity();
+        try {
+            Config.ai_resource_allowed_user = "'root'@'%'";
+            connectContext.setCurrentUserIdentity(UserIdentity.ADMIN);
+
+            Assertions.assertDoesNotThrow(() -> 
createResourceInfo("jdbc").validate());
+        } finally {
+            Config.ai_resource_allowed_user = originalAllowedUser;
+            connectContext.setCurrentUserIdentity(originalUser);
+        }
+    }
+
+    private void allowAdminPrivilege() {
+        Env env = Env.getCurrentEnv();
+        AccessControllerManager spyAcm = Mockito.spy(env.getAccessManager());
+        Mockito.doReturn(true).when(spyAcm).checkGlobalPriv(
+                Mockito.nullable(ConnectContext.class), 
Mockito.eq(PrivPredicate.ADMIN));
+        Deencapsulation.setField(env, "accessManager", spyAcm);
+    }
+
+    private CreateResourceInfo createResourceInfo(String type) {
+        return new CreateResourceInfo(true, false, "test_" + type, 
ImmutableMap.of("type", type));
+    }
+
     private void createResource(String sql) throws Exception {
         LogicalPlan plan = new NereidsParser().parseSingle(sql);
         Assertions.assertTrue(plan instanceof CreateResourceCommand);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to