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 737a7b4d feat(tencent): support ACL role management on Tencent Cloud 
instances (#2048)
737a7b4d is described below

commit 737a7b4d8b6ddf53d4c6dde581e821e6eac66529
Author: Lei Zhiyuan <[email protected]>
AuthorDate: Thu Aug 13 20:14:04 2026 +0800

    feat(tencent): support ACL role management on Tencent Cloud instances 
(#2048)
---
 .../studio/common/domain/DeleteRequestDTO.java     |   3 +
 .../studio/instance/acl/AclController.java         |  30 +-
 .../rocketmq/studio/instance/acl/AclService.java   |  67 ++++-
 .../rocketmq/studio/instance/acl/AclUserVO.java    |   4 +
 .../studio/instance/acl/CreateAclRuleDTO.java      |   2 +
 .../studio/instance/acl/CreateAclUserDTO.java      |   8 +
 .../studio/instance/acl/UpdateAclRuleDTO.java      |   2 +
 .../studio/instance/acl/UpdateAclUserDTO.java      |   8 +
 .../studio/provider/tencent/TencentAclService.java | 327 +++++++++++++++++++++
 ...AuthCredentialAuthorizationIntegrationTest.java |   4 +-
 .../studio/instance/acl/AclControllerTest.java     |  26 +-
 .../studio/instance/acl/AclServiceTest.java        |  56 ++--
 web/src/api/acl.ts                                 |  30 +-
 web/src/pages/instance/__tests__/AclPage.test.tsx  |   4 +
 web/src/pages/instance/acl.tsx                     |  34 ++-
 web/src/services/aclService.ts                     |  33 ++-
 16 files changed, 545 insertions(+), 93 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/domain/DeleteRequestDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/common/domain/DeleteRequestDTO.java
index b01188ba..f5a7ce11 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/common/domain/DeleteRequestDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/domain/DeleteRequestDTO.java
@@ -27,4 +27,7 @@ public class DeleteRequestDTO {
 
     @NotBlank(message = "id is required")
     private String id;
+
+    /** Optional instance id used to route the delete to a cloud-vendor ACL 
backend. */
+    private String instanceId;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
index a08da8a0..949e27df 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
@@ -57,51 +57,57 @@ public class AclController {
     @GetMapping("/rules")
     public Result<List<AclRuleVO>> listRules(
             @RequestParam(required = false) String clusterId,
-            @RequestParam(required = false) String principal) {
-        return Result.ok(aclService.listRules(clusterId, principal));
+            @RequestParam(required = false) String principal,
+            @RequestParam(required = false) String instanceId) {
+        return Result.ok(aclService.listRules(clusterId, principal, 
instanceId));
     }
 
     @PostMapping("/rules/create")
     public Result<AclRuleVO> createRule(@Valid @RequestBody(required = false) 
CreateAclRuleDTO rule) {
-        return Result.ok(aclService.createRule(requireRequest(rule, "ACL rule 
request is required").toAclRuleVO()));
+        CreateAclRuleDTO request = requireRequest(rule, "ACL rule request is 
required");
+        return Result.ok(aclService.createRule(request.toAclRuleVO(), 
request.getInstanceId()));
     }
 
     @PostMapping("/rules/update")
     public Result<AclRuleVO> updateRule(@Valid @RequestBody(required = false) 
UpdateAclRuleDTO rule) {
-        return Result.ok(aclService.updateRule(requireRequest(rule, "ACL rule 
request is required").toAclRuleVO()));
+        UpdateAclRuleDTO request = requireRequest(rule, "ACL rule request is 
required");
+        return Result.ok(aclService.updateRule(request.toAclRuleVO(), 
request.getInstanceId()));
     }
 
     @PostMapping("/rules/delete")
     public Result<Void> deleteRule(@Valid @RequestBody DeleteRequestDTO 
request) {
-        aclService.deleteRule(request.getId());
+        aclService.deleteRule(request.getId(), request.getInstanceId());
         return Result.ok();
     }
 
     @GetMapping("/users")
-    public Result<List<AclUserVO>> listUsers() {
-        return Result.ok(aclService.listUsers());
+    public Result<List<AclUserVO>> listUsers(
+            @RequestParam(required = false) String instanceId) {
+        return Result.ok(aclService.listUsers(instanceId));
     }
 
     @GetMapping("/users/{id}/credentials")
-    public ResponseEntity<Result<AclUserVO>> getUserCredentials(@PathVariable 
String id) {
+    public ResponseEntity<Result<AclUserVO>> getUserCredentials(@PathVariable 
String id,
+            @RequestParam(required = false) String instanceId) {
         return ResponseEntity.ok()
                 .cacheControl(CacheControl.noStore())
-                .body(Result.ok(aclService.getUserCredentials(id)));
+                .body(Result.ok(aclService.getUserCredentials(id, 
instanceId)));
     }
 
     @PostMapping("/users/create")
     public Result<AclUserVO> createUser(@Valid @RequestBody CreateAclUserDTO 
user) {
-        return Result.ok(aclService.createUser(user.toAclUserVO()));
+        return Result.ok(aclService.createUser(user.toAclUserVO(), 
user.getInstanceId()));
     }
 
     @PostMapping("/users/update")
     public Result<AclUserVO> updateUser(@Valid @RequestBody(required = false) 
UpdateAclUserDTO user) {
-        return Result.ok(aclService.updateUser(requireRequest(user, "ACL user 
request is required")));
+        UpdateAclUserDTO request = requireRequest(user, "ACL user request is 
required");
+        return Result.ok(aclService.updateUser(request, 
request.getInstanceId()));
     }
 
     @PostMapping("/users/delete")
     public Result<Void> deleteUser(@Valid @RequestBody DeleteRequestDTO 
request) {
-        aclService.deleteUser(request.getId());
+        aclService.deleteUser(request.getId(), request.getInstanceId());
         return Result.ok();
     }
 
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
index f9b98804..609c9fd7 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
@@ -25,6 +25,7 @@ import org.apache.rocketmq.studio.audit.OperationAuditService;
 import org.apache.rocketmq.studio.model.Acl2PolicyContext;
 import org.apache.rocketmq.studio.instance.InstanceRepository;
 import org.apache.rocketmq.studio.instance.InstanceVO;
+import org.apache.rocketmq.studio.provider.tencent.TencentAclService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -42,6 +43,7 @@ public class AclService {
     private final AclRepository aclRepository;
     private final OperationAuditService operationAuditService;
     private final InstanceRepository instanceRepository;
+    private final TencentAclService tencentAclService;
 
     public AclCapabilitiesVO capabilities(String instanceId) {
         if (!StringUtils.hasText(instanceId)) {
@@ -49,19 +51,29 @@ public class AclService {
         }
         InstanceVO instance = instanceRepository.findByIdentifier(instanceId)
                 .orElseThrow(() -> new BusinessException(404, "Instance not 
found: " + instanceId));
+        if (instance.getVendor() == InstanceVendor.TENCENT) {
+            return new AclCapabilitiesVO(instance.getId(), 
instance.getVendor(), instance.getType(),
+                    "TENCENT_ROLE", true, true);
+        }
         boolean apacheInstance = instance.getVendor() == null || 
instance.getVendor() == InstanceVendor.APACHE;
         return new AclCapabilitiesVO(instance.getId(), instance.getVendor(), 
instance.getType(),
                 apacheInstance ? "APACHE_ACL2" : "STUDIO_LOCAL", 
apacheInstance, false);
     }
 
 
-    public List<AclRuleVO> listRules(String clusterId, String principal) {
+    public List<AclRuleVO> listRules(String clusterId, String principal, 
String instanceId) {
+        if (isTencentInstance(instanceId)) {
+            return tencentAclService.listRules(instanceId, principal);
+        }
         log.info("Listing ACL rules for clusterId={}, principal={}", 
clusterId, principal);
         return aclRepository.findRules(clusterId, principal);
     }
 
 
-    public AclRuleVO createRule(AclRuleVO rule) {
+    public AclRuleVO createRule(AclRuleVO rule, String instanceId) {
+        if (isTencentInstance(instanceId)) {
+            return tencentAclService.createRule(instanceId, rule);
+        }
         log.info("Creating ACL rule for principal={}", rule.getPrincipal());
         if (!StringUtils.hasText(rule.getPrincipal())) {
             throw new BusinessException(400, "ACL principal is required");
@@ -76,7 +88,10 @@ public class AclService {
         return saved;
     }
 
-    public AclRuleVO updateRule(AclRuleVO rule) {
+    public AclRuleVO updateRule(AclRuleVO rule, String instanceId) {
+        if (isTencentInstance(instanceId)) {
+            return tencentAclService.updateRule(instanceId, rule);
+        }
         if (!StringUtils.hasText(rule.getId())) {
             throw new BusinessException(400, "ACL rule id is required");
         }
@@ -87,7 +102,11 @@ public class AclService {
         return saved;
     }
 
-    public void deleteRule(String id) {
+    public void deleteRule(String id, String instanceId) {
+        if (isTencentInstance(instanceId)) {
+            tencentAclService.deleteRule(instanceId, id);
+            return;
+        }
         log.info("Deleting ACL rule id={}", id);
         if (!aclRepository.deleteRule(id)) {
             throw new BusinessException(404, "ACL rule not found: " + id);
@@ -96,7 +115,12 @@ public class AclService {
     }
 
 
-    public List<AclUserVO> listUsers() {
+    public List<AclUserVO> listUsers(String instanceId) {
+        if (isTencentInstance(instanceId)) {
+            return tencentAclService.listUsers(instanceId).stream()
+                    .map(this::maskCredentials)
+                    .toList();
+        }
         log.info("Listing ACL users");
         return aclRepository.findUsers().stream()
                 .map(this::maskCredentials)
@@ -104,7 +128,10 @@ public class AclService {
     }
 
 
-    public AclUserVO createUser(AclUserVO user) {
+    public AclUserVO createUser(AclUserVO user, String instanceId) {
+        if (isTencentInstance(instanceId)) {
+            return tencentAclService.createUser(instanceId, user);
+        }
         log.info("Creating ACL user username={}", user.getUsername());
         if (!StringUtils.hasText(user.getUsername())) {
             throw new BusinessException(400, "ACL username is required");
@@ -118,7 +145,10 @@ public class AclService {
         return saved;
     }
 
-    public AclUserVO updateUser(UpdateAclUserDTO user) {
+    public AclUserVO updateUser(UpdateAclUserDTO user, String instanceId) {
+        if (isTencentInstance(instanceId)) {
+            return tencentAclService.updateUser(instanceId, 
user.toAclUserVO());
+        }
         if (!StringUtils.hasText(user.getId())) {
             throw new BusinessException(400, "ACL user id is required");
         }
@@ -142,7 +172,12 @@ public class AclService {
         return maskCredentials(saved);
     }
 
-    public void deleteUser(String id) {
+    public void deleteUser(String id, String instanceId) {
+        if (isTencentInstance(instanceId)) {
+            // For Tencent roles the id is the role name.
+            tencentAclService.deleteUser(instanceId, id);
+            return;
+        }
         log.info("Deleting ACL user id={}", id);
         if (!aclRepository.deleteUser(id)) {
             throw new BusinessException(404, "ACL user not found: " + id);
@@ -188,7 +223,10 @@ public class AclService {
      * Returns the plain-text credentials of a user for the "view password" 
action.
      * The secret is stored base64-encoded in the database and decoded here.
      */
-    public AclUserVO getUserCredentials(String id) {
+    public AclUserVO getUserCredentials(String id, String instanceId) {
+        if (isTencentInstance(instanceId)) {
+            return tencentAclService.getUserCredentials(instanceId, id);
+        }
         if (!StringUtils.hasText(id)) {
             throw new BusinessException(400, "ACL user id is required");
         }
@@ -234,6 +272,15 @@ public class AclService {
         }
     }
 
+    private boolean isTencentInstance(String instanceId) {
+        if (!StringUtils.hasText(instanceId)) {
+            return false;
+        }
+        return instanceRepository.findByIdentifier(instanceId)
+                .map(instance -> instance.getVendor() == 
InstanceVendor.TENCENT)
+                .orElse(false);
+    }
+
     private boolean isValidAcl2BoundType(String boundType) {
         return switch (boundType.trim().toUpperCase(Locale.ROOT)) {
             case "TOPIC", "GROUP", "*", "USER", "SERVICE_ACCOUNT" -> true;
@@ -248,6 +295,8 @@ public class AclService {
                 .accessKey(CredentialUtils.mask(user.getAccessKey()))
                 .secretKey(CredentialUtils.mask(user.getSecretKey()))
                 .admin(user.isAdmin())
+                .permRead(user.getPermRead())
+                .permWrite(user.getPermWrite())
                 .clusters(user.getClusters() == null ? null : 
List.copyOf(user.getClusters()))
                 .whiteRemoteAddress(user.getWhiteRemoteAddress())
                 .createdAt(user.getCreatedAt())
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclUserVO.java 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclUserVO.java
index 998dbae0..1d7fe1f8 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclUserVO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclUserVO.java
@@ -38,6 +38,10 @@ public class AclUserVO {
     private String secretKey;
     private boolean admin;
     private List<String> clusters;
+    /** Tencent Cloud role read permission (null when not applicable). */
+    private Boolean permRead;
+    /** Tencent Cloud role write permission (null when not applicable). */
+    private Boolean permWrite;
     /** IP whitelist pattern for plain access accounts; empty means no 
restriction. */
     private String whiteRemoteAddress;
     private LocalDateTime createdAt;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclRuleDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclRuleDTO.java
index 9cac8dd8..b9224e24 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclRuleDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclRuleDTO.java
@@ -33,6 +33,8 @@ public class CreateAclRuleDTO {
     private String decision;
     private String scope;
     private String aclVersion;
+    /** Instance id used to route the operation to a cloud-vendor ACL backend. 
*/
+    private String instanceId;
 
     public AclRuleVO toAclRuleVO() {
         return AclRuleVO.builder()
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclUserDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclUserDTO.java
index 6b2d674d..66799bbb 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclUserDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclUserDTO.java
@@ -27,12 +27,20 @@ public class CreateAclUserDTO {
     private String username;
     private boolean admin;
     private List<String> clusters;
+    /** Tencent Cloud role read permission. */
+    private Boolean permRead;
+    /** Tencent Cloud role write permission. */
+    private Boolean permWrite;
+    /** Instance id used to route the operation to a cloud-vendor ACL backend. 
*/
+    private String instanceId;
 
     public AclUserVO toAclUserVO() {
         AclUserVO user = new AclUserVO();
         user.setUsername(username);
         user.setAdmin(admin);
         user.setClusters(clusters);
+        user.setPermRead(permRead);
+        user.setPermWrite(permWrite);
         return user;
     }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclRuleDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclRuleDTO.java
index a7c4862d..6040c996 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclRuleDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclRuleDTO.java
@@ -35,6 +35,8 @@ public class UpdateAclRuleDTO {
     private String decision;
     private String scope;
     private String aclVersion;
+    /** Instance id used to route the operation to a cloud-vendor ACL backend. 
*/
+    private String instanceId;
 
     public AclRuleVO toAclRuleVO() {
         return AclRuleVO.builder()
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclUserDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclUserDTO.java
index 52dd97fb..978fb640 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclUserDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpdateAclUserDTO.java
@@ -32,12 +32,20 @@ public class UpdateAclUserDTO {
      */
     private Boolean admin;
     private List<String> clusters;
+    /** Tencent Cloud role read permission. */
+    private Boolean permRead;
+    /** Tencent Cloud role write permission. */
+    private Boolean permWrite;
+    /** Instance id used to route the operation to a cloud-vendor ACL backend. 
*/
+    private String instanceId;
 
     public AclUserVO toAclUserVO() {
         return AclUserVO.builder()
                 .id(id)
                 .username(username)
                 .admin(admin != null && admin)
+                .permRead(permRead)
+                .permWrite(permWrite)
                 .clusters(clusters)
                 .build();
     }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentAclService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentAclService.java
new file mode 100644
index 00000000..e6235f2e
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentAclService.java
@@ -0,0 +1,327 @@
+/*
+ * 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.rocketmq.studio.provider.tencent;
+
+import com.tencentcloudapi.trocket.v20230308.models.CreateRoleRequest;
+import com.tencentcloudapi.trocket.v20230308.models.DeleteRoleRequest;
+import com.tencentcloudapi.trocket.v20230308.models.DescribeRoleListRequest;
+import com.tencentcloudapi.trocket.v20230308.models.DescribeRoleListResponse;
+import com.tencentcloudapi.trocket.v20230308.models.ModifyRoleRequest;
+import com.tencentcloudapi.trocket.v20230308.models.RoleItem;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.apache.rocketmq.studio.instance.InstanceVO;
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+import org.apache.rocketmq.studio.instance.acl.AclUserVO;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Maps Tencent Cloud TDMQ RocketMQ 5.x role management (DescribeRoleList / 
CreateRole /
+ * ModifyRole / DeleteRole) onto the dashboard's ACL user / rule models.
+ *
+ * <p>Per the product contract, a Tencent role maps to a single cluster-wide 
ACL rule whose
+ * principal is the role name, resource is {@code *}, and actions are derived 
from
+ * {@code PermRead} (SUB) / {@code PermWrite} (PUB). The resulting rule is 
version {@code 1.0}
+ * and scoped to the whole cluster.</p>
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TencentAclService {
+
+    static final int PAGE_SIZE = 100;
+    static final int MAX_PAGES = 100;
+    static final String ACL_VERSION = "1.0";
+    static final String RESOURCE_TYPE = "Cluster";
+    static final String RESOURCE = "*";
+    static final String RESOURCE_PATTERN = "LITERAL";
+    static final String SCOPE = "cluster";
+    static final String DECISION = "ALLOW";
+
+    private final TencentClientFactory clientFactory;
+    private final InstanceRepository instanceRepository;
+
+    public List<AclUserVO> listUsers(String instanceId) {
+        Context context = resolve(instanceId);
+        List<AclUserVO> users = new ArrayList<>();
+        for (int page = 0; page < MAX_PAGES; page++) {
+            DescribeRoleListRequest request = new DescribeRoleListRequest();
+            request.setInstanceId(context.cloudInstanceId());
+            request.setOffset((long) page * PAGE_SIZE);
+            request.setLimit((long) PAGE_SIZE);
+            DescribeRoleListResponse response = 
clientFactory.call(context.credentialId(),
+                    context.regionId(), client -> 
client.DescribeRoleList(request));
+            RoleItem[] data = response == null ? null : response.getData();
+            if (data == null || data.length == 0) {
+                break;
+            }
+            for (RoleItem role : data) {
+                if (role != null) {
+                    users.add(toUser(role, context.cloudInstanceId()));
+                }
+            }
+            if (data.length < PAGE_SIZE) {
+                break;
+            }
+        }
+        return users;
+    }
+
+    public List<AclRuleVO> listRules(String instanceId, String principal) {
+        Context context = resolve(instanceId);
+        List<AclRuleVO> rules = new ArrayList<>();
+        for (int page = 0; page < MAX_PAGES; page++) {
+            DescribeRoleListRequest request = new DescribeRoleListRequest();
+            request.setInstanceId(context.cloudInstanceId());
+            request.setOffset((long) page * PAGE_SIZE);
+            request.setLimit((long) PAGE_SIZE);
+            DescribeRoleListResponse response = 
clientFactory.call(context.credentialId(),
+                    context.regionId(), client -> 
client.DescribeRoleList(request));
+            RoleItem[] data = response == null ? null : response.getData();
+            if (data == null || data.length == 0) {
+                break;
+            }
+            for (RoleItem role : data) {
+                if (role == null) {
+                    continue;
+                }
+                if (StringUtils.hasText(principal)
+                        && !principal.equals(role.getRoleName())) {
+                    continue;
+                }
+                rules.add(toRule(role));
+            }
+            if (data.length < PAGE_SIZE) {
+                break;
+            }
+        }
+        return rules;
+    }
+
+    public AclUserVO createUser(String instanceId, AclUserVO user) {
+        Context context = resolve(instanceId);
+        if (!StringUtils.hasText(user.getUsername())) {
+            throw new BusinessException(400, "ACL username is required");
+        }
+        CreateRoleRequest request = new CreateRoleRequest();
+        request.setInstanceId(context.cloudInstanceId());
+        request.setRole(user.getUsername());
+        request.setPermRead(user.getPermRead() == null || user.getPermRead());
+        request.setPermWrite(user.getPermWrite() == null || 
user.getPermWrite());
+        request.setRemark(user.getUsername());
+        clientFactory.call(context.credentialId(), context.regionId(),
+                client -> client.CreateRole(request));
+        // The created role is not returned by the API; reconstruct from the 
known inputs.
+        return AclUserVO.builder()
+                .id(user.getUsername())
+                .username(user.getUsername())
+                .accessKey(null)
+                .secretKey(null)
+                .admin(false)
+                .permRead(user.getPermRead() == null || user.getPermRead())
+                .permWrite(user.getPermWrite() == null || user.getPermWrite())
+                .clusters(context.cloudInstanceId() == null ? List.of() : 
List.of(context.cloudInstanceId()))
+                .createdAt(LocalDateTime.now())
+                .build();
+    }
+
+    public AclUserVO updateUser(String instanceId, AclUserVO user) {
+        Context context = resolve(instanceId);
+        if (!StringUtils.hasText(user.getUsername())) {
+            throw new BusinessException(400, "ACL username is required");
+        }
+        ModifyRoleRequest request = new ModifyRoleRequest();
+        request.setInstanceId(context.cloudInstanceId());
+        request.setRole(user.getUsername());
+        request.setPermRead(user.getPermRead() == null || user.getPermRead());
+        request.setPermWrite(user.getPermWrite() == null || 
user.getPermWrite());
+        clientFactory.call(context.credentialId(), context.regionId(),
+                client -> client.ModifyRole(request));
+        return AclUserVO.builder()
+                .id(user.getUsername())
+                .username(user.getUsername())
+                .accessKey(null)
+                .secretKey(null)
+                .admin(false)
+                .permRead(user.getPermRead() == null || user.getPermRead())
+                .permWrite(user.getPermWrite() == null || user.getPermWrite())
+                .clusters(context.cloudInstanceId() == null ? List.of() : 
List.of(context.cloudInstanceId()))
+                .createdAt(user.getCreatedAt())
+                .build();
+    }
+
+    public void deleteUser(String instanceId, String username) {
+        Context context = resolve(instanceId);
+        if (!StringUtils.hasText(username)) {
+            throw new BusinessException(400, "ACL username is required");
+        }
+        DeleteRoleRequest request = new DeleteRoleRequest();
+        request.setInstanceId(context.cloudInstanceId());
+        request.setRole(username);
+        clientFactory.call(context.credentialId(), context.regionId(),
+                client -> client.DeleteRole(request));
+    }
+
+    /**
+     * Applies a cluster-wide rule to a Tencent role by translating the rule's 
actions into the
+     * role's PermRead / PermWrite flags. The rule principal is the role name 
and the resource
+     * must be {@code *}; other resource patterns are not representable on 
Tencent roles.
+     */
+    public AclRuleVO createRule(String instanceId, AclRuleVO rule) {
+        Context context = resolve(instanceId);
+        requireRulePrincipal(rule);
+        boolean permRead = hasAction(rule, "SUB");
+        boolean permWrite = hasAction(rule, "PUB");
+        ModifyRoleRequest request = new ModifyRoleRequest();
+        request.setInstanceId(context.cloudInstanceId());
+        request.setRole(rule.getPrincipal());
+        request.setPermRead(permRead);
+        request.setPermWrite(permWrite);
+        clientFactory.call(context.credentialId(), context.regionId(),
+                client -> client.ModifyRole(request));
+        return toRule(rule.getPrincipal(), permRead, permWrite);
+    }
+
+    public AclRuleVO updateRule(String instanceId, AclRuleVO rule) {
+        return createRule(instanceId, rule);
+    }
+
+    public void deleteRule(String instanceId, String principal) {
+        Context context = resolve(instanceId);
+        if (!StringUtils.hasText(principal)) {
+            throw new BusinessException(400, "ACL principal is required");
+        }
+        DeleteRoleRequest request = new DeleteRoleRequest();
+        request.setInstanceId(context.cloudInstanceId());
+        request.setRole(principal);
+        clientFactory.call(context.credentialId(), context.regionId(),
+                client -> client.DeleteRole(request));
+    }
+
+    private static void requireRulePrincipal(AclRuleVO rule) {
+        if (rule == null || !StringUtils.hasText(rule.getPrincipal())) {
+            throw new BusinessException(400, "ACL principal is required");
+        }
+    }
+
+    private static boolean hasAction(AclRuleVO rule, String action) {
+        return rule.getActions() != null && rule.getActions().stream()
+                .anyMatch(a -> action.equalsIgnoreCase(a));
+    }
+
+    private static AclRuleVO toRule(String principal, boolean permRead, 
boolean permWrite) {
+        List<String> actions = new ArrayList<>();
+        if (permWrite) {
+            actions.add("PUB");
+        }
+        if (permRead) {
+            actions.add("SUB");
+        }
+        return AclRuleVO.builder()
+                .id(principal)
+                .principal(principal)
+                .resource(RESOURCE)
+                .resourceType(RESOURCE_TYPE)
+                .resourcePattern(RESOURCE_PATTERN)
+                .actions(actions)
+                .decision(DECISION)
+                .scope(SCOPE)
+                .aclVersion(ACL_VERSION)
+                .build();
+    }
+
+    /**
+     * Returns the plain-text credentials of a Tencent role. The role's 
AccessKey/SecretKey are
+     * available from DescribeRoleList, so re-fetch and match by role name.
+     */
+    public AclUserVO getUserCredentials(String instanceId, String username) {
+        return listUsers(instanceId).stream()
+                .filter(user -> user.getUsername().equals(username))
+                .findFirst()
+                .orElseThrow(() -> new BusinessException(404, "ACL user not 
found: " + username));
+    }
+
+    private Context resolve(String instanceId) {
+        if (!StringUtils.hasText(instanceId)) {
+            throw new BusinessException(400, "instanceId is required");
+        }
+        InstanceVO instance = instanceRepository.findById(instanceId)
+                .orElseThrow(() -> new BusinessException(404, "Instance not 
found: " + instanceId));
+        if (!StringUtils.hasText(instance.getCloudInstanceId()) || 
!StringUtils.hasText(instance.getRegionId())
+                || !StringUtils.hasText(instance.getCredentialId())) {
+            throw new BusinessException(400, "Instance " + instanceId + " is 
missing Tencent Cloud binding");
+        }
+        return new Context(instance.getCloudInstanceId(), 
instance.getRegionId(), instance.getCredentialId());
+    }
+
+    private static AclUserVO toUser(RoleItem role, String cloudInstanceId) {
+        return AclUserVO.builder()
+                .id(role.getRoleName())
+                .username(role.getRoleName())
+                .accessKey(role.getAccessKey())
+                .secretKey(role.getSecretKey())
+                .admin(false)
+                .permRead(role.getPermRead())
+                .permWrite(role.getPermWrite())
+                .clusters(cloudInstanceId == null ? List.of() : 
List.of(cloudInstanceId))
+                .whiteRemoteAddress(null)
+                .createdAt(toLocalDateTime(role.getCreatedTime()))
+                .build();
+    }
+
+    private static AclRuleVO toRule(RoleItem role) {
+        List<String> actions = new ArrayList<>();
+        if (Boolean.TRUE.equals(role.getPermWrite())) {
+            actions.add("PUB");
+        }
+        if (Boolean.TRUE.equals(role.getPermRead())) {
+            actions.add("SUB");
+        }
+        return AclRuleVO.builder()
+                .id(role.getRoleName())
+                .principal(role.getRoleName())
+                .resource(RESOURCE)
+                .resourceType(RESOURCE_TYPE)
+                .resourcePattern(RESOURCE_PATTERN)
+                .actions(actions)
+                .decision(DECISION)
+                .scope(SCOPE)
+                .aclVersion(ACL_VERSION)
+                .createdAt(toLocalDateTime(role.getCreatedTime()))
+                .build();
+    }
+
+    private static LocalDateTime toLocalDateTime(Long epoch) {
+        if (epoch == null || epoch <= 0L) {
+            return null;
+        }
+        long epochMillis = epoch >= 10_000_000_000L ? epoch : epoch * 1000L;
+        return 
Instant.ofEpochMilli(epochMillis).atZone(ZoneId.systemDefault()).toLocalDateTime();
+    }
+
+    private record Context(String cloudInstanceId, String regionId, String 
credentialId) {
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCredentialAuthorizationIntegrationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCredentialAuthorizationIntegrationTest.java
index 947e6275..e0e20373 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCredentialAuthorizationIntegrationTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCredentialAuthorizationIntegrationTest.java
@@ -31,6 +31,8 @@ import org.springframework.context.annotation.Import;
 import org.springframework.http.HttpHeaders;
 import org.springframework.test.web.servlet.MockMvc;
 
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
@@ -101,7 +103,7 @@ class AuthCredentialAuthorizationIntegrationTest {
                         .header(HttpHeaders.AUTHORIZATION, AUTHORIZATION))
                 .andExpect(status().isOk());
 
-        verify(aclService).getUserCredentials("user-1");
+        verify(aclService).getUserCredentials(eq("user-1"), isNull());
         verify(cloudCredentialService).reveal("credential-1");
     }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
index 0f55ca79..47f54a55 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
@@ -102,7 +102,7 @@ class AclControllerTest {
         rule.setId("rule-1");
         rule.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0));
 
-        when(aclService.listRules(isNull(), 
isNull())).thenReturn(List.of(rule));
+        when(aclService.listRules(isNull(), isNull(), 
isNull())).thenReturn(List.of(rule));
 
         mockMvc.perform(get("/api/acl/rules"))
                 .andExpect(status().isOk())
@@ -115,7 +115,7 @@ class AclControllerTest {
 
     @Test
     void listRulesShouldPassQueryParams() throws Exception {
-        when(aclService.listRules(eq("cluster-1"), 
eq("user1"))).thenReturn(List.of());
+        when(aclService.listRules(eq("cluster-1"), eq("user1"), 
isNull())).thenReturn(List.of());
 
         mockMvc.perform(get("/api/acl/rules")
                         .param("clusterId", "cluster-1")
@@ -123,7 +123,7 @@ class AclControllerTest {
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.data").isArray());
 
-        verify(aclService).listRules(eq("cluster-1"), eq("user1"));
+        verify(aclService).listRules(eq("cluster-1"), eq("user1"), isNull());
     }
 
     @Test
@@ -144,7 +144,7 @@ class AclControllerTest {
         created.setId("new-rule-id");
         created.setCreatedAt(LocalDateTime.of(2026, 7, 8, 12, 0));
 
-        when(aclService.createRule(any(AclRuleVO.class))).thenReturn(created);
+        when(aclService.createRule(any(AclRuleVO.class), 
isNull())).thenReturn(created);
 
         mockMvc.perform(post("/api/acl/rules/create")
                         .contentType(MediaType.APPLICATION_JSON)
@@ -177,7 +177,7 @@ class AclControllerTest {
                 .decision("DENY")
                 .build();
 
-        when(aclService.updateRule(any(AclRuleVO.class))).thenReturn(input);
+        when(aclService.updateRule(any(AclRuleVO.class), 
isNull())).thenReturn(input);
 
         mockMvc.perform(post("/api/acl/rules/update")
                         .contentType(MediaType.APPLICATION_JSON)
@@ -196,7 +196,7 @@ class AclControllerTest {
                 .resource("topic-1")
                 .decision("DENY")
                 .build();
-        when(aclService.updateRule(any(AclRuleVO.class)))
+        when(aclService.updateRule(any(AclRuleVO.class), isNull()))
                 .thenThrow(new BusinessException(404, "ACL rule not found: 
missing-rule"));
 
         mockMvc.perform(post("/api/acl/rules/update")
@@ -228,7 +228,7 @@ class AclControllerTest {
                 .andExpect(jsonPath("$.code").value(200))
                 .andExpect(jsonPath("$.message").value("success"));
 
-        verify(aclService).deleteRule("rule-1");
+        verify(aclService).deleteRule(eq("rule-1"), isNull());
     }
 
     @Test
@@ -254,7 +254,7 @@ class AclControllerTest {
         user.setId("user-1");
         user.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0));
 
-        when(aclService.listUsers()).thenReturn(List.of(user));
+        when(aclService.listUsers(isNull())).thenReturn(List.of(user));
 
         mockMvc.perform(get("/api/acl/users"))
                 .andExpect(status().isOk())
@@ -274,7 +274,7 @@ class AclControllerTest {
                 .accessKey("access-key")
                 .secretKey("secret-key")
                 .build();
-        when(aclService.getUserCredentials("user-1")).thenReturn(credentials);
+        when(aclService.getUserCredentials(eq("user-1"), 
isNull())).thenReturn(credentials);
 
         mockMvc.perform(get("/api/acl/users/user-1/credentials"))
                 .andExpect(status().isOk())
@@ -291,7 +291,7 @@ class AclControllerTest {
                 .admin(false)
                 .build();
 
-        when(aclService.createUser(any(AclUserVO.class))).thenReturn(created);
+        when(aclService.createUser(any(AclUserVO.class), 
isNull())).thenReturn(created);
 
         mockMvc.perform(post("/api/acl/users/create")
                         .contentType(MediaType.APPLICATION_JSON)
@@ -308,7 +308,7 @@ class AclControllerTest {
                 
.andExpect(jsonPath("$.data.secretKey").value("secret-key-987654"));
 
         ArgumentCaptor<AclUserVO> captor = 
ArgumentCaptor.forClass(AclUserVO.class);
-        verify(aclService).createUser(captor.capture());
+        verify(aclService).createUser(captor.capture(), isNull());
         assertThat(captor.getValue().getUsername()).isEqualTo("new-user");
         assertThat(captor.getValue().isAdmin()).isTrue();
         
assertThat(captor.getValue().getClusters()).containsExactly("cluster-a");
@@ -344,7 +344,7 @@ class AclControllerTest {
                 .admin(false)
                 .build();
 
-        
when(aclService.updateUser(any(UpdateAclUserDTO.class))).thenReturn(updated);
+        when(aclService.updateUser(any(UpdateAclUserDTO.class), 
isNull())).thenReturn(updated);
 
         mockMvc.perform(post("/api/acl/users/update")
                         .contentType(MediaType.APPLICATION_JSON)
@@ -378,7 +378,7 @@ class AclControllerTest {
                 .andExpect(jsonPath("$.code").value(200))
                 .andExpect(jsonPath("$.message").value("success"));
 
-        verify(aclService).deleteUser("user-1");
+        verify(aclService).deleteUser(eq("user-1"), isNull());
     }
 
     @Test
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
index 1acbb2be..7cd52d48 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
@@ -89,7 +89,7 @@ class AclServiceTest {
         );
         when(aclRepository.findRules("cluster-1", "user1")).thenReturn(rules);
 
-        List<AclRuleVO> result = aclService.listRules("cluster-1", "user1");
+        List<AclRuleVO> result = aclService.listRules("cluster-1", "user1", 
null);
 
         assertThat(result).hasSize(2);
         assertThat(result.get(0).getPrincipal()).isEqualTo("user1");
@@ -129,7 +129,7 @@ class AclServiceTest {
     void listRulesShouldPassNullFilters() {
         when(aclRepository.findRules(null, null)).thenReturn(List.of());
 
-        List<AclRuleVO> result = aclService.listRules(null, null);
+        List<AclRuleVO> result = aclService.listRules(null, null, null);
 
         assertThat(result).isEmpty();
         verify(aclRepository).findRules(null, null);
@@ -146,7 +146,7 @@ class AclServiceTest {
 
         when(aclRepository.saveRule(any(AclRuleVO.class))).thenAnswer(inv -> 
inv.getArgument(0));
 
-        AclRuleVO result = aclService.createRule(input);
+        AclRuleVO result = aclService.createRule(input, null);
 
         assertThat(result.getId()).isNotBlank();
         assertThat(result.getCreatedAt()).isNotNull();
@@ -160,7 +160,7 @@ class AclServiceTest {
     @Test
     void deleteRuleShouldDelegateToRepository() {
         when(aclRepository.deleteRule("rule-1")).thenReturn(true);
-        aclService.deleteRule("rule-1");
+        aclService.deleteRule("rule-1", null);
 
         verify(aclRepository).deleteRule("rule-1");
         verify(operationAuditService).record(eq("DELETE_ACL_RULE"), 
eq("ACL_RULE"), eq("rule-1"), eq(null),
@@ -174,7 +174,7 @@ class AclServiceTest {
                 .resource("topic-1")
                 .build();
 
-        assertThatThrownBy(() -> aclService.updateRule(input))
+        assertThatThrownBy(() -> aclService.updateRule(input, null))
                 .hasMessage("ACL rule id is required");
     }
 
@@ -191,7 +191,7 @@ class AclServiceTest {
 
         when(aclRepository.replaceRule(input)).thenReturn(Optional.of(input));
 
-        AclRuleVO result = aclService.updateRule(input);
+        AclRuleVO result = aclService.updateRule(input, null);
 
         assertThat(result.getId()).isEqualTo("rule-1");
         assertThat(result.getCreatedAt()).isEqualTo(createdAt);
@@ -213,11 +213,11 @@ class AclServiceTest {
                 .decision("DENY")
                 .build();
 
-        assertThatThrownBy(() -> aclService.updateRule(update))
+        assertThatThrownBy(() -> aclService.updateRule(update, null))
                 .isInstanceOf(BusinessException.class)
                 .hasMessage("ACL rule not found: missing-rule")
                 .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(404));
-        assertThat(aclService.listRules(null, null)).isEmpty();
+        assertThat(aclService.listRules(null, null, null)).isEmpty();
         verify(aclRepository, never()).saveRule(any(AclRuleVO.class));
     }
 
@@ -248,7 +248,7 @@ class AclServiceTest {
                 .principal("orders")
                 .resource("orders-topic")
                 .decision("ALLOW")
-                .build());
+                .build(), null);
         LocalDateTime originalCreatedAt = created.getCreatedAt();
 
         LocalDateTime clientCreatedAt = originalCreatedAt.plusDays(1);
@@ -260,7 +260,7 @@ class AclServiceTest {
                 .createdAt(clientCreatedAt)
                 .build();
 
-        AclRuleVO updated = aclService.updateRule(update);
+        AclRuleVO updated = aclService.updateRule(update, null);
 
         assertThat(updated.getCreatedAt()).isEqualTo(originalCreatedAt);
         assertThat(updated.getDecision()).isEqualTo("DENY");
@@ -285,7 +285,7 @@ class AclServiceTest {
         );
         when(aclRepository.findUsers()).thenReturn(users);
 
-        List<AclUserVO> result = aclService.listUsers();
+        List<AclUserVO> result = aclService.listUsers(null);
 
         assertThat(result).hasSize(2);
         assertThat(result.get(0).getUsername()).isEqualTo("admin");
@@ -305,7 +305,7 @@ class AclServiceTest {
                 .secretKey(credential)
                 .build()));
 
-        AclUserVO result = aclService.listUsers().get(0);
+        AclUserVO result = aclService.listUsers(null).get(0);
 
         assertThat(result.getAccessKey()).isEqualTo(expected);
         assertThat(result.getSecretKey()).isEqualTo(expected);
@@ -321,7 +321,7 @@ class AclServiceTest {
                 .clusters(clusters)
                 .build()));
 
-        AclUserVO result = aclService.listUsers().get(0);
+        AclUserVO result = aclService.listUsers(null).get(0);
 
         assertThat(result.getClusters()).containsExactly("cluster-a");
         assertThatThrownBy(() -> result.getClusters().add("cluster-b"))
@@ -349,7 +349,7 @@ class AclServiceTest {
 
         when(aclRepository.saveUser(any(AclUserVO.class))).thenAnswer(inv -> 
inv.getArgument(0));
 
-        AclUserVO result = aclService.createUser(input);
+        AclUserVO result = aclService.createUser(input, null);
 
         assertThat(result.getId()).isNotBlank();
         assertThat(result.getAccessKey()).isNotBlank();
@@ -368,7 +368,7 @@ class AclServiceTest {
     @Test
     void deleteUserShouldDelegateToRepository() {
         when(aclRepository.deleteUser("user-1")).thenReturn(true);
-        aclService.deleteUser("user-1");
+        aclService.deleteUser("user-1", null);
 
         verify(aclRepository).deleteUser("user-1");
         verify(operationAuditService).record(eq("DELETE_ACL_USER"), 
eq("ACL_USER"), eq("user-1"), eq(null),
@@ -379,7 +379,7 @@ class AclServiceTest {
     void deleteRuleShouldRejectUnknownRule() {
         when(aclRepository.deleteRule("missing")).thenReturn(false);
 
-        assertThatThrownBy(() -> aclService.deleteRule("missing"))
+        assertThatThrownBy(() -> aclService.deleteRule("missing", null))
                 .isInstanceOf(BusinessException.class)
                 .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(404));
     }
@@ -388,7 +388,7 @@ class AclServiceTest {
     void deleteUserShouldRejectUnknownUser() {
         when(aclRepository.deleteUser("missing")).thenReturn(false);
 
-        assertThatThrownBy(() -> aclService.deleteUser("missing"))
+        assertThatThrownBy(() -> aclService.deleteUser("missing", null))
                 .isInstanceOf(BusinessException.class)
                 .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(404));
     }
@@ -398,7 +398,7 @@ class AclServiceTest {
         UpdateAclUserDTO input = new UpdateAclUserDTO();
         input.setUsername("newuser");
 
-        assertThatThrownBy(() -> aclService.updateUser(input))
+        assertThatThrownBy(() -> aclService.updateUser(input, null))
                 .hasMessage("ACL user id is required");
     }
 
@@ -413,7 +413,7 @@ class AclServiceTest {
         
when(aclRepository.findUserById("user-1")).thenReturn(Optional.of(existingUser));
         when(aclRepository.saveUser(any(AclUserVO.class))).thenAnswer(inv -> 
inv.getArgument(0));
 
-        AclUserVO result = aclService.updateUser(input);
+        AclUserVO result = aclService.updateUser(input, null);
 
         assertThat(result.getId()).isEqualTo("user-1");
         assertThat(result.getUsername()).isEqualTo("newuser");
@@ -446,7 +446,7 @@ class AclServiceTest {
         
when(aclRepository.findUserById("user-1")).thenReturn(Optional.of(adminUser));
         when(aclRepository.saveUser(any(AclUserVO.class))).thenAnswer(inv -> 
inv.getArgument(0));
 
-        AclUserVO result = aclService.updateUser(input);
+        AclUserVO result = aclService.updateUser(input, null);
 
         assertThat(result.getUsername()).isEqualTo("renamed");
         assertThat(result.isAdmin()).isTrue();
@@ -462,7 +462,7 @@ class AclServiceTest {
 
         
when(aclRepository.findUserById("user-1")).thenReturn(Optional.of(existingUser));
 
-        assertThatThrownBy(() -> aclService.updateUser(input))
+        assertThatThrownBy(() -> aclService.updateUser(input, null))
                 .isInstanceOf(BusinessException.class)
                 .hasMessage("ACL username is required")
                 .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(400));
@@ -477,7 +477,7 @@ class AclServiceTest {
 
         
when(aclRepository.findUserById("missing")).thenReturn(Optional.empty());
 
-        assertThatThrownBy(() -> aclService.updateUser(input))
+        assertThatThrownBy(() -> aclService.updateUser(input, null))
                 .isInstanceOf(BusinessException.class)
                 .hasMessage("ACL user not found: missing")
                 .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(404));
@@ -491,7 +491,7 @@ class AclServiceTest {
                 .resource("topic-1")
                 .build();
 
-        assertThatThrownBy(() -> aclService.createRule(input))
+        assertThatThrownBy(() -> aclService.createRule(input, null))
                 .isInstanceOf(BusinessException.class)
                 .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(400))
                 .hasMessage("ACL principal is required");
@@ -505,7 +505,7 @@ class AclServiceTest {
                 .resource(" ")
                 .build();
 
-        assertThatThrownBy(() -> aclService.createRule(input))
+        assertThatThrownBy(() -> aclService.createRule(input, null))
                 .isInstanceOf(BusinessException.class)
                 .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(400))
                 .hasMessage("ACL resource is required");
@@ -519,7 +519,7 @@ class AclServiceTest {
                 .admin(false)
                 .build();
 
-        assertThatThrownBy(() -> aclService.createUser(input))
+        assertThatThrownBy(() -> aclService.createUser(input, null))
                 .isInstanceOf(BusinessException.class)
                 .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(400))
                 .hasMessage("ACL username is required");
@@ -540,17 +540,17 @@ class AclServiceTest {
                 .username("orders")
                 .admin(false)
                 .clusters(List.of("cluster-a"))
-                .build());
+                .build(), null);
         String accessKey = created.getAccessKey();
         String secretKey = created.getSecretKey();
-        AclUserVO listed = aclService.listUsers().get(0);
+        AclUserVO listed = aclService.listUsers(null).get(0);
 
         UpdateAclUserDTO update = new UpdateAclUserDTO();
         update.setId(listed.getId());
         update.setUsername("orders-admin");
         update.setAdmin(true);
         update.setClusters(listed.getClusters());
-        AclUserVO updated = aclService.updateUser(update);
+        AclUserVO updated = aclService.updateUser(update, null);
 
         assertThat(listed.getAccessKey()).isNotEqualTo(accessKey);
         assertThat(listed.getSecretKey()).isNotEqualTo(secretKey);
diff --git a/web/src/api/acl.ts b/web/src/api/acl.ts
index bbb25a7d..0c1cdfa0 100644
--- a/web/src/api/acl.ts
+++ b/web/src/api/acl.ts
@@ -17,6 +17,13 @@ export interface AclRule {
 export interface AclRuleQuery {
   clusterId?: string;
   principal?: string;
+  instanceId?: string;
+}
+
+// Users list query
+interface AclUserQuery {
+  keyword?: string;
+  instanceId?: string;
 }
 
 export interface AclUser {
@@ -26,6 +33,8 @@ export interface AclUser {
   secretKey: string;
   admin: boolean;
   clusters: string[];
+  permRead?: boolean;
+  permWrite?: boolean;
   createdAt?: string | null;
 }
 
@@ -34,44 +43,45 @@ export async function listAclRules(params?: AclRuleQuery) {
   return res.data.data;
 }
 
-export async function createAclRule(data: Partial<AclRule>) {
+export async function createAclRule(data: Partial<AclRule> & { instanceId?: 
string }) {
   const res = await client.post<{ data: AclRule }>('/acl/rules/create', data);
   return res.data.data;
 }
 
-export async function updateAclRule(data: Partial<AclRule>) {
+export async function updateAclRule(data: Partial<AclRule> & { instanceId?: 
string }) {
   const res = await client.post<{ data: AclRule }>('/acl/rules/update', data);
   return res.data.data;
 }
 
-export async function deleteAclRule(id: string) {
-  await client.post('/acl/rules/delete', { id });
+export async function deleteAclRule(id: string, instanceId?: string) {
+  await client.post('/acl/rules/delete', { id, instanceId });
 }
 
-export async function listAclUsers(params?: { keyword?: string }) {
+export async function listAclUsers(params?: AclUserQuery) {
   const res = await client.get<{ data: AclUser[] }>('/acl/users', { params });
   return res.data.data;
 }
 
-export async function getAclUserCredentials(id: string) {
+export async function getAclUserCredentials(id: string, instanceId?: string) {
   const res = await client.get<{ data: AclUser }>(
     `/acl/users/${encodeURIComponent(id)}/credentials`,
+    { params: { instanceId } },
   );
   return res.data.data;
 }
 
-export async function createAclUser(data: Partial<AclUser>) {
+export async function createAclUser(data: Partial<AclUser> & { instanceId?: 
string }) {
   const res = await client.post<{ data: AclUser }>('/acl/users/create', data);
   return res.data.data;
 }
 
-export async function updateAclUser(data: Partial<AclUser>) {
+export async function updateAclUser(data: Partial<AclUser> & { instanceId?: 
string }) {
   const res = await client.post<{ data: AclUser }>('/acl/users/update', data);
   return res.data.data;
 }
 
-export async function deleteAclUser(id: string) {
-  await client.post('/acl/users/delete', { id });
+export async function deleteAclUser(id: string, instanceId?: string) {
+  await client.post('/acl/users/delete', { id, instanceId });
 }
 
 // ============ ACL 2.0: cluster config & plain access ============
diff --git a/web/src/pages/instance/__tests__/AclPage.test.tsx 
b/web/src/pages/instance/__tests__/AclPage.test.tsx
index 93613964..db39d7a5 100644
--- a/web/src/pages/instance/__tests__/AclPage.test.tsx
+++ b/web/src/pages/instance/__tests__/AclPage.test.tsx
@@ -208,6 +208,7 @@ describe('ACL page', () => {
       username: 'remote-admin',
       admin: true,
       clusters: ['cluster-a'],
+      instanceId: '',
     });
     expect(payload).not.toHaveProperty('accessKey');
     expect(payload).not.toHaveProperty('secretKey');
@@ -252,6 +253,7 @@ describe('ACL page', () => {
       username: 'remote-admin',
       admin: false,
       clusters: ['cluster-a'],
+      instanceId: '',
     });
     expect(payload).not.toHaveProperty('accessKey');
     expect(payload).not.toHaveProperty('secretKey');
@@ -288,6 +290,7 @@ describe('ACL page', () => {
       username: 'orders-service',
       admin: false,
       clusters: ['cluster-a', 'cluster-b'],
+      instanceId: '',
     });
   });
 
@@ -320,6 +323,7 @@ describe('ACL page', () => {
       username: 'remote-admin',
       admin: true,
       clusters: ['cluster-b'],
+      instanceId: '',
     });
   });
 
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index 74151151..65e84ee4 100644
--- a/web/src/pages/instance/acl.tsx
+++ b/web/src/pages/instance/acl.tsx
@@ -105,7 +105,7 @@ const isFormValidationError = (error: unknown) =>
    ═══════════════════════════════════════════ */
 const AclPage = () => {
   const { t } = useLang();
-  const { selectedInstanceId, selectInstance, instanceOptions } = 
useInstanceFilter();
+  const { selectedInstanceId, selectInstance, instanceOptions, instances } = 
useInstanceFilter();
 
   /* ─── State ─── */
   const [rules, setRules] = useState<AclRule[]>([]);
@@ -153,7 +153,7 @@ const AclPage = () => {
   useEffect(() => {
     let mounted = true;
 
-    void listAclRules()
+    void listAclRules({ instanceId: selectedInstanceId })
       .then((nextRules) => {
         if (mounted) setRules(nextRules.map(normalizeRule));
       })
@@ -164,7 +164,7 @@ const AclPage = () => {
         if (mounted) setRulesLoading(false);
       });
 
-    void listAclUsers()
+    void listAclUsers({ instanceId: selectedInstanceId })
       .then((nextUsers) => {
         if (mounted) setUsers(nextUsers.map(normalizeUser));
       })
@@ -178,7 +178,7 @@ const AclPage = () => {
     return () => {
       mounted = false;
     };
-  }, [t]);
+  }, [t, selectedInstanceId]);
 
   /* ─── Filtered rules ─── */
   const filteredRules = rules.filter((r) => {
@@ -239,7 +239,11 @@ const AclPage = () => {
       const values = (await ruleForm.validateFields()) as AclRuleFormValues;
       setRuleSubmitting(true);
       if (editingRule) {
-        const updated = await updateAclRule({ ...editingRule, ...values });
+        const updated = await updateAclRule({
+          ...editingRule,
+          ...values,
+          instanceId: selectedInstanceId,
+        });
         const normalized = normalizeRule(updated);
         setRules((prev) => prev.map((r) => (r.id === editingRule.id ? 
normalized : r)));
         message.success(t('acl.ruleUpdated'));
@@ -247,6 +251,7 @@ const AclPage = () => {
         const created = await createAclRule({
           ...values,
           aclVersion: '2.0',
+          instanceId: selectedInstanceId,
         });
         setRules((prev) => [normalizeRule(created), ...prev]);
         message.success(t('acl.ruleAdded'));
@@ -262,7 +267,7 @@ const AclPage = () => {
 
   const handleDeleteRule = async (id: string) => {
     try {
-      await deleteAclRule(id);
+      await deleteAclRule(id, selectedInstanceId);
       setRules((prev) => prev.filter((r) => r.id !== id));
       message.success(t('acl.ruleDeleted'));
     } catch {
@@ -284,7 +289,7 @@ const AclPage = () => {
     });
     if (!revealing || credentialsByUser[userId]) return;
     try {
-      const credentials = await getAclUserCredentials(userId);
+      const credentials = await getAclUserCredentials(userId, 
selectedInstanceId);
       setCredentialsByUser((prev) => ({
         ...prev,
         [userId]: {
@@ -329,6 +334,7 @@ const AclPage = () => {
           username: values.username,
           admin: values.admin ?? false,
           clusters: values.clusters ?? [],
+          instanceId: selectedInstanceId,
         });
         const normalized = normalizeUser(updated);
         setUsers((prev) => prev.map((u) => (u.id === editingUser.id ? 
normalized : u)));
@@ -338,6 +344,7 @@ const AclPage = () => {
           username: values.username,
           admin: values.admin ?? false,
           clusters: values.clusters ?? [],
+          instanceId: selectedInstanceId,
         });
         setUsers((prev) => [normalizeUser(created), ...prev]);
         message.success(t('acl.userAdded'));
@@ -353,7 +360,7 @@ const AclPage = () => {
 
   const handleDeleteUser = async (id: string) => {
     try {
-      await deleteAclUser(id);
+      await deleteAclUser(id, selectedInstanceId);
       setUsers((prev) => prev.filter((u) => u.id !== id));
       message.success(t('acl.userDeleted'));
     } catch {
@@ -371,6 +378,7 @@ const AclPage = () => {
         username: user.username,
         admin: checked,
         clusters: user.clusters,
+        instanceId: selectedInstanceId,
       });
       const normalized = normalizeUser(updated);
       setUsers((prev) => prev.map((u) => (u.id === user.id ? normalized : u)));
@@ -1222,7 +1230,15 @@ const AclPage = () => {
           </Form.Item>
 
           <Form.Item name="clusters" label={t('acl.associatedClusters')}>
-            <Select mode="tags" tokenSeparators={[',']} allowClear />
+            <Select
+              mode="tags"
+              tokenSeparators={[',']}
+              allowClear
+              options={instances.map((instance) => ({
+                value: instance.cloudInstanceId ?? instance.id,
+                label: instance.name,
+              }))}
+            />
           </Form.Item>
         </Form>
       </Modal>
diff --git a/web/src/services/aclService.ts b/web/src/services/aclService.ts
index 34e85ea7..6163e5d0 100644
--- a/web/src/services/aclService.ts
+++ b/web/src/services/aclService.ts
@@ -49,7 +49,10 @@ export async function listAclRules(params?: AclRuleQuery): 
Promise<AclRule[]> {
   return aclApi.listAclRules(params);
 }
 
-export async function listAclUsers(params?: { keyword?: string }): 
Promise<AclUser[]> {
+export async function listAclUsers(params?: {
+  keyword?: string;
+  instanceId?: string;
+}): Promise<AclUser[]> {
   if (isMockMode()) {
     let result = [...aclUsersState];
     if (params?.keyword) {
@@ -61,16 +64,18 @@ export async function listAclUsers(params?: { keyword?: 
string }): Promise<AclUs
   return aclApi.listAclUsers(params);
 }
 
-export async function getAclUserCredentials(id: string): Promise<AclUser> {
+export async function getAclUserCredentials(id: string, instanceId?: string): 
Promise<AclUser> {
   if (isMockMode()) {
     const user = aclUsersState.find((u) => u.id === id);
     if (!user) throw new Error(`ACL user not found: ${id}`);
     return copyAclUser(user);
   }
-  return aclApi.getAclUserCredentials(id);
+  return aclApi.getAclUserCredentials(id, instanceId);
 }
 
-export async function createAclRule(data: Partial<AclRule>): Promise<AclRule> {
+export async function createAclRule(
+  data: Partial<AclRule> & { instanceId?: string },
+): Promise<AclRule> {
   if (isMockMode()) {
     const rule: AclRule = {
       id: `acl-${Date.now()}`,
@@ -91,7 +96,9 @@ export async function createAclRule(data: Partial<AclRule>): 
Promise<AclRule> {
   return aclApi.createAclRule(data);
 }
 
-export async function updateAclRule(data: Partial<AclRule>): Promise<AclRule> {
+export async function updateAclRule(
+  data: Partial<AclRule> & { instanceId?: string },
+): Promise<AclRule> {
   if (isMockMode()) {
     const idx = aclRulesState.findIndex((rule) => rule.id === data.id);
     if (idx < 0) throw new Error(`ACL rule not found: ${data.id}`);
@@ -105,16 +112,18 @@ export async function updateAclRule(data: 
Partial<AclRule>): Promise<AclRule> {
   return aclApi.updateAclRule(data);
 }
 
-export async function deleteAclRule(id: string): Promise<void> {
+export async function deleteAclRule(id: string, instanceId?: string): 
Promise<void> {
   if (isMockMode()) {
     const idx = aclRulesState.findIndex((rule) => rule.id === id);
     if (idx >= 0) aclRulesState.splice(idx, 1);
     return;
   }
-  return aclApi.deleteAclRule(id);
+  return aclApi.deleteAclRule(id, instanceId);
 }
 
-export async function createAclUser(data: Partial<AclUser>): Promise<AclUser> {
+export async function createAclUser(
+  data: Partial<AclUser> & { instanceId?: string },
+): Promise<AclUser> {
   if (isMockMode()) {
     const user: AclUser = {
       id: `user-${Date.now()}`,
@@ -132,7 +141,9 @@ export async function createAclUser(data: 
Partial<AclUser>): Promise<AclUser> {
   return aclApi.createAclUser(data);
 }
 
-export async function updateAclUser(data: Partial<AclUser>): Promise<AclUser> {
+export async function updateAclUser(
+  data: Partial<AclUser> & { instanceId?: string },
+): Promise<AclUser> {
   if (isMockMode()) {
     const idx = aclUsersState.findIndex((user) => user.id === data.id);
     if (idx < 0) throw new Error(`ACL user not found: ${data.id}`);
@@ -146,13 +157,13 @@ export async function updateAclUser(data: 
Partial<AclUser>): Promise<AclUser> {
   return aclApi.updateAclUser(data);
 }
 
-export async function deleteAclUser(id: string): Promise<void> {
+export async function deleteAclUser(id: string, instanceId?: string): 
Promise<void> {
   if (isMockMode()) {
     const idx = aclUsersState.findIndex((user) => user.id === id);
     if (idx >= 0) aclUsersState.splice(idx, 1);
     return;
   }
-  return aclApi.deleteAclUser(id);
+  return aclApi.deleteAclUser(id, instanceId);
 }
 
 /* ═══════════════════════════════════════════

Reply via email to