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 231e9c4d [ISSUE #1511] Restore SettingsService tests after SSRF
hardening (#1512)
231e9c4d is described below
commit 231e9c4dbe1b56436ca1e3661a84b6f45ecfd862
Author: youngkermit8-coder <[email protected]>
AuthorDate: Tue Aug 11 20:19:59 2026 +0800
[ISSUE #1511] Restore SettingsService tests after SSRF hardening (#1512)
Signed-off-by: youngkermit8-coder <[email protected]>
---
.../rocketmq/studio/settings/SettingsService.java | 17 ++-
.../studio/settings/SettingsServiceTest.java | 123 +++++++++------------
2 files changed, 67 insertions(+), 73 deletions(-)
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 80873331..432e3bee 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
@@ -73,15 +73,24 @@ public class SettingsService {
public SettingsService(SettingsRepository settingsRepository,
RestClient.Builder restClientBuilder,
ObjectMapper objectMapper, OperationAuditService
operationAuditService) {
- SimpleClientHttpRequestFactory requestFactory = new
SimpleClientHttpRequestFactory();
- requestFactory.setConnectTimeout(DATA_SOURCE_TEST_CONNECT_TIMEOUT);
- requestFactory.setReadTimeout(DATA_SOURCE_TEST_READ_TIMEOUT);
+ this(settingsRepository, buildDataSourceRestClient(restClientBuilder),
objectMapper, operationAuditService);
+ }
+
+ SettingsService(SettingsRepository settingsRepository, RestClient
restClient,
+ ObjectMapper objectMapper, OperationAuditService
operationAuditService) {
this.settingsRepository = settingsRepository;
- this.restClient =
restClientBuilder.requestFactory(requestFactory).build();
+ this.restClient = restClient;
this.objectMapper = objectMapper;
this.operationAuditService = operationAuditService;
}
+ private static RestClient buildDataSourceRestClient(RestClient.Builder
restClientBuilder) {
+ SimpleClientHttpRequestFactory requestFactory = new
SimpleClientHttpRequestFactory();
+ requestFactory.setConnectTimeout(DATA_SOURCE_TEST_CONNECT_TIMEOUT);
+ requestFactory.setReadTimeout(DATA_SOURCE_TEST_READ_TIMEOUT);
+ return restClientBuilder.requestFactory(requestFactory).build();
+ }
+
public GeneralSettingsVO getGeneralSettings() {
log.debug("Loading general settings");
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
index ee111fcf..1b3d9ba9 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
@@ -17,8 +17,6 @@
package org.apache.rocketmq.studio.settings;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpServer;
import org.apache.rocketmq.studio.audit.OperationAuditService;
import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.junit.jupiter.api.AfterEach;
@@ -27,17 +25,19 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;
-import java.io.IOException;
-import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
-import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
@@ -47,10 +47,20 @@ import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
+import static
org.springframework.test.web.client.match.MockRestRequestMatchers.header;
+import static
org.springframework.test.web.client.match.MockRestRequestMatchers.method;
+import static
org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static
org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
+import static
org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
@ExtendWith(MockitoExtension.class)
class SettingsServiceTest {
+ private static final String PROMETHEUS_BASE_URL = "http://192.0.2.1:9090";
+ private static final String PROMETHEUS_QUERY_URL = PROMETHEUS_BASE_URL +
"/api/v1/query?query=up";
+ private static final String PROMETHEUS_SUCCESS_BODY =
+
"{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}";
+
@Mock
private SettingsRepository settingsRepository;
@@ -59,31 +69,20 @@ class SettingsServiceTest {
private SettingsService settingsService;
- private HttpServer prometheusServer;
- private String prometheusBaseUrl;
+ private MockRestServiceServer prometheusServer;
@BeforeEach
- void setUp() throws IOException {
- prometheusServer = HttpServer.create(new
InetSocketAddress("127.0.0.1", 0), 0);
- prometheusBaseUrl = "http://127.0.0.1:" +
prometheusServer.getAddress().getPort();
- prometheusServer.start();
- settingsService = new SettingsService(settingsRepository,
RestClient.builder(), new ObjectMapper(), operationAuditService) {
- @Override
- boolean isAllowedDataSourceHost(String host) {
- // The embedded test server listens on loopback, which the
production SSRF
- // guard blocks; admit it here and keep the real policy for
every other host.
- if ("127.0.0.1".equals(host)) {
- return true;
- }
- return super.isAllowedDataSourceHost(host);
- }
- };
+ void setUp() {
+ RestClient.Builder restClientBuilder = RestClient.builder();
+ prometheusServer =
MockRestServiceServer.bindTo(restClientBuilder).build();
+ settingsService = new SettingsService(settingsRepository,
restClientBuilder.build(),
+ new ObjectMapper(), operationAuditService);
}
@AfterEach
void tearDown() {
- prometheusServer.stop(0);
+ prometheusServer.verify();
}
@Test
@@ -343,15 +342,11 @@ class SettingsServiceTest {
@Test
void testConnectionShouldQueryPrometheusEndpoint() {
- AtomicReference<String> requestPath = new AtomicReference<>();
- AtomicReference<String> requestQuery = new AtomicReference<>();
- prometheusServer.createContext("/api/v1/query", exchange -> {
- requestPath.set(exchange.getRequestURI().getPath());
- requestQuery.set(exchange.getRequestURI().getRawQuery());
- respond(exchange, 200,
"{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}");
- });
+ prometheusServer.expect(requestTo(PROMETHEUS_QUERY_URL))
+ .andExpect(method(HttpMethod.GET))
+ .andRespond(withSuccess(PROMETHEUS_SUCCESS_BODY,
MediaType.APPLICATION_JSON));
DataSourceTestDTO request = DataSourceTestDTO.builder()
- .url(prometheusBaseUrl)
+ .url(PROMETHEUS_BASE_URL)
.type("Prometheus")
.build();
@@ -359,19 +354,18 @@ class SettingsServiceTest {
assertThat(result.isSuccess()).isTrue();
assertThat(result.getMessage()).isEqualTo("Connection successful");
- assertThat(requestPath.get()).isEqualTo("/api/v1/query");
- assertThat(requestQuery.get()).isEqualTo("query=up");
}
@Test
void testConnectionShouldApplyBasicAuthentication() {
- AtomicReference<String> authorization = new AtomicReference<>();
- prometheusServer.createContext("/api/v1/query", exchange -> {
-
authorization.set(exchange.getRequestHeaders().getFirst("Authorization"));
- respond(exchange, 200,
"{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}");
- });
+ String expectedAuthorization = "Basic "
+ +
Base64.getEncoder().encodeToString("prom:secret".getBytes(StandardCharsets.UTF_8));
+ prometheusServer.expect(requestTo(PROMETHEUS_QUERY_URL))
+ .andExpect(method(HttpMethod.GET))
+ .andExpect(header(HttpHeaders.AUTHORIZATION,
expectedAuthorization))
+ .andRespond(withSuccess(PROMETHEUS_SUCCESS_BODY,
MediaType.APPLICATION_JSON));
DataSourceTestDTO request = DataSourceTestDTO.builder()
- .url(prometheusBaseUrl)
+ .url(PROMETHEUS_BASE_URL)
.type("Prometheus")
.auth("Basic Auth")
.username("prom")
@@ -381,19 +375,18 @@ class SettingsServiceTest {
DataSourceTestResultVO result =
settingsService.testDataSource(request);
assertThat(result.isSuccess()).isTrue();
- assertThat(authorization.get()).isEqualTo("Basic "
- +
Base64.getEncoder().encodeToString("prom:secret".getBytes(StandardCharsets.UTF_8)));
}
@Test
void
testConnectionShouldNormalizeIdentifiersIndependentlyOfDefaultLocale() {
- AtomicReference<String> authorization = new AtomicReference<>();
- prometheusServer.createContext("/api/v1/query", exchange -> {
-
authorization.set(exchange.getRequestHeaders().getFirst("Authorization"));
- respond(exchange, 200,
"{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}");
- });
+ String expectedAuthorization = "Basic "
+ +
Base64.getEncoder().encodeToString("prom:secret".getBytes(StandardCharsets.UTF_8));
+ prometheusServer.expect(requestTo(PROMETHEUS_QUERY_URL))
+ .andExpect(method(HttpMethod.GET))
+ .andExpect(header(HttpHeaders.AUTHORIZATION,
expectedAuthorization))
+ .andRespond(withSuccess(PROMETHEUS_SUCCESS_BODY,
MediaType.APPLICATION_JSON));
DataSourceTestDTO request = DataSourceTestDTO.builder()
- .url(prometheusBaseUrl)
+ .url(PROMETHEUS_BASE_URL)
.type("MIMIR")
.auth("Basic Auth")
.username("prom")
@@ -410,19 +403,16 @@ class SettingsServiceTest {
}
assertThat(result.isSuccess()).isTrue();
- assertThat(authorization.get()).isEqualTo("Basic "
- +
Base64.getEncoder().encodeToString("prom:secret".getBytes(StandardCharsets.UTF_8)));
}
@Test
void testConnectionShouldApplyBearerAuthentication() {
- AtomicReference<String> authorization = new AtomicReference<>();
- prometheusServer.createContext("/api/v1/query", exchange -> {
-
authorization.set(exchange.getRequestHeaders().getFirst("Authorization"));
- respond(exchange, 200,
"{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}");
- });
+ prometheusServer.expect(requestTo(PROMETHEUS_QUERY_URL))
+ .andExpect(method(HttpMethod.GET))
+ .andExpect(header(HttpHeaders.AUTHORIZATION, "Bearer token-1"))
+ .andRespond(withSuccess(PROMETHEUS_SUCCESS_BODY,
MediaType.APPLICATION_JSON));
DataSourceTestDTO request = DataSourceTestDTO.builder()
- .url(prometheusBaseUrl)
+ .url(PROMETHEUS_BASE_URL)
.type("Prometheus")
.auth("Bearer Token")
.bearerToken("token-1")
@@ -431,7 +421,6 @@ class SettingsServiceTest {
DataSourceTestResultVO result =
settingsService.testDataSource(request);
assertThat(result.isSuccess()).isTrue();
- assertThat(authorization.get()).isEqualTo("Bearer token-1");
}
@Test
@@ -503,7 +492,7 @@ class SettingsServiceTest {
@Test
void testConnectionShouldRejectIncompleteBasicAuthentication() {
DataSourceTestResultVO result =
settingsService.testDataSource(DataSourceTestDTO.builder()
- .url(prometheusBaseUrl)
+ .url(PROMETHEUS_BASE_URL)
.type("Prometheus")
.auth("Basic Auth")
.username("prom")
@@ -517,7 +506,7 @@ class SettingsServiceTest {
@Test
void testConnectionShouldRejectMissingBearerToken() {
DataSourceTestResultVO result =
settingsService.testDataSource(DataSourceTestDTO.builder()
- .url(prometheusBaseUrl)
+ .url(PROMETHEUS_BASE_URL)
.type("Prometheus")
.auth("Bearer Token")
.build());
@@ -528,10 +517,13 @@ class SettingsServiceTest {
@Test
void testConnectionShouldReturnPrometheusErrorDetails() {
- prometheusServer.createContext("/api/v1/query", exchange ->
respond(exchange, 422,
-
"{\"status\":\"error\",\"errorType\":\"bad_data\",\"error\":\"invalid
query\"}"));
+ prometheusServer.expect(requestTo(PROMETHEUS_QUERY_URL))
+ .andExpect(method(HttpMethod.GET))
+ .andRespond(withStatus(HttpStatus.UNPROCESSABLE_ENTITY)
+ .contentType(MediaType.APPLICATION_JSON)
+
.body("{\"status\":\"error\",\"errorType\":\"bad_data\",\"error\":\"invalid
query\"}"));
DataSourceTestDTO request = DataSourceTestDTO.builder()
- .url(prometheusBaseUrl)
+ .url(PROMETHEUS_BASE_URL)
.type("VictoriaMetrics")
.build();
@@ -557,7 +549,7 @@ class SettingsServiceTest {
@Test
void testConnectionShouldRejectUnsupportedType() {
DataSourceTestResultVO result =
settingsService.testDataSource(DataSourceTestDTO.builder()
- .url(prometheusBaseUrl)
+ .url(PROMETHEUS_BASE_URL)
.type("rocketmq")
.build());
@@ -565,11 +557,4 @@ class SettingsServiceTest {
assertThat(result.getMessage()).isEqualTo("Unsupported data source
type: rocketmq");
}
- private void respond(HttpExchange exchange, int statusCode, String body)
throws IOException {
- byte[] response = body.getBytes(StandardCharsets.UTF_8);
- exchange.getResponseHeaders().set("Content-Type", "application/json");
- exchange.sendResponseHeaders(statusCode, response.length);
- exchange.getResponseBody().write(response);
- exchange.close();
- }
}