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 <[email protected]>
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) => ({

Reply via email to