You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/09/24 10:03:30 UTC

(rocketmq-dashboard) branch rocketmq-studio updated: fix(web): lock the Tencent ACL principal when editing an existing rule (#4782)

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 e81747bb0 fix(web): lock the Tencent ACL principal when editing an existing rule (#4782)
e81747bb0 is described below

commit e81747bb05fb44ef15eb5ea2864bc05b0c561c15
Author: Wang1rrr <15...@users.noreply.github.com>
AuthorDate: Thu Sep 24 18:03:25 2026 +0800

    fix(web): lock the Tencent ACL principal when editing an existing rule (#4782)
    
    fix(acl): preserve the selected Tencent role during rule edits
    
    Tencent updates target the principal, while the editor retains the original row
    ID. Keep that principal fixed for existing Tencent rules so editing A cannot
    retarget A's permissions to B. Creation and local metadata editing stay unchanged.
    
    Constraint: Retain Tencent string-ID DTO compatibility and local edit semantics
    Rejected: Rewrite backend identity handling | unnecessary API compatibility risk
    Confidence: high
    Scope-risk: narrow
    Directive: A disabled Tencent principal must remain in the submitted Form values
    Tested: Red/green real-control regressions; 139 frontend files / 1283 tests; lint, build, format and independent review
    Not-tested: Live Tencent/provider mutation
    Related: https://github.com/apache/rocketmq-dashboard/issues/4780
---
 web/src/pages/instance/__tests__/AclPage.test.tsx | 147 ++++++++++++++++++++++
 web/src/pages/instance/acl.tsx                    |   1 +
 2 files changed, 148 insertions(+)

diff --git a/web/src/pages/instance/__tests__/AclPage.test.tsx b/web/src/pages/instance/__tests__/AclPage.test.tsx
index b8219c964..2888f2645 100644
--- a/web/src/pages/instance/__tests__/AclPage.test.tsx
+++ b/web/src/pages/instance/__tests__/AclPage.test.tsx
@@ -424,6 +424,153 @@ describe('ACL page', () => {
     expect(await within(userRow).findByText('full-secret-key')).toBeInTheDocument();
   });
 
+  it.each(['edit', 'create'] as const)('preservesTencentPrincipal%sTest', async (mode) => {
+    const user = userEvent.setup();
+    const readerRule = {
+      principal: 'reader-role',
+      resource: '*',
+      resourceType: 'Cluster',
+      resourcePattern: 'LITERAL',
+      actions: ['SUB'],
+      decision: 'ALLOW',
+      scope: 'cluster',
+      aclVersion: '1.0',
+      gmtCreate: '2026-07-23T00:00:00Z',
+    } as AclRule;
+    vi.mocked(instanceService.listInstances).mockResolvedValue([
+      {
+        id: 21,
+        name: 'tencent-rmq',
+        type: 'CLOUD',
+        endpoint: 'vpc.tencent:8080',
+        vendor: 'TENCENT',
+        cloudInstanceId: 'rmq-cloud',
+        remark: '',
+        topicCount: 0,
+        consumerGroupCount: 0,
+        gmtCreate: '',
+        gmtModified: '',
+      },
+    ]);
+    vi.mocked(aclService.listAclRules).mockResolvedValue({
+      items: [readerRule, { ...readerRule, principal: 'publisher-role', actions: ['PUB'] }],
+      total: 2,
+      page: 1,
+      size: 20,
+    });
+    vi.mocked(aclService.pageAclUsers).mockResolvedValue({
+      items: ['reader-role', 'publisher-role'].map(
+        (username) =>
+          ({
+            username,
+            admin: false,
+            clusters: ['rmq-cloud'],
+            accessKey: null,
+            secretKey: null,
+          }) as AclUser,
+      ),
+      total: 2,
+      page: 1,
+      size: 20,
+    });
+    vi.mocked(aclService.updateAclRule).mockResolvedValue({ ...readerRule, id: 'reader-role' });
+    vi.mocked(aclService.createAclRule).mockResolvedValue({
+      ...readerRule,
+      id: 'publisher-role',
+      principal: 'publisher-role',
+      actions: ['PUB'],
+    });
+    renderWithProviders(<AclPage />, '/instance/tencent-rmq/acl');
+    await waitFor(() =>
+      expect(screen.getByTestId('acl-local-metadata-notice')).toHaveTextContent(
+        'Tencent Cloud Role',
+      ),
+    );
+    if (mode === 'edit') {
+      const readerRow = await screen.findByRole('row', { name: /reader-role/ });
+      await user.click(within(readerRow).getByRole('button', { name: /编辑/ }));
+    } else {
+      await user.click(screen.getByRole('button', { name: /添加规则/ }));
+    }
+    const dialog = await screen.findByRole('dialog');
+    const principal = within(dialog).getByLabelText('主体');
+    if (mode === 'edit') {
+      expect(principal).toBeDisabled();
+      expect(
+        within(dialog).getByText('reader-role', { selector: '.ant-select-selection-item' }),
+      ).toBeInTheDocument();
+      expect(within(dialog).getByLabelText('订阅 (SUB)')).toBeChecked();
+      await user.click(within(dialog).getByLabelText('发布 (PUB)'));
+      await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+      await waitFor(() => expect(aclService.updateAclRule).toHaveBeenCalledTimes(1));
+      expect(aclService.createAclRule).not.toHaveBeenCalled();
+      expect(aclService.updateAclRule).toHaveBeenCalledWith(
+        expect.objectContaining({
+          id: 'reader-role',
+          principal: 'reader-role',
+          actions: expect.arrayContaining(['SUB', 'PUB']),
+          instanceId: 'tencent-rmq',
+        }),
+      );
+      expect(vi.mocked(aclService.updateAclRule).mock.calls[0][0].actions).toHaveLength(2);
+    } else {
+      expect(principal).toBeEnabled();
+      await user.click(principal);
+      await user.click(
+        await screen.findByText('publisher-role', { selector: '.ant-select-item-option-content' }),
+      );
+      expect(
+        within(dialog).getByText('publisher-role', { selector: '.ant-select-selection-item' }),
+      ).toBeInTheDocument();
+      await user.click(within(dialog).getByRole('button', { name: /添\s*加/ }));
+      await waitFor(() => expect(aclService.createAclRule).toHaveBeenCalledTimes(1));
+      expect(aclService.updateAclRule).not.toHaveBeenCalled();
+      expect(aclService.createAclRule).toHaveBeenCalledWith(
+        expect.objectContaining({
+          principal: 'publisher-role',
+          actions: ['PUB'],
+          instanceId: 'tencent-rmq',
+        }),
+      );
+    }
+  });
+
+  it('keepsLocalMetadataPrincipalEditableTest', async () => {
+    const user = userEvent.setup();
+    vi.mocked(aclService.updateAclRule).mockResolvedValue({
+      id: 1,
+      principal: 'remote-admin',
+      resource: 'remote-topic',
+      resourceType: 'Topic',
+      resourcePattern: 'LITERAL',
+      actions: ['PUB'],
+      decision: 'ALLOW',
+      scope: 'cluster',
+      aclVersion: 2,
+    });
+    renderWithProviders(<AclPage />);
+    const row = await screen.findByRole('row', { name: /remote-user/ });
+    await user.click(within(row).getByRole('button', { name: /编辑/ }));
+    const dialog = await screen.findByRole('dialog');
+    const principal = within(dialog).getByLabelText('主体');
+    expect(principal).toBeEnabled();
+    await user.click(principal);
+    await user.click(
+      await screen.findByText('remote-admin', { selector: '.ant-select-item-option-content' }),
+    );
+    await user.click(within(dialog).getByRole('button', { name: /保\s*存/ }));
+    await waitFor(() => expect(aclService.updateAclRule).toHaveBeenCalledTimes(1));
+    expect(aclService.createAclRule).not.toHaveBeenCalled();
+    expect(aclService.updateAclRule).toHaveBeenCalledWith(
+      expect.objectContaining({
+        id: 1,
+        principal: 'remote-admin',
+        resource: 'remote-topic',
+        actions: ['PUB'],
+      }),
+    );
+  });
+
   it('renders backend users on the user tab', async () => {
     const user = userEvent.setup({ pointerEventsCheck: 0 });
     renderWithProviders(<AclPage />);
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index 161475663..282c598fc 100644
--- a/web/src/pages/instance/acl.tsx
+++ b/web/src/pages/instance/acl.tsx
@@ -1463,6 +1463,7 @@ const AclPageContent = ({
           >
             <Select
               placeholder={t('acl.selectPrincipal')}
+              disabled={tencentRoleMode && !!editingRule}
               showSearch
               optionFilterProp="label"
               options={users.map((u) => ({