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 5c3170ea fix: protect ACL user credentials (#569)
5c3170ea is described below

commit 5c3170ea2c1263b2ee224193c384709ba1aa80af
Author: yx9o <[email protected]>
AuthorDate: Tue Jul 28 20:31:24 2026 +0800

    fix: protect ACL user credentials (#569)
---
 docs/api-spec.md                                   |  25 +++-
 .../studio/instance/acl/AclRepository.java         |   3 +
 .../rocketmq/studio/instance/acl/AclService.java   |  48 ++++++-
 .../studio/instance/acl/InMemoryAclRepository.java |   6 +
 .../studio/instance/acl/AclControllerTest.java     |  42 +++++-
 .../studio/instance/acl/AclServiceTest.java        | 154 ++++++++++++++++++++-
 web/src/api/acl.test.ts                            |   2 +-
 web/src/pages/instance/__tests__/AclPage.test.tsx  |  70 +++++++++-
 web/src/pages/instance/acl.tsx                     |  31 ++---
 9 files changed, 339 insertions(+), 42 deletions(-)

diff --git a/docs/api-spec.md b/docs/api-spec.md
index 2fa9602f..782ad796 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -1056,6 +1056,8 @@ GET /api/acl/users
 | `clusters` | `string[]` | 授权集群列表 |
 | `createdAt` | `string` | 创建时间 |
 
+完整的 AccessKey 和 SecretKey 仅在创建用户的响应中返回一次,后续列表查询和更新响应只返回脱敏值。
+
 ### 7.5 创建 ACL 用户
 
 ```
@@ -1067,12 +1069,31 @@ POST /api/acl/users/create
 | 字段 | 类型 | 必填 | 说明 |
 |------|------|------|------|
 | `username` | `string` | 是 | 用户名 |
-| `admin` | `boolean` | 否 | 是否管理员 |
+| `admin` | `boolean` | 否 | 是否管理员,默认 false |
 | `clusters` | `string[]` | 否 | 授权集群 |
 
 **Response `data`:** `AclUser`(含生成的 accessKey/secretKey)
 
-### 7.6 删除 ACL 用户
+### 7.6 更新 ACL 用户
+
+```
+POST /api/acl/users/update
+```
+
+**Request Body:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `id` | `string` | 是 | 用户 ID |
+| `username` | `string` | 否 | 用户名 |
+| `admin` | `boolean` | 是 | 是否管理员 |
+| `clusters` | `string[]` | 否 | 授权集群 |
+
+更新请求不得包含 `accessKey` 或 `secretKey`;服务端会读取原用户并保留已存储凭证。若用户 ID 不存在,返回业务错误 `ACL 
user not found: {id}`。
+
+**Response `data`:** `AclUser`(accessKey/secretKey 为脱敏值)
+
+### 7.7 删除 ACL 用户
 
 ```
 POST /api/acl/users/delete
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
index d7c72fb5..bc1a9514 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
@@ -18,6 +18,7 @@ package org.apache.rocketmq.studio.instance.acl;
 
 
 import java.util.List;
+import java.util.Optional;
 
 public interface AclRepository {
     List<AclRuleVO> findRules(String clusterId, String principal);
@@ -28,6 +29,8 @@ public interface AclRepository {
 
     List<AclUserVO> findUsers();
 
+    Optional<AclUserVO> findUserById(String id);
+
     AclUserVO saveUser(AclUserVO user);
 
     void deleteUser(String id);
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 1013cd2f..d195033a 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
@@ -30,6 +30,10 @@ import java.util.UUID;
 @RequiredArgsConstructor
 public class AclService {
 
+    private static final int VISIBLE_CREDENTIAL_CHARS = 4;
+    private static final int MIN_PARTIALLY_MASKED_CREDENTIAL_CHARS = 17;
+    private static final String CREDENTIAL_MASK = "****";
+
     private final AclRepository aclRepository;
 
 
@@ -65,7 +69,9 @@ public class AclService {
 
     public List<AclUserVO> listUsers() {
         log.info("Listing ACL users");
-        return aclRepository.findUsers();
+        return aclRepository.findUsers().stream()
+                .map(this::maskCredentials)
+                .toList();
     }
 
 
@@ -83,10 +89,18 @@ public class AclService {
             throw new BusinessException(400, "ACL user id is required");
         }
         log.info("Updating ACL user id={}, username={}", user.getId(), 
user.getUsername());
-        if (user.getCreatedAt() == null) {
-            user.setCreatedAt(LocalDateTime.now());
-        }
-        return aclRepository.saveUser(user);
+        AclUserVO existing = aclRepository.findUserById(user.getId())
+                .orElseThrow(() -> new BusinessException(404, "ACL user not 
found: " + user.getId()));
+        AclUserVO merged = AclUserVO.builder()
+                .id(existing.getId())
+                .username(user.getUsername() == null ? existing.getUsername() 
: user.getUsername())
+                .accessKey(existing.getAccessKey())
+                .secretKey(existing.getSecretKey())
+                .admin(user.isAdmin())
+                .clusters(user.getClusters() == null ? existing.getClusters() 
: user.getClusters())
+                .createdAt(existing.getCreatedAt())
+                .build();
+        return maskCredentials(aclRepository.saveUser(merged));
     }
 
     public void deleteUser(String id) {
@@ -97,4 +111,28 @@ public class AclService {
     private boolean isBlank(String value) {
         return value == null || value.trim().isEmpty();
     }
+
+    private AclUserVO maskCredentials(AclUserVO user) {
+        return AclUserVO.builder()
+                .id(user.getId())
+                .username(user.getUsername())
+                .accessKey(maskCredential(user.getAccessKey()))
+                .secretKey(maskCredential(user.getSecretKey()))
+                .admin(user.isAdmin())
+                .clusters(user.getClusters() == null ? null : 
List.copyOf(user.getClusters()))
+                .createdAt(user.getCreatedAt())
+                .build();
+    }
+
+    private String maskCredential(String credential) {
+        if (credential == null || credential.isEmpty()) {
+            return credential;
+        }
+        if (credential.length() < MIN_PARTIALLY_MASKED_CREDENTIAL_CHARS) {
+            return CREDENTIAL_MASK;
+        }
+        return credential.substring(0, VISIBLE_CREDENTIAL_CHARS)
+                + CREDENTIAL_MASK
+                + credential.substring(credential.length() - 
VISIBLE_CREDENTIAL_CHARS);
+    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/InMemoryAclRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/InMemoryAclRepository.java
index 6e894df5..3f1fece9 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/InMemoryAclRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/InMemoryAclRepository.java
@@ -22,6 +22,7 @@ import org.springframework.stereotype.Component;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 
@@ -58,6 +59,11 @@ public class InMemoryAclRepository implements AclRepository {
         return new ArrayList<>(users.values());
     }
 
+    @Override
+    public Optional<AclUserVO> findUserById(String id) {
+        return Optional.ofNullable(users.get(id));
+    }
+
     @Override
     public AclUserVO saveUser(AclUserVO user) {
         users.put(user.getId(), user);
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 8b9ba1a6..fa0d923e 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
@@ -141,8 +141,8 @@ class AclControllerTest {
     void listUsersShouldReturnAllUsers() throws Exception {
         AclUserVO user = AclUserVO.builder()
                 .username("admin")
-                .accessKey("ak123")
-                .secretKey("sk456")
+                .accessKey("acce****3456")
+                .secretKey("secr****7654")
                 .admin(true)
                 .build();
         user.setId("user-1");
@@ -156,20 +156,48 @@ class AclControllerTest {
                 .andExpect(jsonPath("$.data").isArray())
                 .andExpect(jsonPath("$.data[0].id").value("user-1"))
                 .andExpect(jsonPath("$.data[0].username").value("admin"))
+                
.andExpect(jsonPath("$.data[0].accessKey").value("acce****3456"))
+                
.andExpect(jsonPath("$.data[0].secretKey").value("secr****7654"))
                 .andExpect(jsonPath("$.data[0].admin").value(true));
     }
 
     @Test
-    void updateUserShouldReturnUpdatedUser() throws Exception {
+    void createUserShouldReturnGeneratedCredentials() throws Exception {
+        AclUserVO created = AclUserVO.builder()
+                .id("user-1")
+                .username("new-user")
+                .accessKey("access-key-123456")
+                .secretKey("secret-key-987654")
+                .admin(false)
+                .build();
+
+        when(aclService.createUser(any(AclUserVO.class))).thenReturn(created);
+
+        mockMvc.perform(post("/api/acl/users/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"username\":\"new-user\"}"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                
.andExpect(jsonPath("$.data.accessKey").value("access-key-123456"))
+                
.andExpect(jsonPath("$.data.secretKey").value("secret-key-987654"));
+    }
+
+    @Test
+    void updateUserShouldReturnMaskedUpdatedUser() throws Exception {
         AclUserVO input = AclUserVO.builder()
                 .id("user-1")
                 .username("admin")
-                .accessKey("ak123")
-                .secretKey("sk456")
+                .admin(false)
+                .build();
+        AclUserVO updated = AclUserVO.builder()
+                .id("user-1")
+                .username("admin")
+                .accessKey("acce****3456")
+                .secretKey("secr****7654")
                 .admin(false)
                 .build();
 
-        when(aclService.updateUser(any(AclUserVO.class))).thenReturn(input);
+        when(aclService.updateUser(any(AclUserVO.class))).thenReturn(updated);
 
         mockMvc.perform(post("/api/acl/users/update")
                         .contentType(MediaType.APPLICATION_JSON)
@@ -177,6 +205,8 @@ class AclControllerTest {
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.code").value(200))
                 .andExpect(jsonPath("$.data.id").value("user-1"))
+                .andExpect(jsonPath("$.data.accessKey").value("acce****3456"))
+                .andExpect(jsonPath("$.data.secretKey").value("secr****7654"))
                 .andExpect(jsonPath("$.data.admin").value(false));
     }
 }
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 75bfe482..d74b58f9 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
@@ -17,17 +17,26 @@
 
 package org.apache.rocketmq.studio.instance.acl;
 
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentCaptor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
 
 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.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -40,6 +49,20 @@ class AclServiceTest {
     @InjectMocks
     private AclService aclService;
 
+    private AclUserVO existingUser;
+
+    @BeforeEach
+    void setUp() {
+        existingUser = AclUserVO.builder()
+                .id("user-1")
+                .username("orders")
+                .accessKey("access-key-123456")
+                .secretKey("secret-key-987654")
+                .admin(false)
+                .clusters(List.of("cluster-a"))
+                .build();
+    }
+
     @Test
     void listRulesShouldReturnRulesFromRepository() {
         List<AclRuleVO> rules = List.of(
@@ -123,10 +146,20 @@ class AclServiceTest {
     }
 
     @Test
-    void listUsersShouldReturnAllUsers() {
+    void listUsersShouldMaskCredentialsWithoutChangingStoredUsers() {
         List<AclUserVO> users = List.of(
-                AclUserVO.builder().username("admin").admin(true).build(),
-                AclUserVO.builder().username("reader").admin(false).build()
+                AclUserVO.builder()
+                        .username("admin")
+                        .accessKey("access-key-123456")
+                        .secretKey("secret-key-987654")
+                        .admin(true)
+                        .build(),
+                AclUserVO.builder()
+                        .username("reader")
+                        .accessKey("access-key-654321")
+                        .secretKey("secret-key-456789")
+                        .admin(false)
+                        .build()
         );
         when(aclRepository.findUsers()).thenReturn(users);
 
@@ -134,9 +167,57 @@ class AclServiceTest {
 
         assertThat(result).hasSize(2);
         assertThat(result.get(0).getUsername()).isEqualTo("admin");
+        assertThat(result.get(0).getAccessKey()).isEqualTo("acce****3456");
+        assertThat(result.get(0).getSecretKey()).isEqualTo("secr****7654");
+        assertThat(users.get(0).getAccessKey()).isEqualTo("access-key-123456");
+        assertThat(users.get(0).getSecretKey()).isEqualTo("secret-key-987654");
         verify(aclRepository).findUsers();
     }
 
+    @ParameterizedTest
+    @MethodSource("credentialMasks")
+    void listUsersShouldMaskCredentialLengthBoundaries(String credential, 
String expected) {
+        when(aclRepository.findUsers()).thenReturn(List.of(AclUserVO.builder()
+                .username("boundary")
+                .accessKey(credential)
+                .secretKey(credential)
+                .build()));
+
+        AclUserVO result = aclService.listUsers().get(0);
+
+        assertThat(result.getAccessKey()).isEqualTo(expected);
+        assertThat(result.getSecretKey()).isEqualTo(expected);
+    }
+
+    @Test
+    void listUsersShouldCopyClustersWhenMaskingCredentials() {
+        List<String> clusters = new 
java.util.ArrayList<>(List.of("cluster-a"));
+        when(aclRepository.findUsers()).thenReturn(List.of(AclUserVO.builder()
+                .username("admin")
+                .accessKey("access-key-123456")
+                .secretKey("secret-key-987654")
+                .clusters(clusters)
+                .build()));
+
+        AclUserVO result = aclService.listUsers().get(0);
+
+        assertThat(result.getClusters()).containsExactly("cluster-a");
+        assertThatThrownBy(() -> result.getClusters().add("cluster-b"))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThat(clusters).containsExactly("cluster-a");
+    }
+
+    static Stream<Arguments> credentialMasks() {
+        return Stream.of(
+                Arguments.of(null, null),
+                Arguments.of("", ""),
+                Arguments.of("12345678", "****"),
+                Arguments.of("123456789", "****"),
+                Arguments.of("1234567890123456", "****"),
+                Arguments.of("access-key-123456", "acce****3456")
+        );
+    }
+
     @Test
     void createUserShouldGenerateKeysAndSetTimestamp() {
         AclUserVO input = AclUserVO.builder()
@@ -180,18 +261,77 @@ class AclServiceTest {
         AclUserVO input = AclUserVO.builder()
                 .id("user-1")
                 .username("newuser")
-                .accessKey("ak")
-                .secretKey("sk")
+                .accessKey("client-access-key")
+                .secretKey("client-secret-key")
                 .admin(true)
                 .build();
 
+        ArgumentCaptor<AclUserVO> captor = 
ArgumentCaptor.forClass(AclUserVO.class);
+        
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);
 
         assertThat(result.getId()).isEqualTo("user-1");
-        assertThat(result.getCreatedAt()).isNotNull();
+        assertThat(result.getUsername()).isEqualTo("newuser");
+        assertThat(result.getAccessKey()).isEqualTo("acce****3456");
+        assertThat(result.getSecretKey()).isEqualTo("secr****7654");
         assertThat(result.isAdmin()).isTrue();
-        verify(aclRepository).saveUser(any(AclUserVO.class));
+        verify(aclRepository).saveUser(captor.capture());
+        
assertThat(captor.getValue().getAccessKey()).isEqualTo("access-key-123456");
+        
assertThat(captor.getValue().getSecretKey()).isEqualTo("secret-key-987654");
+    }
+
+    @Test
+    void updateUserShouldThrowWhenUserDoesNotExist() {
+        AclUserVO input = AclUserVO.builder()
+                .id("missing")
+                .username("ghost")
+                .build();
+
+        
when(aclRepository.findUserById("missing")).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> aclService.updateUser(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("ACL user not found: missing")
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(404));
+        verify(aclRepository, never()).saveUser(any(AclUserVO.class));
+    }
+
+    @Test
+    void createListUpdateShouldPreserveStoredCredentials() {
+        InMemoryAclRepository repository = new InMemoryAclRepository();
+        AclService service = new AclService(repository);
+        AclUserVO created = service.createUser(AclUserVO.builder()
+                .username("orders")
+                .admin(false)
+                .clusters(List.of("cluster-a"))
+                .build());
+        String accessKey = created.getAccessKey();
+        String secretKey = created.getSecretKey();
+        AclUserVO listed = service.listUsers().get(0);
+
+        AclUserVO updated = service.updateUser(AclUserVO.builder()
+                .id(listed.getId())
+                .username("orders-admin")
+                .accessKey(listed.getAccessKey())
+                .secretKey(listed.getSecretKey())
+                .admin(true)
+                .clusters(listed.getClusters())
+                .build());
+
+        assertThat(listed.getAccessKey()).isNotEqualTo(accessKey);
+        assertThat(listed.getSecretKey()).isNotEqualTo(secretKey);
+        assertThat(updated.getAccessKey()).isEqualTo(mask(accessKey));
+        assertThat(updated.getSecretKey()).isEqualTo(mask(secretKey));
+        AclUserVO stored = 
repository.findUserById(created.getId()).orElseThrow();
+        assertThat(stored.getAccessKey()).isEqualTo(accessKey);
+        assertThat(stored.getSecretKey()).isEqualTo(secretKey);
+        assertThat(stored.getUsername()).isEqualTo("orders-admin");
+        assertThat(stored.isAdmin()).isTrue();
+    }
+
+    private String mask(String credential) {
+        return credential.substring(0, 4) + "****" + 
credential.substring(credential.length() - 4);
     }
 }
diff --git a/web/src/api/acl.test.ts b/web/src/api/acl.test.ts
index 499a6c4c..6188841b 100644
--- a/web/src/api/acl.test.ts
+++ b/web/src/api/acl.test.ts
@@ -107,7 +107,7 @@ describe('ACL API contract', () => {
       return [200, { code: 200, data: rule }];
     });
     mock.onPost('/acl/users/update').reply((config) => {
-      expect(JSON.parse(config.data)).toMatchObject({ id: user.id, admin: true 
});
+      expect(JSON.parse(config.data)).toEqual({ id: user.id, admin: true });
       return [200, { code: 200, data: user }];
     });
     mock.onPost('/acl/rules/delete').reply((config) => {
diff --git a/web/src/pages/instance/__tests__/AclPage.test.tsx 
b/web/src/pages/instance/__tests__/AclPage.test.tsx
index e25de3d0..a28122a0 100644
--- a/web/src/pages/instance/__tests__/AclPage.test.tsx
+++ b/web/src/pages/instance/__tests__/AclPage.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { App } from 'antd';
-import { render, screen } from '@testing-library/react';
+import { render, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import type React from 'react';
 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -60,6 +60,7 @@ const renderWithProviders = (ui: React.ReactElement) =>
 
 describe('ACL page', () => {
   beforeEach(() => {
+    vi.clearAllMocks();
     vi.mocked(aclService.listAclRules).mockResolvedValue([
       {
         id: 'rule-remote',
@@ -78,8 +79,8 @@ describe('ACL page', () => {
       {
         id: 'user-remote',
         username: 'remote-admin',
-        accessKey: 'ak-remote',
-        secretKey: 'sk-remote',
+        accessKey: 'acce****3456',
+        secretKey: 'secr****7654',
         admin: true,
         clusters: ['cluster-a'],
         createdAt: '2026-07-23T00:00:00Z',
@@ -105,4 +106,67 @@ describe('ACL page', () => {
     expect(await screen.findByText('remote-admin')).toBeInTheDocument();
     expect(screen.getByText('cluster-a')).toBeInTheDocument();
   });
+
+  it('does not submit masked credentials when editing a user', async () => {
+    const user = userEvent.setup();
+    vi.mocked(aclService.updateAclUser).mockResolvedValue({
+      id: 'user-remote',
+      username: 'remote-admin',
+      accessKey: 'acce****3456',
+      secretKey: 'secr****7654',
+      admin: true,
+      clusters: ['cluster-a'],
+      createdAt: '2026-07-23T00:00:00Z',
+    });
+    renderWithProviders(<AclPage />);
+
+    await user.click(await screen.findByText('用户管理'));
+    expect(await screen.findByText('remote-admin')).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /编辑/ }));
+    const dialog = await screen.findByRole('dialog');
+
+    expect(within(dialog).queryByText('Access Key')).not.toBeInTheDocument();
+    expect(within(dialog).queryByText('Secret Key')).not.toBeInTheDocument();
+    await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+
+    await waitFor(() => 
expect(aclService.updateAclUser).toHaveBeenCalledTimes(1));
+    const payload = vi.mocked(aclService.updateAclUser).mock.calls[0][0];
+    expect(payload).toEqual({
+      id: 'user-remote',
+      username: 'remote-admin',
+      admin: true,
+      clusters: ['cluster-a'],
+    });
+    expect(payload).not.toHaveProperty('accessKey');
+    expect(payload).not.toHaveProperty('secretKey');
+  });
+
+  it('does not submit masked credentials when toggling admin', async () => {
+    const user = userEvent.setup();
+    vi.mocked(aclService.updateAclUser).mockResolvedValue({
+      id: 'user-remote',
+      username: 'remote-admin',
+      accessKey: 'acce****3456',
+      secretKey: 'secr****7654',
+      admin: false,
+      clusters: ['cluster-a'],
+      createdAt: '2026-07-23T00:00:00Z',
+    });
+    renderWithProviders(<AclPage />);
+
+    await user.click(await screen.findByText('用户管理'));
+    expect(await screen.findByText('remote-admin')).toBeInTheDocument();
+    await user.click(screen.getByRole('switch'));
+
+    await waitFor(() => 
expect(aclService.updateAclUser).toHaveBeenCalledTimes(1));
+    const payload = vi.mocked(aclService.updateAclUser).mock.calls[0][0];
+    expect(payload).toEqual({
+      id: 'user-remote',
+      username: 'remote-admin',
+      admin: false,
+      clusters: ['cluster-a'],
+    });
+    expect(payload).not.toHaveProperty('accessKey');
+    expect(payload).not.toHaveProperty('secretKey');
+  });
 });
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index 97295085..d682d1d5 100644
--- a/web/src/pages/instance/acl.tsx
+++ b/web/src/pages/instance/acl.tsx
@@ -56,7 +56,7 @@ type AclRuleFormValues = Pick<
   AclRule,
   'principal' | 'resource' | 'resourceType' | 'resourcePattern' | 'actions' | 
'decision' | 'scope'
 >;
-type AclUserFormValues = Pick<AclUser, 'username' | 'accessKey' | 'secretKey' 
| 'admin'>;
+type AclUserFormValues = Pick<AclUser, 'username' | 'admin'>;
 
 const normalizeRule = (rule: AclRule): AclRule => ({
   ...rule,
@@ -254,8 +254,6 @@ const AclPage = () => {
     setEditingUser(user);
     userForm.setFieldsValue({
       username: user.username,
-      accessKey: user.accessKey,
-      secretKey: user.secretKey,
       admin: user.admin,
     });
     setUserModalOpen(true);
@@ -266,15 +264,18 @@ const AclPage = () => {
       const values = (await userForm.validateFields()) as AclUserFormValues;
       setUserSubmitting(true);
       if (editingUser) {
-        const updated = await updateAclUser({ ...editingUser, ...values });
+        const updated = await updateAclUser({
+          id: editingUser.id,
+          username: values.username,
+          admin: values.admin ?? false,
+          clusters: editingUser.clusters,
+        });
         const normalized = normalizeUser(updated);
         setUsers((prev) => prev.map((u) => (u.id === editingUser.id ? 
normalized : u)));
         message.success(t('acl.userUpdated'));
       } else {
         const created = await createAclUser({
           username: values.username,
-          accessKey: values.accessKey,
-          secretKey: values.secretKey,
           admin: values.admin ?? false,
           clusters: ['rmq-cn-v5-prod-01'],
         });
@@ -302,7 +303,12 @@ const AclPage = () => {
 
   const handleToggleAdmin = async (user: AclUser, checked: boolean) => {
     try {
-      const updated = await updateAclUser({ ...user, admin: checked });
+      const updated = await updateAclUser({
+        id: user.id,
+        username: user.username,
+        admin: checked,
+        clusters: user.clusters,
+      });
       const normalized = normalizeUser(updated);
       setUsers((prev) => prev.map((u) => (u.id === user.id ? normalized : u)));
       message.success(checked ? t('acl.adminSet') : t('acl.adminRemoved'));
@@ -833,17 +839,6 @@ const AclPage = () => {
             />
           </Form.Item>
 
-          <Form.Item name="accessKey" label="Access Key">
-            <Input placeholder={t('acl.autoOrManual')} style={{ fontFamily: 
'monospace' }} />
-          </Form.Item>
-
-          <Form.Item name="secretKey" label="Secret Key">
-            <Input.Password
-              placeholder={t('acl.autoOrManual')}
-              style={{ fontFamily: 'monospace' }}
-            />
-          </Form.Item>
-
           <Form.Item name="admin" label={t('acl.admin')} 
valuePropName="checked">
             <Switch checkedChildren={t('common.yes')} 
unCheckedChildren={t('common.no')} />
           </Form.Item>

Reply via email to