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 903fa759 feat: support managing ACL user cluster scope (#741)
903fa759 is described below
commit 903fa759f638385e3f68f25bf962e311e10459d4
Author: yx9o <[email protected]>
AuthorDate: Mon Aug 3 11:17:02 2026 +0800
feat: support managing ACL user cluster scope (#741)
---
web/src/pages/instance/__tests__/AclPage.test.tsx | 66 +++++++++++++++++++++++
web/src/pages/instance/acl.tsx | 13 +++--
2 files changed, 75 insertions(+), 4 deletions(-)
diff --git a/web/src/pages/instance/__tests__/AclPage.test.tsx
b/web/src/pages/instance/__tests__/AclPage.test.tsx
index a28122a0..c2410a7c 100644
--- a/web/src/pages/instance/__tests__/AclPage.test.tsx
+++ b/web/src/pages/instance/__tests__/AclPage.test.tsx
@@ -169,4 +169,70 @@ describe('ACL page', () => {
expect(payload).not.toHaveProperty('accessKey');
expect(payload).not.toHaveProperty('secretKey');
});
+
+ it('creates a user with the selected cluster scope', async () => {
+ const user = userEvent.setup();
+ vi.mocked(aclService.createAclUser).mockResolvedValue({
+ id: 'user-created',
+ username: 'orders-service',
+ accessKey: 'acce****3456',
+ secretKey: 'secr****7654',
+ admin: false,
+ clusters: ['cluster-a', 'cluster-b'],
+ createdAt: '2026-08-01T00:00:00Z',
+ });
+ renderWithProviders(<AclPage />);
+
+ await user.click(await screen.findByText('用户管理'));
+ const userPanel = screen.getByRole('tabpanel', { name: '用户管理' });
+ await user.click(within(userPanel).getByRole('button', { name: /添加用户/ }));
+ const dialog = await screen.findByRole('dialog');
+
+ await user.type(
+ within(dialog).getByPlaceholderText('例:user-order-service'),
+ 'orders-service',
+ );
+ const clusterInput = within(dialog).getByRole('combobox');
+ await user.type(clusterInput, 'cluster-a,cluster-b,');
+ await user.click(within(dialog).getByRole('button', { name: /添\s*加/ }));
+
+ await waitFor(() =>
expect(aclService.createAclUser).toHaveBeenCalledTimes(1));
+ expect(aclService.createAclUser).toHaveBeenCalledWith({
+ username: 'orders-service',
+ admin: false,
+ clusters: ['cluster-a', 'cluster-b'],
+ });
+ });
+
+ it('replaces the cluster scope of an existing 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-b'],
+ createdAt: '2026-07-23T00:00:00Z',
+ });
+ renderWithProviders(<AclPage />);
+
+ await user.click(await screen.findByText('用户管理'));
+ await user.click(screen.getByRole('button', { name: /编辑/ }));
+ const dialog = await screen.findByRole('dialog');
+
+ const clusterInput = within(dialog).getByRole('combobox');
+ await user.click(clusterInput);
+ await user.keyboard('{Backspace}');
+ await user.type(clusterInput, 'cluster-b{enter}');
+ await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+
+ await waitFor(() =>
expect(aclService.updateAclUser).toHaveBeenCalledTimes(1));
+ expect(aclService.updateAclUser).toHaveBeenCalledWith({
+ id: 'user-remote',
+ username: 'remote-admin',
+ admin: true,
+ clusters: ['cluster-b'],
+ });
+ });
});
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index d682d1d5..9417cbd1 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' | 'admin'>;
+type AclUserFormValues = Pick<AclUser, 'username' | 'admin' | 'clusters'>;
const normalizeRule = (rule: AclRule): AclRule => ({
...rule,
@@ -246,7 +246,7 @@ const AclPage = () => {
const openAddUserModal = () => {
setEditingUser(null);
userForm.resetFields();
- userForm.setFieldsValue({ admin: false });
+ userForm.setFieldsValue({ admin: false, clusters: [] });
setUserModalOpen(true);
};
@@ -255,6 +255,7 @@ const AclPage = () => {
userForm.setFieldsValue({
username: user.username,
admin: user.admin,
+ clusters: [...user.clusters],
});
setUserModalOpen(true);
};
@@ -268,7 +269,7 @@ const AclPage = () => {
id: editingUser.id,
username: values.username,
admin: values.admin ?? false,
- clusters: editingUser.clusters,
+ clusters: values.clusters ?? [],
});
const normalized = normalizeUser(updated);
setUsers((prev) => prev.map((u) => (u.id === editingUser.id ?
normalized : u)));
@@ -277,7 +278,7 @@ const AclPage = () => {
const created = await createAclUser({
username: values.username,
admin: values.admin ?? false,
- clusters: ['rmq-cn-v5-prod-01'],
+ clusters: values.clusters ?? [],
});
setUsers((prev) => [normalizeUser(created), ...prev]);
message.success(t('acl.userAdded'));
@@ -842,6 +843,10 @@ const AclPage = () => {
<Form.Item name="admin" label={t('acl.admin')}
valuePropName="checked">
<Switch checkedChildren={t('common.yes')}
unCheckedChildren={t('common.no')} />
</Form.Item>
+
+ <Form.Item name="clusters" label={t('acl.associatedClusters')}>
+ <Select mode="tags" tokenSeparators={[',']} allowClear />
+ </Form.Item>
</Form>
</Modal>
</div>