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();