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>

Reply via email to