From b46e2badbd053de4868fdedb0d3c488375af4b6b Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 24 Sep 2026 10:24:34 +0200 Subject: [PATCH] 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. --- .../security/SecurityConfig.java | 5 +- .../api/SteamTestController.java | 32 ---------- src/main/resources/application.yml | 12 ++-- .../security/PublicEndpointsTest.java | 64 +++++++++++++++++++ 4 files changed, 69 insertions(+), 44 deletions(-) delete mode 100644 src/main/java/core_service/modules/steam_adapter/api/SteamTestController.java create mode 100644 src/test/java/core_service/configuration/security/PublicEndpointsTest.java diff --git a/src/main/java/core_service/configuration/security/SecurityConfig.java b/src/main/java/core_service/configuration/security/SecurityConfig.java index 19bc823..2aaf500 100644 --- a/src/main/java/core_service/configuration/security/SecurityConfig.java +++ b/src/main/java/core_service/configuration/security/SecurityConfig.java @@ -25,11 +25,8 @@ import java.util.List; public class SecurityConfig { private static final String[] WHITE_LIST_URLS = { - "/internal/**", // Allow internal service communication - "/actuator/health", - "/actuator/prometheus", + "/actuator/health", // Docker health check; details are off "/error", - "/api/steam/test/**", "/api/stats/public" }; diff --git a/src/main/java/core_service/modules/steam_adapter/api/SteamTestController.java b/src/main/java/core_service/modules/steam_adapter/api/SteamTestController.java deleted file mode 100644 index 993e254..0000000 --- a/src/main/java/core_service/modules/steam_adapter/api/SteamTestController.java +++ /dev/null @@ -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 profileOpt = steamApiService.getFullPlayerProfile(steamId); - - if (profileOpt.isPresent()) { - return ResponseEntity.ok(profileOpt.get()); - } else { - return ResponseEntity.notFound().build(); - } - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 68e4414..292ce0f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,14 +26,10 @@ management: endpoints: web: exposure: - include: health, prometheus, metrics - endpoint: - health: - show-details: always - prometheus: - metrics: - export: - enabled: true + include: health + endpoint: + health: + show-details: never security: jwt: public-key: ${JWT_PUBLIC_KEY} diff --git a/src/test/java/core_service/configuration/security/PublicEndpointsTest.java b/src/test/java/core_service/configuration/security/PublicEndpointsTest.java new file mode 100644 index 0000000..803e09b --- /dev/null +++ b/src/test/java/core_service/configuration/security/PublicEndpointsTest.java @@ -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/76561198000000001", "/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); + } +}