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 88b7f3c3b fix(topic): preserve live permissions when updates omit them 
(#7096)
88b7f3c3b is described below

commit 88b7f3c3b0472b41ff882e8df398534925d0a084
Author: Xiao Yang <[email protected]>
AuthorDate: Mon Oct 26 16:48:46 2026 +0800

    fix(topic): preserve live permissions when updates omit them (#7096)
    
    * fix(topic): preserve live permissions when updates omit them
    * docs(api-spec): state the omitted-permission rule in the table's own 
language
    The perm row of POST /api/topics/update was replaced by a full English 
paragraph
    inside an otherwise Chinese table. Same semantics, matching the surrounding 
cells.
---
 docs/api-spec.md                                   |  2 +-
 .../provider/apache/RocketMQAdminClientImpl.java   | 21 +++++--
 .../apache/RocketMQAdminClientImplTest.java        | 73 +++++++++++++++++++++-
 3 files changed, 89 insertions(+), 7 deletions(-)

diff --git a/docs/api-spec.md b/docs/api-spec.md
index 0c5c7ec73..c32bd4d23 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -1042,7 +1042,7 @@ POST /api/topics/update
 | `clusterId` | `string` | 否 | 所属集群;Apache 写路径按实例与所有权记录解析目标集群,不读取该字段 |
 | `writeQueues` | `number` | 否 | 写队列数,须 ≥ 0;缺省或非正数时保留现值 |
 | `readQueues` | `number` | 否 | 读队列数,须 ≥ 0;缺省或非正数时保留现值 |
-| `perm` | `string` | 否 | 权限,默认 `RW` |
+| `perm` | `string` | 否 | 权限。Apache:显式传值即更新权限;省略时各既有 Broker 保留自身运行时的权限掩码(含 
Studio 不可编辑的位)、库内存值不变;新增 Broker 用库内存值兜底,取不到时用 `RW` |
 | `remark` | `string` | 否 | 备注 |
 
 **Response `data`:** `Topic`
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
index 3204b1677..e7fb3378b 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImpl.java
@@ -414,10 +414,14 @@ public class RocketMQAdminClientImpl implements 
AdminClient {
                 // Preflight every broker before any write so a failed read 
cannot erase its
                 // configuration or leave earlier brokers updated. Each broker 
keeps its flags.
                 for (String addr : brokerAddrs.stream().sorted().toList()) {
-                    TopicConfig topicConfig = topicConfigForUpdate(admin, 
addr, topicName);
+                    TopicConfig topicConfig = topicConfigForUpdate(admin, 
addr, topicName, perm);
                     topicConfig.setWriteQueueNums(writeQueues);
                     topicConfig.setReadQueueNums(readQueues);
-                    topicConfig.setPerm(perm);
+                    // A broker may have been made read-only outside Studio. 
An omitted permission
+                    // must keep its live mask, including non-editable bits, 
rather than replay stale metadata.
+                    if (topic.getPerm() != null) {
+                        topicConfig.setPerm(perm);
+                    }
                     // Broker attributes are merged separately and writes 
require delta keys.
                     topicConfig.setAttributes(Map.of());
                     applyTopicType(topicConfig, topic.getType());
@@ -431,7 +435,9 @@ public class RocketMQAdminClientImpl implements AdminClient 
{
                 if (existing != null) {
                     existing.setWriteQueueNums(writeQueues);
                     existing.setReadQueueNums(readQueues);
-                    existing.setPerm(perm);
+                    if (topic.getPerm() != null) {
+                        existing.setPerm(perm);
+                    }
                     if (topic.getType() != null) {
                         existing.setTopicType(topic.getType().name());
                     }
@@ -474,7 +480,7 @@ public class RocketMQAdminClientImpl implements AdminClient 
{
         });
     }
 
-    private TopicConfig topicConfigForUpdate(MQAdminExt admin, String 
brokerAddr, String topicName)
+    private TopicConfig topicConfigForUpdate(MQAdminExt admin, String 
brokerAddr, String topicName, int fallbackPerm)
             throws Exception {
         TopicConfig current;
         try {
@@ -486,7 +492,12 @@ public class RocketMQAdminClientImpl implements 
AdminClient {
             current = null;
         }
         // A newly added master may not hold the topic yet. Preserve update's 
provisioning behavior.
-        return current == null ? new TopicConfig(topicName) : new 
TopicConfig(current);
+        if (current == null) {
+            TopicConfig created = new TopicConfig(topicName);
+            created.setPerm(fallbackPerm);
+            return created;
+        }
+        return new TopicConfig(current);
     }
 
     private void applyTopicType(TopicConfig topicConfig, TopicType topicType) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
index 6a1fd5947..dbe9c512a 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQAdminClientImplTest.java
@@ -1167,6 +1167,76 @@ class RocketMQAdminClientImplTest {
         verify(topicMapper).updateById(existing);
     }
 
+    @ParameterizedTest
+    @CsvSource({"0, 4", "2, 5", "3, 7"})
+    void updateTopicWithoutPermissionPreservesEachBrokersPermissionTest(int 
firstPerm, int secondPerm) throws Exception {
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqTopic.class);
+        
when(adminExt.examineBrokerClusterInfo()).thenReturn(clusterInfoWithTwoMasters());
+        RmqTopic existing = new RmqTopic();
+        existing.setName("orders");
+        existing.setWriteQueueNums(8);
+        existing.setReadQueueNums(8);
+        existing.setPerm(6);
+        when(topicMapper.selectOne(any())).thenReturn(existing);
+        Map<String, Integer> permissions = Map.of("10.0.0.1:10911", firstPerm, 
"10.0.0.2:10911", secondPerm);
+        for (var entry : permissions.entrySet()) {
+            when(adminExt.examineTopicConfig(entry.getKey(), "orders"))
+                    .thenReturn(new TopicConfig("orders", 8, 8, 
entry.getValue()));
+        }
+        TopicVO topic = new TopicVO();
+        topic.setInstanceId("instance-a");
+        topic.setName("orders");
+        topic.setWriteQueues(16);
+        topic.setReadQueues(12);
+        topic.setRemark("updated remark");
+
+        adminClient.updateTopic(topic);
+
+        ArgumentCaptor<String> addresses = 
ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<TopicConfig> configs = 
ArgumentCaptor.forClass(TopicConfig.class);
+        verify(adminExt, 
times(2)).createAndUpdateTopicConfig(addresses.capture(), configs.capture());
+        for (int i = 0; i < addresses.getAllValues().size(); i++) {
+            TopicConfig sent = configs.getAllValues().get(i);
+            
assertThat(sent.getPerm()).isEqualTo(permissions.get(addresses.getAllValues().get(i)));
+            assertThat(sent.getWriteQueueNums()).isEqualTo(16);
+            assertThat(sent.getReadQueueNums()).isEqualTo(12);
+        }
+        assertThat(existing.getPerm()).isEqualTo(6);
+        assertThat(existing.getRemark()).isEqualTo("updated remark");
+        verify(topicMapper).updateById(existing);
+    }
+
+    @ParameterizedTest
+    @CsvSource({"RO, 4", "WO, 2", "RW, 6"})
+    void updateTopicWithPermissionOverridesEachBrokersPermissionTest(TopicPerm 
requested, int expected) throws Exception {
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqTopic.class);
+        
when(adminExt.examineBrokerClusterInfo()).thenReturn(clusterInfoWithTwoMasters());
+        RmqTopic existing = new RmqTopic();
+        existing.setName("orders");
+        existing.setWriteQueueNums(8);
+        existing.setReadQueueNums(8);
+        existing.setPerm(6);
+        when(topicMapper.selectOne(any())).thenReturn(existing);
+        when(adminExt.examineTopicConfig("10.0.0.1:10911", "orders"))
+                .thenReturn(new TopicConfig("orders", 8, 8, 0));
+        when(adminExt.examineTopicConfig("10.0.0.2:10911", "orders"))
+                .thenReturn(new TopicConfig("orders", 8, 8, 5));
+        TopicVO topic = new TopicVO();
+        topic.setInstanceId("instance-a");
+        topic.setName("orders");
+        topic.setWriteQueues(8);
+        topic.setReadQueues(8);
+        topic.setPerm(requested);
+
+        adminClient.updateTopic(topic);
+
+        ArgumentCaptor<TopicConfig> configs = 
ArgumentCaptor.forClass(TopicConfig.class);
+        verify(adminExt, times(2)).createAndUpdateTopicConfig(anyString(), 
configs.capture());
+        assertThat(configs.getAllValues()).allSatisfy(config -> 
assertThat(config.getPerm()).isEqualTo(expected));
+        assertThat(existing.getPerm()).isEqualTo(expected);
+        verify(topicMapper).updateById(existing);
+    }
+
     @Test
     void updateTopicReadsAllConfigurationsBeforeWritingTest() throws Exception 
{
         TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqTopic.class);
@@ -1205,7 +1275,7 @@ class RocketMQAdminClientImplTest {
         existing.setName("orders");
         existing.setWriteQueueNums(8);
         existing.setReadQueueNums(8);
-        existing.setPerm(6);
+        existing.setPerm(4);
         when(topicMapper.selectOne(any())).thenReturn(existing);
         var reads = lenient().when(adminExt.examineTopicConfig(anyString(), 
eq("orders")));
         if (absentException) {
@@ -1225,6 +1295,7 @@ class RocketMQAdminClientImplTest {
         verify(adminExt, times(2)).createAndUpdateTopicConfig(anyString(), 
configs.capture());
         assertThat(configs.getAllValues()).allSatisfy(config -> {
             assertThat(config.getTopicName()).isEqualTo("orders");
+            assertThat(config.getPerm()).isEqualTo(4);
             assertThat(config.getReadQueueNums()).isEqualTo(12);
             assertThat(config.getWriteQueueNums()).isEqualTo(16);
             assertThat(config.getTopicSysFlag()).isZero();

Reply via email to