This is an automated email from the ASF dual-hosted git repository.
starocean999 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new ee4a91720f6 [Enhance](resource) Restrict modification of root-created
AI resources (#67659)
ee4a91720f6 is described below
commit ee4a91720f66ec3e2f6023a990e4ebcb9d66fd77
Author: linrrarity <[email protected]>
AuthorDate: Thu Sep 10 12:23:16 2026 +0800
[Enhance](resource) Restrict modification of root-created AI resources
(#67659)
- Persist whether an `AI resource` was created by root.
- Only `root` can alter or drop root-created AI resources.
- `ADMIN` users can still alter or drop AI resources created by `ADMIN`.
---
.../java/org/apache/doris/catalog/AIResource.java | 10 ++
.../java/org/apache/doris/catalog/Resource.java | 8 ++
.../java/org/apache/doris/catalog/ResourceMgr.java | 7 +-
.../trees/plans/commands/AlterResourceCommand.java | 6 +-
.../plans/commands/CreateResourceCommand.java | 2 +-
.../trees/plans/commands/DropResourceCommand.java | 8 ++
.../org/apache/doris/catalog/AIResourceTest.java | 30 +++++-
.../plans/commands/AIResourceCommandTest.java | 118 +++++++++++++++++++++
.../auth_call/test_ddl_ai_resource_auth.groovy | 28 ++++-
9 files changed, 208 insertions(+), 9 deletions(-)
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/AIResource.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/AIResource.java
index 882be79b98c..346e58285fd 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/AIResource.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/AIResource.java
@@ -57,6 +57,8 @@ public class AIResource extends Resource {
private static final Logger LOG = LogManager.getLogger(AIResource.class);
@SerializedName(value = "properties")
private Map<String, String> properties;
+ @SerializedName(value = "createdByRoot")
+ private boolean createdByRoot;
public AIResource() {
super();
@@ -67,6 +69,14 @@ public class AIResource extends Resource {
properties = Maps.newHashMap();
}
+ public boolean isCreatedByRoot() {
+ return createdByRoot;
+ }
+
+ void setCreatedByRoot(boolean createdByRoot) {
+ this.createdByRoot = createdByRoot;
+ }
+
@Override
protected void setProperties(ImmutableMap<String, String> newProperties)
throws DdlException {
Preconditions.checkState(newProperties != null);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Resource.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/Resource.java
index 4b669c155f0..92ec990f557 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Resource.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Resource.java
@@ -17,6 +17,7 @@
package org.apache.doris.catalog;
+import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.FeConstants;
@@ -129,11 +130,18 @@ public abstract class Resource implements Writable,
GsonPostProcessable {
}
public static Resource fromCommand(CreateResourceCommand command) throws
DdlException {
+ return fromCommand(command, null);
+ }
+
+ public static Resource fromCommand(CreateResourceCommand command,
UserIdentity creator) throws DdlException {
CreateResourceInfo info = command.getInfo();
Resource resource = getResourceInstance(info.getResourceType(),
info.getResourceName());
resource.id = Env.getCurrentEnv().getNextId();
resource.version = 0;
resource.setProperties(info.getProperties());
+ if (resource instanceof AIResource) {
+ ((AIResource) resource).setCreatedByRoot(creator != null &&
creator.isRootUser());
+ }
return resource;
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java
index 98a8abb9701..2c7c16b78bc 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java
@@ -17,6 +17,7 @@
package org.apache.doris.catalog;
+import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Resource.ResourceType;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.DdlException;
@@ -76,12 +77,16 @@ public class ResourceMgr implements Writable {
}
public void createResource(CreateResourceCommand command) throws
DdlException {
+ createResource(command, null);
+ }
+
+ public void createResource(CreateResourceCommand command, UserIdentity
creator) throws DdlException {
CreateResourceInfo info = command.getInfo();
if (info.getResourceType() == ResourceType.UNKNOWN) {
throw new DdlException(
"Only support SPARK, ODBC_CATALOG, JDBC, S3_COOLDOWN, S3,
HDFS(JFS/JUICEFS), and HMS resource.");
}
- Resource resource = Resource.fromCommand(command);
+ Resource resource = Resource.fromCommand(command, creator);
if (createResource(resource, info.isIfNotExists())) {
Env.getCurrentEnv().getEditLog().logCreateResource(resource);
LOG.info("Create resource success. Resource: {}",
resource.getName());
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterResourceCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterResourceCommand.java
index 576e8ee2027..fd1bcc641fc 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterResourceCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterResourceCommand.java
@@ -17,6 +17,7 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.catalog.AIResource;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.Resource;
import org.apache.doris.common.AnalysisException;
@@ -64,6 +65,10 @@ public class AlterResourceCommand extends AlterCommand
implements NeedAuditEncry
if (resource == null) {
throw new AnalysisException("Unknown resource: " + resourceName);
}
+ if (resource instanceof AIResource && ((AIResource)
resource).isCreatedByRoot()
+ && (ctx.getCurrentUserIdentity() == null ||
!ctx.getCurrentUserIdentity().isRootUser())) {
+ throw new AnalysisException("Only root user can modify
root-created AI resource: " + resourceName);
+ }
// check properties
resource.checkProperties(properties);
}
@@ -86,4 +91,3 @@ public class AlterResourceCommand extends AlterCommand
implements NeedAuditEncry
return true;
}
}
-
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateResourceCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateResourceCommand.java
index db7e989a0f3..0a56e87393e 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateResourceCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateResourceCommand.java
@@ -44,7 +44,7 @@ public class CreateResourceCommand extends Command implements
ForwardWithSync, N
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
info.validate();
- Env.getCurrentEnv().getResourceMgr().createResource(this);
+ Env.getCurrentEnv().getResourceMgr().createResource(this,
ctx.getCurrentUserIdentity());
}
@Override
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropResourceCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropResourceCommand.java
index 5f7c45d7f8e..236e6519af4 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropResourceCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropResourceCommand.java
@@ -19,7 +19,9 @@ package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.analysis.ResourceTypeEnum;
import org.apache.doris.analysis.StmtType;
+import org.apache.doris.catalog.AIResource;
import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.Resource;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
@@ -67,6 +69,12 @@ public class DropResourceCommand extends DropCommand {
}
FeNameFormat.checkResourceName(resourceName, ResourceTypeEnum.GENERAL);
+
+ Resource resource =
Env.getCurrentEnv().getResourceMgr().getResource(resourceName);
+ if (resource instanceof AIResource && ((AIResource)
resource).isCreatedByRoot()
+ && (ctx.getCurrentUserIdentity() == null ||
!ctx.getCurrentUserIdentity().isRootUser())) {
+ throw new AnalysisException("Only root user can modify
root-created AI resource: " + resourceName);
+ }
}
@Override
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/catalog/AIResourceTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/AIResourceTest.java
index 70d37bf5ebf..25e4a76e7b8 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/AIResourceTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/AIResourceTest.java
@@ -17,10 +17,12 @@
package org.apache.doris.catalog;
+import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.FeConstants;
import org.apache.doris.common.FeMetaVersion;
import org.apache.doris.common.UserException;
+import org.apache.doris.common.io.Text;
import org.apache.doris.datasource.property.constants.AIProperties;
import org.apache.doris.meta.MetaContext;
import org.apache.doris.mysql.privilege.AccessControllerManager;
@@ -28,9 +30,12 @@ import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.commands.CreateResourceCommand;
import org.apache.doris.nereids.trees.plans.commands.info.CreateResourceInfo;
import org.apache.doris.persist.EditLog;
+import org.apache.doris.persist.gson.GsonUtils;
import org.apache.doris.qe.ConnectContext;
import com.google.common.collect.ImmutableMap;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Assertions;
@@ -97,11 +102,21 @@ public class AIResourceTest {
.thenReturn(true);
// resource with default settings
- CreateResourceCommand createResourceCommand = new
CreateResourceCommand(
+ CreateResourceCommand initialCreateResourceCommand = new
CreateResourceCommand(
new CreateResourceInfo(true, false, name,
ImmutableMap.copyOf(aiProperties)));
- createResourceCommand.getInfo().validate();
+ initialCreateResourceCommand.getInfo().validate();
+
+ AIResource legacyAIResource = (AIResource)
Resource.fromCommand(initialCreateResourceCommand);
+ Assertions.assertFalse(legacyAIResource.isCreatedByRoot());
+
+ AIResource rootCreatedAIResource =
+ (AIResource)
Resource.fromCommand(initialCreateResourceCommand, UserIdentity.ROOT);
+ Assertions.assertTrue(rootCreatedAIResource.isCreatedByRoot());
+
+ AIResource aiResource =
+ (AIResource)
Resource.fromCommand(initialCreateResourceCommand, UserIdentity.ADMIN);
+ Assertions.assertFalse(aiResource.isCreatedByRoot());
- AIResource aiResource = (AIResource)
Resource.fromCommand(createResourceCommand);
Assertions.assertEquals(name, aiResource.getName());
Assertions.assertEquals(type,
aiResource.getType().name().toLowerCase());
Assertions.assertEquals(endpoint,
aiResource.getProperty(AIProperties.ENDPOINT));
@@ -124,7 +139,7 @@ public class AIResourceTest {
aiProperties.put(AIProperties.MAX_RETRIES, maxRetries);
aiProperties.put(AIProperties.RETRY_DELAY_SECOND,
retryDelaySecond);
- createResourceCommand = new CreateResourceCommand(
+ CreateResourceCommand createResourceCommand = new
CreateResourceCommand(
new CreateResourceInfo(true, false, name,
ImmutableMap.copyOf(aiProperties)));
createResourceCommand.getInfo().validate();
@@ -233,7 +248,9 @@ public class AIResourceTest {
DataOutputStream aiDos = new
DataOutputStream(Files.newOutputStream(path));
AIResource aiResource1 = new AIResource("ai_1");
- aiResource1.write(aiDos);
+ JsonObject legacyResourceJson =
JsonParser.parseString(GsonUtils.GSON.toJson(aiResource1)).getAsJsonObject();
+ legacyResourceJson.remove("createdByRoot");
+ Text.writeString(aiDos, legacyResourceJson.toString());
ImmutableMap<String, String> properties = ImmutableMap.of(
"ai.endpoint", endpoint,
@@ -243,6 +260,7 @@ public class AIResourceTest {
"ai.validity_check", "false"
);
AIResource aiResource2 = new AIResource("ai_2");
+ aiResource2.setCreatedByRoot(true);
aiResource2.setProperties(properties);
aiResource2.write(aiDos);
@@ -256,6 +274,8 @@ public class AIResourceTest {
Assertions.assertEquals("ai_1", rAiResource1.getName());
Assertions.assertEquals("ai_2", rAiResource2.getName());
+ Assertions.assertFalse(rAiResource1.isCreatedByRoot());
+ Assertions.assertTrue(rAiResource2.isCreatedByRoot());
Assertions.assertEquals(rAiResource2.getProperty(AIProperties.ENDPOINT),
endpoint);
Assertions.assertEquals(rAiResource2.getProperty(AIProperties.PROVIDER_TYPE),
providerType.toUpperCase());
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AIResourceCommandTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AIResourceCommandTest.java
new file mode 100644
index 00000000000..3f6af103f13
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AIResourceCommandTest.java
@@ -0,0 +1,118 @@
+// 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.trees.plans.commands;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.AIResource;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.ResourceMgr;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.mysql.privilege.AccessControllerManager;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.SessionVariable;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+
+public class AIResourceCommandTest {
+ private static final String RESOURCE_NAME = "root_ai_resource";
+
+ private ConnectContext connectContext;
+ private AIResource aiResource;
+ private MockedStatic<Env> envMockedStatic;
+ private MockedStatic<ConnectContext> connectContextMockedStatic;
+
+ @BeforeEach
+ public void setUp() {
+ Env env = Mockito.mock(Env.class);
+ ResourceMgr resourceMgr = Mockito.mock(ResourceMgr.class);
+ AccessControllerManager accessManager =
Mockito.mock(AccessControllerManager.class);
+ connectContext = Mockito.mock(ConnectContext.class);
+ aiResource = Mockito.mock(AIResource.class);
+
+ envMockedStatic = Mockito.mockStatic(Env.class);
+ connectContextMockedStatic = Mockito.mockStatic(ConnectContext.class);
+ envMockedStatic.when(Env::getCurrentEnv).thenReturn(env);
+
connectContextMockedStatic.when(ConnectContext::get).thenReturn(connectContext);
+
+ Mockito.when(env.getAccessManager()).thenReturn(accessManager);
+ Mockito.when(env.getResourceMgr()).thenReturn(resourceMgr);
+ Mockito.when(connectContext.getEnv()).thenReturn(env);
+ Mockito.when(connectContext.getSessionVariable()).thenReturn(new
SessionVariable());
+ Mockito.when(accessManager.checkGlobalPriv(connectContext,
PrivPredicate.ADMIN)).thenReturn(true);
+
Mockito.when(resourceMgr.getResource(RESOURCE_NAME)).thenReturn(aiResource);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ connectContextMockedStatic.close();
+ envMockedStatic.close();
+ }
+
+ @Test
+ public void testAdminCannotAlterOrDropRootCreatedAIResource() {
+ Mockito.when(aiResource.isCreatedByRoot()).thenReturn(true);
+
Mockito.when(connectContext.getCurrentUserIdentity()).thenReturn(UserIdentity.ADMIN);
+
+ AlterResourceCommand alterCommand = new AlterResourceCommand(
+ RESOURCE_NAME, Collections.singletonMap("ai.temperature",
"0.8"));
+ AnalysisException alterException = Assertions.assertThrows(
+ AnalysisException.class, () ->
alterCommand.doRun(connectContext, null));
+ Assertions.assertTrue(alterException.getMessage()
+ .contains("Only root user can modify root-created AI
resource"));
+
+ DropResourceCommand dropCommand = new DropResourceCommand(false,
RESOURCE_NAME);
+ AnalysisException dropException = Assertions.assertThrows(
+ AnalysisException.class, () ->
dropCommand.doRun(connectContext, null));
+ Assertions.assertTrue(dropException.getMessage()
+ .contains("Only root user can modify root-created AI
resource"));
+ }
+
+ @Test
+ public void testRootCanAlterAndDropRootCreatedAIResource() {
+ Mockito.when(aiResource.isCreatedByRoot()).thenReturn(true);
+
Mockito.when(connectContext.getCurrentUserIdentity()).thenReturn(UserIdentity.ROOT);
+
+ AlterResourceCommand alterCommand = new AlterResourceCommand(
+ RESOURCE_NAME, Collections.singletonMap("ai.temperature",
"0.8"));
+ Assertions.assertDoesNotThrow(() -> alterCommand.doRun(connectContext,
null));
+
+ DropResourceCommand dropCommand = new DropResourceCommand(false,
RESOURCE_NAME);
+ Assertions.assertDoesNotThrow(() -> dropCommand.doRun(connectContext,
null));
+ }
+
+ @Test
+ public void testAdminCanAlterAndDropLegacyAIResource() {
+ Mockito.when(aiResource.isCreatedByRoot()).thenReturn(false);
+
Mockito.when(connectContext.getCurrentUserIdentity()).thenReturn(UserIdentity.ADMIN);
+
+ AlterResourceCommand alterCommand = new AlterResourceCommand(
+ RESOURCE_NAME, Collections.singletonMap("ai.temperature",
"0.8"));
+ Assertions.assertDoesNotThrow(() -> alterCommand.doRun(connectContext,
null));
+
+ DropResourceCommand dropCommand = new DropResourceCommand(false,
RESOURCE_NAME);
+ Assertions.assertDoesNotThrow(() -> dropCommand.doRun(connectContext,
null));
+ }
+}
diff --git a/regression-test/suites/auth_call/test_ddl_ai_resource_auth.groovy
b/regression-test/suites/auth_call/test_ddl_ai_resource_auth.groovy
index 1e8d040b585..90464713824 100644
--- a/regression-test/suites/auth_call/test_ddl_ai_resource_auth.groovy
+++ b/regression-test/suites/auth_call/test_ddl_ai_resource_auth.groovy
@@ -89,7 +89,33 @@ suite("test_ddl_ai_resource_auth","p0,auth_call") {
assertTrue(res.size() == 0)
}
+ sql """CREATE RESOURCE "${resourceName}"
+ PROPERTIES(
+ 'type' = 'ai',
+ 'ai.provider_type' = 'deepseek',
+ 'ai.endpoint' = 'https://api.deepseek.com/chat/completions',
+ 'ai.model_name' = 'deepseek-chat',
+ 'ai.api_key' = 'sk-xxx',
+ 'ai.temperature' = '0.7',
+ 'ai.max_token' = '1024',
+ 'ai.max_retries' = '3',
+ 'ai.retry_delay_second' = '1',
+ 'ai.validity_check' = 'false'
+ );"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql """ALTER RESOURCE '${resourceName}' PROPERTIES
('ai.temperature' = '0.8');"""
+ exception "Only root user can modify root-created AI resource"
+ }
+ test {
+ sql """DROP RESOURCE '${resourceName}'"""
+ exception "Only root user can modify root-created AI resource"
+ }
+ }
+ sql """ALTER RESOURCE '${resourceName}' PROPERTIES ('ai.temperature' =
'0.8');"""
+ sql """DROP RESOURCE '${resourceName}'"""
+
try_sql("""DROP RESOURCE '${resourceName}'""")
sql """drop database if exists ${dbName}"""
try_sql("DROP USER ${user}")
-}
\ No newline at end of file
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]