diff --git a/dap-was-cus/src/main/resources/application.yml b/dap-was-cus/src/main/resources/application.yml index dfc56afed..ed740b986 100644 --- a/dap-was-cus/src/main/resources/application.yml +++ b/dap-was-cus/src/main/resources/application.yml @@ -15,5 +15,6 @@ mcp: # Set the AA-assigned prefix before MCP pull activation (for example: cus.). name-prefix: "" security: + api-key: ${TOOL_SERVER_API_KEY:tool-server-key} tenant-domains: TESTER-DEV: ALL diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java index de2b65280..28ea41e03 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java @@ -40,25 +40,30 @@ public class ApiKeyInterceptor implements HandlerInterceptor { } String apiKey = request.getHeader("X-Tool-Server-API-Key"); + String configuredApiKey = securityProperties.getApiKey(); Map validApiKeys = securityProperties.getApiKeys(); + boolean singleApiKeyConfigured = configuredApiKey != null && !configuredApiKey.isBlank(); + boolean multipleApiKeysConfigured = validApiKeys != null && !validApiKeys.isEmpty(); // 2. 만약 프로퍼티에 API Key가 하나도 설정되어 있지 않다면 (개발/로컬 환경 등) 인증 없이 통과시킵니다. - if (validApiKeys == null || validApiKeys.isEmpty()) { + if (!singleApiKeyConfigured && !multipleApiKeysConfigured) { MDC.put("tenantId", "anonymous"); request.setAttribute("tenantId", "anonymous"); log.debug(" [보안 패스] 등록된 API Key 없음 - 익명 사용자(anonymous)로 통과"); return true; } - // 3. 헤더로 들어온 API Key가 우리가 발급해준 목록(Map)에 존재하는지 확인합니다. - if (apiKey == null || !validApiKeys.containsKey(apiKey)) { + // 3. 헤더로 들어온 API Key가 단일 공통 Key 또는 다중 테넌트 Key 목록에 존재하는지 확인합니다. + boolean matchesSingleApiKey = singleApiKeyConfigured && configuredApiKey.equals(apiKey); + boolean matchesMultipleApiKeys = apiKey != null && multipleApiKeysConfigured && validApiKeys.containsKey(apiKey); + if (!matchesSingleApiKey && !matchesMultipleApiKeys) { log.warn(" [보안 차단] 유효하지 않은 API Key 접근 시도 - IP: {}", request.getRemoteAddr()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API Key"); return false; // 컨트롤러로 넘어가지 않음 } // 4. 유효하다면 해당 키에 맵핑된 Tenant ID(식별자)를 가져옵니다. (ex. mcp-client-1) - String tenantId = validApiKeys.get(apiKey); + String tenantId = matchesSingleApiKey ? "dapms" : validApiKeys.get(apiKey); // 4. 추출한 Tenant ID를 현재 스레드의 로깅 컨텍스트(MDC)에 저장합니다. // 이렇게 하면 이 요청이 끝날 때까지 찍히는 모든 로그에 어떤 테넌트가 호출했는지 자동으로 기록됩니다. diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/SecurityProperties.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/SecurityProperties.java index 381164e61..e69e6f034 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/SecurityProperties.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/SecurityProperties.java @@ -31,6 +31,9 @@ import java.util.Map; @Component @ConfigurationProperties(prefix = "mcp.security") public class SecurityProperties { + // DAPMS가 X-Tool-Server-API-Key 헤더로 전달하는 단일 공통 Key + private String apiKey; + // API Key를 Key로, Tenant ID를 Value로 가지는 맵 private Map apiKeys = new HashMap<>(); diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptorTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptorTest.java index d0ca486a4..42cc9cf1e 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptorTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptorTest.java @@ -4,6 +4,9 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -23,4 +26,40 @@ class ApiKeyInterceptorTest { assertThat(allowed).isTrue(); assertThat(request.getAttribute("tenantId")).isEqualTo("dapms"); } + + @Test + void rejectsARequestWhenItDoesNotMatchTheConfiguredSingleApiKey() throws Exception { + SecurityProperties properties = bindSingleApiKey("tool-server-key"); + ApiKeyInterceptor interceptor = new ApiKeyInterceptor(properties); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Tool-Server-API-Key", "wrong-key"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, new Object()); + + assertThat(allowed).isFalse(); + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void authenticatesARequestWithTheConfiguredSingleApiKey() throws Exception { + SecurityProperties properties = bindSingleApiKey("tool-server-key"); + ApiKeyInterceptor interceptor = new ApiKeyInterceptor(properties); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Tool-Server-API-Key", "tool-server-key"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, new Object()); + + assertThat(allowed).isTrue(); + assertThat(request.getAttribute("tenantId")).isEqualTo("dapms"); + } + + private SecurityProperties bindSingleApiKey(String apiKey) { + SecurityProperties properties = new SecurityProperties(); + Binder binder = new Binder(new MapConfigurationPropertySource( + Map.of("mcp.security.api-key", apiKey))); + binder.bind("mcp.security", Bindable.ofInstance(properties)); + return properties; + } } diff --git a/dap-was-pro/src/main/resources/application.yml b/dap-was-pro/src/main/resources/application.yml index 8838ee170..4054bcc1c 100644 --- a/dap-was-pro/src/main/resources/application.yml +++ b/dap-was-pro/src/main/resources/application.yml @@ -14,5 +14,6 @@ mcp: bundle-id: was-pro name-prefix: "" security: + api-key: ${TOOL_SERVER_API_KEY:tool-server-key} tenant-domains: TESTER-DEV: ALL diff --git a/dap-was-sal/src/main/resources/application.yml b/dap-was-sal/src/main/resources/application.yml index 964b96532..e26a45e7c 100644 --- a/dap-was-sal/src/main/resources/application.yml +++ b/dap-was-sal/src/main/resources/application.yml @@ -15,5 +15,6 @@ mcp: # Set the AA-assigned prefix before MCP pull activation (for example: sal.). name-prefix: "" security: + api-key: ${TOOL_SERVER_API_KEY:tool-server-key} tenant-domains: TESTER-DEV: ALL diff --git a/dap-was-sys/src/main/resources/application.yml b/dap-was-sys/src/main/resources/application.yml index 5c05991ba..5b1001f06 100644 --- a/dap-was-sys/src/main/resources/application.yml +++ b/dap-was-sys/src/main/resources/application.yml @@ -14,5 +14,6 @@ mcp: bundle-id: was-sys name-prefix: "" security: + api-key: ${TOOL_SERVER_API_KEY:tool-server-key} tenant-domains: TESTER-DEV: ALL