fix(security): close the Steam test endpoint, /internal and Prometheus to the public

/api/steam/test/{steamId} let anyone query any Steam profile with our API key. /internal/** had no controller but was open for any future one. Actuator now exposes only health, without details. The management block was also indented under endpoints, so show-details never applied.
This commit is contained in:
lobo committed 2026-09-24 10:24:34 +02:00
1 parent 8499678a4d
commit 22bc036c9c
4 files changed
+69 -44

No files matched your search

@@ -25,11 +25,8 @@ import java.util.List;
public class SecurityConfig { public class SecurityConfig {
private static final String[] WHITE_LIST_URLS = { private static final String[] WHITE_LIST_URLS = {
"/internal/**", // Allow internal service communication "/actuator/health", // Docker health check; details are off
"/actuator/health",
"/actuator/prometheus",
"/error", "/error",
"/api/steam/test/**",
"/api/stats/public" "/api/stats/public"
}; };
@@ -1,32 +0,0 @@
package core_service.modules.steam_adapter.api;
import core_service.modules.steam_adapter.api.dto.response.SteamProfileAndGamesResponse;
import core_service.modules.steam_adapter.service.SteamApiService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RestController
@RequestMapping("/api/steam/test")
@RequiredArgsConstructor
public class SteamTestController {
private final SteamApiService steamApiService;
@GetMapping("/{steamId}")
public ResponseEntity<?> testSteamConnection(@PathVariable String steamId) {
Optional<SteamProfileAndGamesResponse> profileOpt = steamApiService.getFullPlayerProfile(steamId);
if (profileOpt.isPresent()) {
return ResponseEntity.ok(profileOpt.get());
} else {
return ResponseEntity.notFound().build();
}
}
}
+4 -8
View File
@@ -26,14 +26,10 @@ management:
endpoints: endpoints:
web: web:
exposure: exposure:
include: health, prometheus, metrics include: health
endpoint: endpoint:
health: health:
show-details: always show-details: never
prometheus:
metrics:
export:
enabled: true
security: security:
jwt: jwt:
public-key: ${JWT_PUBLIC_KEY} public-key: ${JWT_PUBLIC_KEY}
@@ -0,0 +1,64 @@
package core_service.configuration.security;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.security.KeyPairGenerator;
import java.util.Base64;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Runs with the production security filter (not the dev profile, which injects a dummy user).
*/
@Tag("IntegrationTest")
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class PublicEndpointsTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine");
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) throws Exception {
byte[] publicKey = KeyPairGenerator.getInstance("Ed25519").generateKeyPair().getPublic().getEncoded();
registry.add("security.jwt.public-key", () -> Base64.getEncoder().encodeToString(publicKey));
registry.add("app.steam.api-key", () -> "test-key");
}
@Autowired
private MockMvc mockMvc;
@Test
void healthIsPublicWithoutDetails() throws Exception {
mockMvc.perform(get("/actuator/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"))
.andExpect(jsonPath("$.components").doesNotExist());
}
@ParameterizedTest
@ValueSource(strings = {"/actuator/prometheus", "/api/steam/test/76561198005679788", "/internal/anything"})
void endpointIsNotPublic(String path) throws Exception {
int status = mockMvc.perform(get(path)).andReturn().getResponse().getStatus();
assertTrue(Set.of(401, 403).contains(status), path + " answered " + status);
}
}