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>