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 95a36793c fix(topic): reject the cloud topic updates a vendor cannot 
apply (#6526)
95a36793c is described below

commit 95a36793cf8dae0cdea3539c9c37996f03232c6c
Author: Apulupie <[email protected]>
AuthorDate: Thu Oct 15 18:32:57 2026 +0800

    fix(topic): reject the cloud topic updates a vendor cannot apply (#6526)
    
    Both cloud providers accepted a topic update that asked for properties 
their vendor API
    cannot change and returned the requested values as if they had been applied.
    
    Aliyun's CreateTopicRequest/UpdateTopicRequest carry no queue-count and no 
permission field,
    yet updateTopic sent only the remark and echoed the rest of the incoming 
VO; Tencent's
    ModifyTopicRequest carries the queue count but no permission, and both 
providers report
    every cloud topic as read-write on the read path. The console's edit dialog 
sends the
    topic's current values (zero queues and RW for Aliyun), so a remark-only 
update keeps
    working, while an explicit queue-count or permission change is now rejected 
with 400 the
    way the provider already rejects other operations it cannot perform. The 
create path is
    left unchanged: the console's create dialog always sends its 8/8 defaults.
---
 .../provider/alibaba/AliyunInstanceProvider.java   | 20 ++++++++
 .../provider/tencent/TencentInstanceProvider.java  |  7 +++
 .../alibaba/AliyunInstanceProviderTest.java        | 53 ++++++++++++++++++++++
 .../tencent/TencentInstanceProviderTest.java       | 15 ++++++
 4 files changed, 95 insertions(+)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
index 58db22f71..b2f9c9be4 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
@@ -53,6 +53,7 @@ import org.springframework.util.StringUtils;
 
 import org.apache.rocketmq.studio.common.domain.PageResult;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.common.util.AliyunResponseValidator;
 import org.apache.rocketmq.studio.common.util.Pagination;
@@ -277,6 +278,7 @@ public class AliyunInstanceProvider implements 
InstanceProvider {
         if (topic == null || !StringUtils.hasText(topic.getName())) {
             throw new BusinessException(400, "Topic name is required");
         }
+        requireUpdatableFields(topic);
         UpdateTopicRequest request = UpdateTopicRequest.builder()
                 .instanceId(ctx.cloudInstanceId())
                 .topicName(topic.getName())
@@ -287,6 +289,24 @@ public class AliyunInstanceProvider implements 
InstanceProvider {
         return topic;
     }
 
+    /**
+     * Rejects the topic properties the Aliyun OpenAPI cannot change. Neither 
{@code
+     * CreateTopicRequest} nor {@code UpdateTopicRequest} carries a queue 
count or a permission
+     * field (and reads report the fixed read-write default), so an update 
that asks for either
+     * would otherwise return the requested values as if they had been written 
to the cloud. The
+     * console sends the topic's current values, which are zero queue counts 
and RW.
+     */
+    private static void requireUpdatableFields(TopicVO topic) {
+        if (topic.getWriteQueues() > 0 || topic.getReadQueues() > 0) {
+            throw new BusinessException(400,
+                    "Aliyun RocketMQ does not support changing a topic's queue 
counts");
+        }
+        if (topic.getPerm() != null && topic.getPerm() != TopicPerm.RW) {
+            throw new BusinessException(400,
+                    "Aliyun RocketMQ topics are always read-write; the topic 
permission cannot be changed");
+        }
+    }
+
     @Override
     public void deleteTopic(String instanceId, String topicName) {
         Context ctx = resolve(instanceId);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
index fcd93cd3b..cd363ace1 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
@@ -1189,6 +1189,13 @@ public class TencentInstanceProvider implements 
InstanceProvider {
     private static void validateUpdateTopic(TopicVO topic) {
         validateTopicName(topic);
         validateQueueNumber(topic);
+        // ModifyTopicRequest carries the queue count and no permission field, 
and reads report
+        // every cloud topic as read-write: an update cannot change the 
permission, so it must not
+        // return the requested value as if it had been applied.
+        if (topic.getPerm() != null && topic.getPerm() != TopicPerm.RW) {
+            throw new BusinessException(400,
+                    "Tencent Cloud RocketMQ topics are always read-write; the 
topic permission cannot be changed");
+        }
     }
 
     private static void validateTopicName(TopicVO topic) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
index 88b3551d9..c199c86c6 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
@@ -41,6 +41,8 @@ import 
com.aliyun.sdk.service.rocketmq20220801.models.ListTopicsResponseBody;
 import 
com.aliyun.sdk.service.rocketmq20220801.models.ResetConsumeOffsetRequest;
 import 
com.aliyun.sdk.service.rocketmq20220801.models.ResetConsumeOffsetResponse;
 import 
com.aliyun.sdk.service.rocketmq20220801.models.ResetConsumeOffsetResponseBody;
+import com.aliyun.sdk.service.rocketmq20220801.models.UpdateTopicRequest;
+import com.aliyun.sdk.service.rocketmq20220801.models.UpdateTopicResponse;
 import com.aliyun.sdk.service.rocketmq20220801.models.VerifySendMessageRequest;
 import 
com.aliyun.sdk.service.rocketmq20220801.models.VerifySendMessageResponse;
 import 
com.aliyun.sdk.service.rocketmq20220801.models.VerifySendMessageResponseBody;
@@ -90,6 +92,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
@@ -1172,6 +1175,56 @@ class AliyunInstanceProviderTest {
                 
.isInstanceOf(BusinessException.class).extracting("code").isEqualTo(422);
     }
 
+    @Test
+    void updateTopicShouldRejectQueueCountsTheVendorCannotApplyTest() {
+        stubInstance();
+        TopicVO topic = new TopicVO();
+        topic.setName("orders");
+        topic.setWriteQueues(4);
+        topic.setReadQueues(4);
+
+        assertThatThrownBy(() -> provider.updateTopic(STUDIO_INSTANCE_ID, 
topic))
+                .isInstanceOfSatisfying(BusinessException.class, exception -> {
+                    assertThat(exception.getCode()).isEqualTo(400);
+                    assertThat(exception.getMessage()).contains("queue 
counts");
+                });
+        verifyNoInteractions(clientFactory);
+    }
+
+    @Test
+    void updateTopicShouldRejectAPermissionTheVendorCannotApplyTest() {
+        stubInstance();
+        TopicVO topic = new TopicVO();
+        topic.setName("orders");
+        topic.setPerm(TopicPerm.RO);
+
+        assertThatThrownBy(() -> provider.updateTopic(STUDIO_INSTANCE_ID, 
topic))
+                .isInstanceOfSatisfying(BusinessException.class, exception -> {
+                    assertThat(exception.getCode()).isEqualTo(400);
+                    assertThat(exception.getMessage()).contains("read-write");
+                });
+        verifyNoInteractions(clientFactory);
+    }
+
+    @Test
+    void updateTopicShouldStillSendARemarkOnlyUpdateTest() {
+        stubInstance();
+        stubCallThrough();
+        
when(asyncClient.updateTopic(any(UpdateTopicRequest.class))).thenReturn(CompletableFuture.completedFuture(
+                
UpdateTopicResponse.create().toBuilder().statusCode(200).build()));
+        TopicVO topic = new TopicVO();
+        topic.setName("orders");
+        topic.setRemark("updated remark");
+
+        TopicVO updated = provider.updateTopic(STUDIO_INSTANCE_ID, topic);
+
+        assertThat(updated.getRemark()).isEqualTo("updated remark");
+        ArgumentCaptor<UpdateTopicRequest> captor = 
ArgumentCaptor.forClass(UpdateTopicRequest.class);
+        verify(asyncClient).updateTopic(captor.capture());
+        assertThat(captor.getValue().getTopicName()).isEqualTo("orders");
+        assertThat(captor.getValue().getRemark()).isEqualTo("updated remark");
+    }
+
     private void stubInstance() {
         InstanceVO instance = InstanceVO.builder()
                 .name("aliyun-prod")
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
index fb22003aa..02598f59f 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
@@ -542,6 +542,21 @@ class TencentInstanceProviderTest {
         assertThat(updated.getReadQueues()).isEqualTo(12);
     }
 
+    @Test
+    void updateTopicShouldRejectAPermissionTheVendorCannotApplyTest() {
+        TopicVO topic = new TopicVO();
+        topic.setName("orders");
+        topic.setType(TopicType.NORMAL);
+        topic.setPerm(TopicPerm.RO);
+
+        assertThatThrownBy(() -> provider.updateTopic(STUDIO_INSTANCE_ID, 
topic))
+                .isInstanceOfSatisfying(BusinessException.class, exception -> {
+                    assertThat(exception.getCode()).isEqualTo(400);
+                    assertThat(exception.getMessage()).contains("read-write");
+                });
+        verifyNoInteractions(client);
+    }
+
     @Test
     void getTopicConsumersShouldMapSubscriptionsTest() throws Exception {
         SubscriptionData subscription = new SubscriptionData();

Reply via email to