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: /详情/ }));