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]
