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) => ({