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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new d5d0d502c fix(ai): address an ACL rule by principal when it has no 
numeric id (#6536)
d5d0d502c is described below

commit d5d0d502c09ce0ec00e0a5585be34cca85fb96d3
Author: Apulupie <[email protected]>
AuthorDate: Thu Oct 15 18:14:50 2026 +0800

    fix(ai): address an ACL rule by principal when it has no numeric id (#6536)
    
    rmq.acl.update demanded a numeric id, so on an instance whose ACL rules 
have no numeric database id
    (Tencent Cloud roles; rmq.acl.list omits the id for them since #4574) the 
tool always failed with
    "ACL id must be numeric", for both the preview and the execute call - even 
though AclService.getRule
    already resolves a role name (id.equals(rule.getPrincipal())) and 
AclService.updateRule's Tencent
    branch never asks for an id (TencentAclService.updateRule maps the rule's 
principal onto ModifyRole).
    rmq.acl.get and rmq.acl.delete pass the identifier through and work; #4574 
left this update path as a
    follow-up.
    
    The handler now resolves the addressed rule through AclService.getRule - 
the service that owns the
    "numeric id or principal" decision - and updates by that rule's id, in 
preview and execute alike.
    A Tencent rule therefore travels with a null id and is updated by 
principal, an Apache rule keeps the
    numeric id it has today, and a bogus identifier is reported by the service 
(EntityIds.parseId answers
    400 "id must be a numeric value", a missing rule 404). The acl.yaml id 
field of get/update/delete now
    documents both accepted forms so the model knows a principal is a valid 
identifier, and catalog_gen.go
    is regenerated. ToolError.ACL_ID_INVALID lost its only reference and is 
removed.
    
    Verified with `mvn -B -ntp test 
-Dtest='org.apache.rocketmq.studio.ops.ai.**.*Test'`: 951 tests green
    (including AclUpdateToolHandlerTest, AclMutationPlanTest, ToolCatalogTest 
and
    ToolOutputSchemaContractTest), 0 checkstyle violations, and `make 
catalog-verify` green.
---
 rmqctl/internal/catalog/catalog_gen.go             |  8 +--
 .../studio/ops/ai/tool/core/ToolError.java         |  4 --
 .../ai/tool/handler/acl/AclUpdateToolHandler.java  | 21 ++----
 .../src/main/resources/tool-catalog/tools/acl.yaml |  9 +++
 .../tool/handler/acl/AclUpdateToolHandlerTest.java | 74 ++++++++++++++++++++++
 5 files changed, 93 insertions(+), 23 deletions(-)

diff --git a/rmqctl/internal/catalog/catalog_gen.go 
b/rmqctl/internal/catalog/catalog_gen.go
index 73a23cb2d..fedfdff2a 100644
--- a/rmqctl/internal/catalog/catalog_gen.go
+++ b/rmqctl/internal/catalog/catalog_gen.go
@@ -21,7 +21,7 @@ package catalog
 var defaultDocument = Document{
        Version:              "2.0.0",
        MinimumClientVersion: "2.0.0",
-       Digest:               
"d067890205e30d380d26cf26ba456adba96294f2343fcd6b49e589c42e9f3694",
+       Digest:               
"66ac58f23fbec620f3a0127e901a89f4bcad6c7ee08dd54cbd8f304730ed2199",
        Tools: []Tool{
                {
                        Name:                 "rmq.acl.list",
@@ -58,7 +58,7 @@ var defaultDocument = Document{
                        InputSchema: InputSchema{
                                Fields: []Field{
                                        {Name: "instanceId", Flag: 
"instance-id", Description: "Studio Instance identifier.", Kind: StringField, 
Required: true, MinLength: 1},
-                                       {Name: "id", Flag: "id", Kind: 
StringField, Required: true, MinLength: 1},
+                                       {Name: "id", Flag: "id", Description: 
"Numeric ACL rule id, or the rule's principal on a provider whose rules have no 
numeric id (Tencent Cloud roles); rmq.acl.list reports no id for those rules.", 
Kind: StringField, Required: true, MinLength: 1},
                                },
                        },
                        ViewHint: "object",
@@ -96,7 +96,7 @@ var defaultDocument = Document{
                        InputSchema: InputSchema{
                                Fields: []Field{
                                        {Name: "instanceId", Flag: 
"instance-id", Description: "Studio Instance identifier.", Kind: StringField, 
Required: true, MinLength: 1},
-                                       {Name: "id", Flag: "id", Kind: 
StringField, Required: true, MinLength: 1},
+                                       {Name: "id", Flag: "id", Description: 
"Numeric ACL rule id, or the rule's principal on a provider whose rules have no 
numeric id (Tencent Cloud roles); rmq.acl.list reports no id for those rules.", 
Kind: StringField, Required: true, MinLength: 1},
                                        {Name: "principal", Flag: "principal", 
Kind: StringField},
                                        {Name: "resource", Flag: "resource", 
Kind: StringField},
                                        {Name: "resourceType", Flag: 
"resource-type", Kind: StringField},
@@ -120,7 +120,7 @@ var defaultDocument = Document{
                        InputSchema: InputSchema{
                                Fields: []Field{
                                        {Name: "instanceId", Flag: 
"instance-id", Description: "Studio Instance identifier.", Kind: StringField, 
Required: true, MinLength: 1},
-                                       {Name: "id", Flag: "id", Kind: 
StringField, Required: true, MinLength: 1},
+                                       {Name: "id", Flag: "id", Description: 
"Numeric ACL rule id, or the rule's principal on a provider whose rules have no 
numeric id (Tencent Cloud roles); rmq.acl.list reports no id for those rules.", 
Kind: StringField, Required: true, MinLength: 1},
                                        {Name: "dry_run", Flag: "dry-run", 
Description: "When true, preview the mutation without executing it; omit to 
apply with a confirm_token.", Kind: BooleanField},
                                        {Name: "confirm_token", Flag: 
"confirm-token", Kind: StringField, MinLength: 1},
                                        {Name: "break_glass", Flag: 
"break-glass", Kind: BooleanField},
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java
index 5220136ac..38ce5a4c7 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java
@@ -92,10 +92,6 @@ public enum ToolError {
             "Tool execution failed unexpectedly.",
             "Retry once; if the failure persists, contact an administrator."),
 
-    ACL_ID_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
-            "ACL id must be numeric: %s",
-            "Use the numeric ACL id returned by rmq.acl.list."),
-
     ROLE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
             "Role not found: %s",
             "List the roles for the selected Instance and choose an existing 
role."),
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java
index 5f859357b..a5a62039d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java
@@ -18,7 +18,6 @@ package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
 
 import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
 import org.apache.rocketmq.studio.instance.acl.AclService;
-import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
 import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
 import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
 import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclMutationInput;
@@ -51,8 +50,9 @@ public class AclUpdateToolHandler extends 
MutationToolHandler<AclMutationInput,
     @Override
     public ToolPlan preview(AclMutationInput input, ToolExecutionContext 
context) {
         String instanceId = context.instanceId();
-        AclMutationInput before = 
AclMutationInput.from(aclService.getRule(input.id(), instanceId));
-        AclMutationInput after = 
AclMutationInput.from(input.toRule(parseId(input.id())));
+        AclRuleVO existing = aclService.getRule(input.id(), instanceId);
+        AclMutationInput before = AclMutationInput.from(existing);
+        AclMutationInput after = 
AclMutationInput.from(input.toRule(existing.getId()));
         return PLAN_DESCRIPTION.builder(input.id(), context.instanceId())
                 .before(before)
                 .after(after)
@@ -63,17 +63,8 @@ public class AclUpdateToolHandler extends 
MutationToolHandler<AclMutationInput,
 
     @Override
     public AclRuleVO execute(AclMutationInput input, ToolExecutionContext 
context) {
-        return aclService.updateRule(input.toRule(parseId(input.id())), 
context.instanceId());
-    }
-
-    private static Long parseId(String id) {
-        if (id == null || id.isBlank()) {
-            throw ToolError.ACL_ID_INVALID.exception(id);
-        }
-        try {
-            return Long.parseLong(id);
-        } catch (NumberFormatException e) {
-            throw ToolError.ACL_ID_INVALID.exception(id);
-        }
+        String instanceId = context.instanceId();
+        AclRuleVO existing = aclService.getRule(input.id(), instanceId);
+        return aclService.updateRule(input.toRule(existing.getId()), 
instanceId);
     }
 }
diff --git a/server/src/main/resources/tool-catalog/tools/acl.yaml 
b/server/src/main/resources/tool-catalog/tools/acl.yaml
index d03f9e30d..9f649af3e 100644
--- a/server/src/main/resources/tool-catalog/tools/acl.yaml
+++ b/server/src/main/resources/tool-catalog/tools/acl.yaml
@@ -108,6 +108,9 @@ tools:
           minLength: 1
         id:
           type: string
+          description: >-
+            Numeric ACL rule id, or the rule's principal on a provider whose 
rules have no numeric
+            id (Tencent Cloud roles); rmq.acl.list reports no id for those 
rules.
           minLength: 1
     outputSchema:
       type: object
@@ -255,6 +258,9 @@ tools:
           minLength: 1
         id:
           type: string
+          description: >-
+            Numeric ACL rule id, or the rule's principal on a provider whose 
rules have no numeric
+            id (Tencent Cloud roles); rmq.acl.list reports no id for those 
rules.
           minLength: 1
         principal:
           type: string
@@ -344,6 +350,9 @@ tools:
           minLength: 1
         id:
           type: string
+          description: >-
+            Numeric ACL rule id, or the rule's principal on a provider whose 
rules have no numeric
+            id (Tencent Cloud roles); rmq.acl.list reports no id for those 
rules.
           minLength: 1
         dry_run:
           type: boolean
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandlerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandlerTest.java
index 5c62738bc..621f1d4b2 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandlerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandlerTest.java
@@ -17,8 +17,10 @@
 package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
 
 import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.instance.acl.AclService;
 import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclMutationInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
@@ -27,6 +29,7 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
@@ -51,6 +54,11 @@ class AclUpdateToolHandlerTest {
                 .resource("TopicB")
                 .decision("DENY")
                 .build();
+        when(aclService.getRule("1", 
"cluster-1")).thenReturn(AclRuleVO.builder()
+                .id(1L)
+                .principal("user-1")
+                .resource("TopicA")
+                .build());
         when(aclService.updateRule(any(AclRuleVO.class), eq("cluster-1")))
                 .thenReturn(updated);
 
@@ -67,4 +75,70 @@ class AclUpdateToolHandlerTest {
         assertThat(captor.getValue().getId()).isEqualTo(1L);
     }
 
+    /**
+     * Providers whose ACL rules have no numeric database id are addressed by 
their principal:
+     * AclService.getRule resolves a role name and AclService.updateRule never 
asks Tencent for an
+     * id (TencentAclService.updateRule maps the rule's principal onto 
ModifyRole). The handler must
+     * therefore resolve the addressed rule instead of demanding a number, or 
an ACL rule of such an
+     * instance can never be updated through the tool while get, list and 
delete all work.
+     */
+    @Test
+    void applyShouldUpdateARuleAddressedByPrincipalTest() {
+        AclRuleVO existing = AclRuleVO.builder()
+                .principal("GID_role")
+                .resource("TopicA")
+                .decision("GRANT")
+                .build();
+        AclRuleVO updated = AclRuleVO.builder()
+                .principal("GID_role")
+                .resource("TopicB")
+                .decision("DENY")
+                .build();
+        when(aclService.getRule("GID_role", "cluster-1")).thenReturn(existing);
+        when(aclService.updateRule(any(AclRuleVO.class), eq("cluster-1")))
+                .thenReturn(updated);
+
+        Object result = handler.execute(new AclMutationInput(
+                "cluster-1", "GID_role", "GID_role", "TopicB", null, null,
+                null, "DENY", null), context("cluster-1"));
+
+        assertThat(result).isSameAs(updated);
+        ArgumentCaptor<AclRuleVO> captor = 
ArgumentCaptor.forClass(AclRuleVO.class);
+        verify(aclService).updateRule(captor.capture(), eq("cluster-1"));
+        assertThat(captor.getValue().getId()).isNull();
+        assertThat(captor.getValue().getPrincipal()).isEqualTo("GID_role");
+        assertThat(captor.getValue().getResource()).isEqualTo("TopicB");
+        assertThat(captor.getValue().getDecision()).isEqualTo("DENY");
+    }
+
+    @Test
+    void planShouldPreviewARuleAddressedByPrincipalTest() {
+        AclRuleVO existing = AclRuleVO.builder()
+                .principal("GID_role")
+                .resource("TopicA")
+                .decision("GRANT")
+                .build();
+        when(aclService.getRule("GID_role", "cluster-1")).thenReturn(existing);
+
+        ToolPlan plan = handler.preview(new AclMutationInput(
+                "cluster-1", "GID_role", "GID_role", "TopicB", null, null,
+                null, "DENY", null), context("cluster-1"));
+
+        assertThat(plan.before()).containsEntry("principal", "GID_role");
+        assertThat(plan.after())
+                .containsEntry("principal", "GID_role")
+                .containsEntry("resource", "TopicB");
+    }
+
+    @Test
+    void applyShouldReportAnUnknownRuleFromTheServiceTest() {
+        when(aclService.getRule("999", "cluster-1"))
+                .thenThrow(new BusinessException(404, "ACL rule not found: 
999"));
+
+        assertThatThrownBy(() -> handler.execute(new AclMutationInput(
+                "cluster-1", "999", "user-1", "TopicB", null, null,
+                null, "DENY", null), context("cluster-1")))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("ACL rule not found");
+    }
 }

Reply via email to