From ffdab5337183c8a354fb7ee1b759a057fecb47d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E8=B6=8A?= <1939455790@qq.com> Date: Sat, 11 Jul 2026 12:01:00 +0000 Subject: [PATCH 1/2] [ISSUE #9341] Fix NamespaceV2 not isolating resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When only namespaceV2 is set, topics and consumer/producer groups are not wrapped on the client side, because ClientConfig.getNamespace() — the single value used by withNamespace/withoutNamespace and every resource wrapping call site — only returns the deprecated V1 namespace, which stays empty. namespaceV2 was intended to be resolved server-side via the nsd/ns extension fields attached by NamespaceRpcHook, but the broker never consumes those fields; instead it derives the namespace from already-wrapped resource names (NamespaceUtil.getNamespaceFromResource). With neither side wrapping the resource, producers and consumers configured with different namespaces operate on the same physical topic, so a namespace can consume messages from another namespace. Make getNamespace() fall back to namespaceV2 when the V1 namespace is unset, so that resources are wrapped with the namespaceV2 value and isolated correctly. The V1 namespace still takes precedence when both are set. Signed-off-by: 王越 <1939455790@qq.com> --- .../apache/rocketmq/client/ClientConfig.java | 4 +++ .../rocketmq/client/ClientConfigTest.java | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/client/src/main/java/org/apache/rocketmq/client/ClientConfig.java b/client/src/main/java/org/apache/rocketmq/client/ClientConfig.java index 9e012254329..d7db54e96c7 100644 --- a/client/src/main/java/org/apache/rocketmq/client/ClientConfig.java +++ b/client/src/main/java/org/apache/rocketmq/client/ClientConfig.java @@ -414,6 +414,10 @@ public String getNamespace() { return namespace; } + if (StringUtils.isNotEmpty(namespaceV2)) { + return namespaceV2; + } + if (StringUtils.isNotEmpty(this.namesrvAddr)) { if (NameServerAddressUtils.validateInstanceEndpoint(namesrvAddr)) { namespace = NameServerAddressUtils.parseInstanceIdFromEndpoint(namesrvAddr); diff --git a/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java b/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java index 5afe9cc011d..89bcf7a7d38 100644 --- a/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java @@ -65,6 +65,38 @@ public void testQueuesWithNamespace() { assertEquals("lmq%defaultTopic", messageQueues.iterator().next().getTopic()); } + @Test + public void testGetNamespaceFallbackToNamespaceV2() { + ClientConfig config = new ClientConfig(); + config.setNamespaceV2("InstanceTest"); + assertEquals("InstanceTest", config.getNamespace()); + } + + @Test + public void testWithNamespaceUsesNamespaceV2() { + ClientConfig config = new ClientConfig(); + config.setNamespaceV2("InstanceTest"); + assertEquals("InstanceTest%resource", config.withNamespace(resource)); + assertEquals("InstanceTest%resource", + config.withNamespace("InstanceTest%resource")); + } + + @Test + public void testWithoutNamespaceUsesNamespaceV2() { + ClientConfig config = new ClientConfig(); + config.setNamespaceV2("InstanceTest"); + assertEquals(resource, config.withoutNamespace("InstanceTest%resource")); + } + + @Test + public void testNamespaceV1PrecedesNamespaceV2() { + ClientConfig config = new ClientConfig(); + config.setNamespace("lmq"); + config.setNamespaceV2("InstanceTest"); + assertEquals("lmq", config.getNamespace()); + assertEquals("lmq%resource", config.withNamespace(resource)); + } + private ClientConfig createClientConfig() { ClientConfig result = new ClientConfig(); result.setUnitName("unitName"); From c2c4e866eeab80d34ac3851b92cdd34b1d904ddc Mon Sep 17 00:00:00 2001 From: Yue Wang <1939455790@qq.com> Date: Tue, 21 Jul 2026 12:33:12 +0800 Subject: [PATCH 2/2] [ISSUE #9341] Add cross-namespace isolation regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a regression guard for the cross-namespace isolation symptom reported in #9341: two clients configured with different namespaceV2 values must resolve the same resource to different fully-qualified names, so a consumer in InstanceA cannot accidentally address InstanceB's resources. The test also covers the negative case explicitly — configA.withoutNamespace on a resource resolved under configB's namespace must NOT reduce to the bare resource, confirming the prefix is namespace-pinned and the two clients stay isolated. Requested in #10608 code review. --- .../rocketmq/client/ClientConfigTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java b/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java index 89bcf7a7d38..dcca899846d 100644 --- a/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/ClientConfigTest.java @@ -28,6 +28,7 @@ import java.util.Set; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; @RunWith(MockitoJUnitRunner.class) @@ -97,6 +98,38 @@ public void testNamespaceV1PrecedesNamespaceV2() { assertEquals("lmq%resource", config.withNamespace(resource)); } + // Regression guard for the cross-namespace isolation symptom reported in + // #9341: two clients configured with different namespaceV2 values must + // resolve the same resource to *different* fully-qualified names, so a + // consumer in ns-A cannot accidentally address ns-B's resources. Before + // the fix, namespaceV2 was ignored and both clients would resolve + // "resource" (no namespace prefix at all), collapsing the two namespaces. + @Test + public void testNamespaceV2IsolatesResourcesAcrossNamespaces() { + ClientConfig configA = new ClientConfig(); + configA.setNamespaceV2("InstanceA"); + + ClientConfig configB = new ClientConfig(); + configB.setNamespaceV2("InstanceB"); + + String resolvedA = configA.withNamespace(resource); + String resolvedB = configB.withNamespace(resource); + + // Each client pins its own namespace prefix. + assertEquals("InstanceA%resource", resolvedA); + assertEquals("InstanceB%resource", resolvedB); + + // Negative case: the two resolutions must not collide. If they did, + // a consumer in InstanceA could subscribe to / pull from InstanceB. + assertNotEquals(resolvedA, resolvedB); + + // And each client must reject the other namespace's prefixed resource + // when stripping its own namespace — i.e. configA cannot "claim" a + // resource that belongs to configB. + assertNotEquals(resource, configA.withoutNamespace(resolvedB)); + assertEquals(resource, configA.withoutNamespace(resolvedA)); + } + private ClientConfig createClientConfig() { ClientConfig result = new ClientConfig(); result.setUnitName("unitName");