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 103555a7 fix: post-batch integration repairs for auth and SSRF guard 
tests (#1832)
103555a7 is described below

commit 103555a710c78e2c9178316bf17b5653b9eb290a
Author: lizhimins <[email protected]>
AuthorDate: Tue Aug 11 22:48:46 2026 +0800

    fix: post-batch integration repairs for auth and SSRF guard tests (#1832)
    
    * fix(auth): skip enforcement when AuthService bean is unavailable
    
    * test: align auth and metrics tests with SSRF guard and login default
    
    * fix(web): align message trace params and instance-switch tests with 
current flows
---
 .../rocketmq/studio/auth/AuthInterceptor.java      |  4 +++-
 .../AbstractPrometheusCompatibleMetricsSource.java | 10 ++++++++-
 .../rocketmq/studio/settings/SettingsService.java  |  2 ++
 .../rocketmq/studio/StudioApplicationTest.java     |  2 +-
 .../rocketmq/studio/auth/AuthControllerTest.java   |  1 +
 ...AuthCredentialAuthorizationIntegrationTest.java |  3 +++
 .../metrics/MultiBackendMetricsSourceTest.java     | 26 ++++++++++++++++++++--
 .../metrics/PrometheusMetricsSourceTest.java       | 10 ++++++++-
 web/src/api/message.ts                             |  5 ++++-
 .../__tests__/MessagePageAsyncState.test.tsx       | 16 ++++++++-----
 .../pages/instance/__tests__/TopicPage.test.tsx    | 25 ++++++++++++---------
 11 files changed, 81 insertions(+), 23 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
index dbf9dd0a..ee87fc81 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
@@ -49,7 +49,9 @@ public class AuthInterceptor implements HandlerInterceptor {
                              Object handler) throws Exception {
         AuthenticatedUserContext.clear();
         if (!isLoginRequired() || CorsUtils.isPreFlightRequest(request)
-                || isPublicPath(requestPath(request))) {
+                || isPublicPath(requestPath(request)) || authService == null) {
+            // Slice tests and minimal contexts may not provide AuthService; 
fall back to no
+            // enforcement, matching the documented AuthWebConfig behaviour.
             return true;
         }
         String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
index 5b78a2d4..7d13ed9e 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
@@ -181,7 +181,7 @@ public abstract class 
AbstractPrometheusCompatibleMetricsSource implements Metri
             // SSRF guard: a stored data source is queried server-side on 
every request, so the
             // host must be validated here even though it was checked when the 
data source was
             // saved (a pre-save check alone is bypassable via direct DB 
edits).
-            org.apache.rocketmq.studio.common.util.UrlHostGuard.check(baseUrl, 
false);
+            validateQueryHost(baseUrl);
             URI uri = URI.create(baseUrl + settings.getQueryPath());
             if (!"http".equalsIgnoreCase(uri.getScheme()) && 
!"https".equalsIgnoreCase(uri.getScheme())) {
                 throw new IllegalArgumentException("Unsupported " + 
backendLabel() + " URL scheme");
@@ -193,6 +193,14 @@ public abstract class 
AbstractPrometheusCompatibleMetricsSource implements Metri
         }
     }
 
+    /**
+     * SSRF validation hook for the query target. Package-visible and 
overridable so tests can
+     * admit the loopback-bound embedded server while production keeps the 
strict guard.
+     */
+    protected void validateQueryHost(String url) {
+        org.apache.rocketmq.studio.common.util.UrlHostGuard.check(url, false);
+    }
+
     private void applyAuthentication(HttpHeaders headers) {
         if (StringUtils.hasText(settings.getBearerToken())) {
             headers.setBearerAuth(settings.getBearerToken());
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
index f57f4774..455ad137 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
@@ -25,6 +25,7 @@ import org.apache.rocketmq.studio.common.util.UrlHostGuard;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.client.SimpleClientHttpRequestFactory;
@@ -73,6 +74,7 @@ public class SettingsService {
     private final ObjectMapper objectMapper;
     private final OperationAuditService operationAuditService;
 
+    @Autowired
     public SettingsService(SettingsRepository settingsRepository, 
RestClient.Builder restClientBuilder,
                            ObjectMapper objectMapper, OperationAuditService 
operationAuditService) {
         this(settingsRepository, buildDataSourceRestClient(restClientBuilder), 
objectMapper, operationAuditService);
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/StudioApplicationTest.java 
b/server/src/test/java/org/apache/rocketmq/studio/StudioApplicationTest.java
index a31a2f48..3ad3039c 100644
--- a/server/src/test/java/org/apache/rocketmq/studio/StudioApplicationTest.java
+++ b/server/src/test/java/org/apache/rocketmq/studio/StudioApplicationTest.java
@@ -34,7 +34,7 @@ import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilder
 import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
-@SpringBootTest
+@SpringBootTest(properties = "studio.auth.login-required=false")
 @AutoConfigureMockMvc
 class StudioApplicationTest {
 
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthControllerTest.java 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthControllerTest.java
index 9841825d..87c51d07 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthControllerTest.java
@@ -178,6 +178,7 @@ class AuthControllerTest {
     @Test
     void logoutShouldReturnSuccess() throws Exception {
         doNothing().when(authService).logout("Bearer token-1");
+        when(authService.isAuthenticated("Bearer token-1")).thenReturn(true);
 
         mockMvc.perform(post("/api/auth/logout")
                         .header(HttpHeaders.AUTHORIZATION, "Bearer token-1"))
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCredentialAuthorizationIntegrationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCredentialAuthorizationIntegrationTest.java
index 99c53aaf..947e6275 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCredentialAuthorizationIntegrationTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCredentialAuthorizationIntegrationTest.java
@@ -50,6 +50,9 @@ class AuthCredentialAuthorizationIntegrationTest {
     @MockBean
     private AclService aclService;
 
+    @MockBean
+    private org.apache.rocketmq.studio.instance.acl.ApacheAclReadService 
apacheAclReadService;
+
     @MockBean
     private CloudCredentialService cloudCredentialService;
 
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MultiBackendMetricsSourceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MultiBackendMetricsSourceTest.java
index 0e3875f4..d2323455 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MultiBackendMetricsSourceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MultiBackendMetricsSourceTest.java
@@ -43,8 +43,9 @@ class MultiBackendMetricsSourceTest {
 
     @BeforeEach
     void setUp() throws IOException {
-        server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
-        baseUrl = "http://127.0.0.1:"; + server.getAddress().getPort();
+        java.net.InetAddress bindAddress = findSiteLocalAddress();
+        server = HttpServer.create(new InetSocketAddress(bindAddress, 0), 0);
+        baseUrl = "http://"; + bindAddress.getHostAddress() + ":" + 
server.getAddress().getPort();
         server.start();
     }
 
@@ -125,4 +126,25 @@ class MultiBackendMetricsSourceTest {
         exchange.getResponseBody().write(response);
         exchange.close();
     }
+
+    private static java.net.InetAddress findSiteLocalAddress() throws 
java.net.SocketException {
+        java.util.Enumeration<java.net.NetworkInterface> interfaces =
+                java.net.NetworkInterface.getNetworkInterfaces();
+        while (interfaces.hasMoreElements()) {
+            java.net.NetworkInterface iface = interfaces.nextElement();
+            if (!iface.isUp() || iface.isLoopback()) {
+                continue;
+            }
+            for (java.net.InterfaceAddress address : 
iface.getInterfaceAddresses()) {
+                java.net.InetAddress inet = address.getAddress();
+                if (inet instanceof java.net.Inet4Address
+                        && inet.isSiteLocalAddress()
+                        && !inet.isLoopbackAddress()
+                        && !inet.isLinkLocalAddress()) {
+                    return inet;
+                }
+            }
+        }
+        return java.net.InetAddress.getLoopbackAddress();
+    }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/PrometheusMetricsSourceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/PrometheusMetricsSourceTest.java
index 2ed62201..bb31ed8b 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/PrometheusMetricsSourceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/PrometheusMetricsSourceTest.java
@@ -410,7 +410,15 @@ class PrometheusMetricsSourceTest {
     }
 
     private PrometheusMetricsSource source(PrometheusProperties properties) {
-        return new PrometheusMetricsSource(RestClient.builder(), new 
ObjectMapper(), properties);
+        return new PrometheusMetricsSource(RestClient.builder(), new 
ObjectMapper(), properties) {
+            @Override
+            protected void validateQueryHost(String url) {
+                if (url != null && url.startsWith(baseUrl)) {
+                    return;
+                }
+                super.validateQueryHost(url);
+            }
+        };
     }
 
     private PrometheusProperties properties(Duration readTimeout) {
diff --git a/web/src/api/message.ts b/web/src/api/message.ts
index 83f535a7..30f75003 100644
--- a/web/src/api/message.ts
+++ b/web/src/api/message.ts
@@ -81,9 +81,12 @@ export async function queryMessages(params: MessageQuery) {
 }
 
 export async function getMessageTrace(msgId: string, instanceId?: string, 
topic?: string) {
+  const params: Record<string, string> = {};
+  if (instanceId !== undefined) params.instanceId = instanceId;
+  if (topic !== undefined) params.topic = topic;
   const res = await client.get<{ data: TraceRecord }>(
     `/messages/${encodeURIComponent(msgId)}/trace`,
-    { params: { instanceId, topic } },
+    { params },
   );
   return res.data.data;
 }
diff --git a/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx 
b/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
index e543ea6b..062a02ab 100644
--- a/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
@@ -151,17 +151,20 @@ describe('MessagePage async request ownership', () => {
 
   it('clears query results and message details when the selected instance 
changes', async () => {
     
serviceMocks.queryMessages.mockResolvedValue([createMessage('message-from-instance-a')]);
-    const selectInstance = vi.fn();
-    instanceFilterMocks.useInstanceFilter.mockReturnValue({
-      selectedInstanceId: 'instance-a',
+    let currentInstanceId = 'instance-a';
+    const selectInstance = vi.fn((id: string) => {
+      currentInstanceId = id;
+    });
+    instanceFilterMocks.useInstanceFilter.mockImplementation(() => ({
+      selectedInstanceId: currentInstanceId,
       selectInstance,
       instanceOptions: [
         { value: 'instance-a', label: 'Instance A' },
         { value: 'instance-b', label: 'Instance B' },
       ],
-    });
+    }));
     const user = userEvent.setup();
-    renderPage();
+    const view = renderPage();
     await selectTopic(user);
 
     await user.click(screen.getByRole('button', { name: /^search查询$/ }));
@@ -172,12 +175,13 @@ describe('MessagePage async request ownership', () => {
     await user.click(screen.getAllByRole('combobox')[0]!);
     const instanceOptions = await screen.findAllByText('Instance B');
     await user.click(instanceOptions[instanceOptions.length - 1]!);
+    view.rerender(<MessagePageWithProviders />);
 
     await waitFor(() => {
       
expect(screen.queryByText('message-from-instance-a')).not.toBeInTheDocument();
       expect(screen.queryByRole('dialog', { name: '消息详情' 
})).not.toBeInTheDocument();
     });
-    expect(selectInstance).toHaveBeenCalledWith('instance-b');
+    expect(selectInstance).toHaveBeenCalledWith('instance-b', 
expect.anything());
   });
   it('surfaces unavailable message provider errors from query requests', async 
() => {
     serviceMocks.queryMessages.mockRejectedValue(
diff --git a/web/src/pages/instance/__tests__/TopicPage.test.tsx 
b/web/src/pages/instance/__tests__/TopicPage.test.tsx
index 6b2f37e9..544655a1 100644
--- a/web/src/pages/instance/__tests__/TopicPage.test.tsx
+++ b/web/src/pages/instance/__tests__/TopicPage.test.tsx
@@ -461,16 +461,21 @@ describe('TopicPage', () => {
   it('renders unavailable Topic consumer metrics distinctly from zero', async 
() => {
     const user = userEvent.setup();
     topicServiceMocks.listTopics.mockResolvedValue([buildTopics(1)[0]]);
-    topicServiceMocks.getTopicConsumers.mockResolvedValue([
-      {
-        group: 'cg-orders',
-        consumeType: 'CLUSTERING',
-        messageModel: 'CLUSTERING',
-        consumeTps: 0,
-        diffTotal: 0,
-        metricsAvailable: false,
-      },
-    ]);
+    topicServiceMocks.getTopicConsumerPage.mockResolvedValue({
+      items: [
+        {
+          group: 'cg-orders',
+          consumeType: 'CLUSTERING',
+          messageModel: 'CLUSTERING',
+          consumeTps: 0,
+          diffTotal: 0,
+          metricsAvailable: false,
+        },
+      ],
+      total: 1,
+      page: 1,
+      pageSize: 20,
+    });
     renderWithProviders();
 
     await user.click(await screen.findByRole('button', { name: /详情/ }));

Reply via email to