forked from kimhyungsik/ax_hub_mcp_tool
fix: enforce DAPMS tool server API key
This commit is contained in:
@@ -15,5 +15,6 @@ mcp:
|
|||||||
# Set the AA-assigned prefix before MCP pull activation (for example: cus.).
|
# Set the AA-assigned prefix before MCP pull activation (for example: cus.).
|
||||||
name-prefix: ""
|
name-prefix: ""
|
||||||
security:
|
security:
|
||||||
|
api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
|
||||||
tenant-domains:
|
tenant-domains:
|
||||||
TESTER-DEV: ALL
|
TESTER-DEV: ALL
|
||||||
|
|||||||
@@ -40,25 +40,30 @@ public class ApiKeyInterceptor implements HandlerInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String apiKey = request.getHeader("X-Tool-Server-API-Key");
|
String apiKey = request.getHeader("X-Tool-Server-API-Key");
|
||||||
|
String configuredApiKey = securityProperties.getApiKey();
|
||||||
Map<String, String> validApiKeys = securityProperties.getApiKeys();
|
Map<String, String> validApiKeys = securityProperties.getApiKeys();
|
||||||
|
boolean singleApiKeyConfigured = configuredApiKey != null && !configuredApiKey.isBlank();
|
||||||
|
boolean multipleApiKeysConfigured = validApiKeys != null && !validApiKeys.isEmpty();
|
||||||
|
|
||||||
// 2. 만약 프로퍼티에 API Key가 하나도 설정되어 있지 않다면 (개발/로컬 환경 등) 인증 없이 통과시킵니다.
|
// 2. 만약 프로퍼티에 API Key가 하나도 설정되어 있지 않다면 (개발/로컬 환경 등) 인증 없이 통과시킵니다.
|
||||||
if (validApiKeys == null || validApiKeys.isEmpty()) {
|
if (!singleApiKeyConfigured && !multipleApiKeysConfigured) {
|
||||||
MDC.put("tenantId", "anonymous");
|
MDC.put("tenantId", "anonymous");
|
||||||
request.setAttribute("tenantId", "anonymous");
|
request.setAttribute("tenantId", "anonymous");
|
||||||
log.debug(" [보안 패스] 등록된 API Key 없음 - 익명 사용자(anonymous)로 통과");
|
log.debug(" [보안 패스] 등록된 API Key 없음 - 익명 사용자(anonymous)로 통과");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 헤더로 들어온 API Key가 우리가 발급해준 목록(Map)에 존재하는지 확인합니다.
|
// 3. 헤더로 들어온 API Key가 단일 공통 Key 또는 다중 테넌트 Key 목록에 존재하는지 확인합니다.
|
||||||
if (apiKey == null || !validApiKeys.containsKey(apiKey)) {
|
boolean matchesSingleApiKey = singleApiKeyConfigured && configuredApiKey.equals(apiKey);
|
||||||
|
boolean matchesMultipleApiKeys = apiKey != null && multipleApiKeysConfigured && validApiKeys.containsKey(apiKey);
|
||||||
|
if (!matchesSingleApiKey && !matchesMultipleApiKeys) {
|
||||||
log.warn(" [보안 차단] 유효하지 않은 API Key 접근 시도 - IP: {}", request.getRemoteAddr());
|
log.warn(" [보안 차단] 유효하지 않은 API Key 접근 시도 - IP: {}", request.getRemoteAddr());
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API Key");
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API Key");
|
||||||
return false; // 컨트롤러로 넘어가지 않음
|
return false; // 컨트롤러로 넘어가지 않음
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 유효하다면 해당 키에 맵핑된 Tenant ID(식별자)를 가져옵니다. (ex. mcp-client-1)
|
// 4. 유효하다면 해당 키에 맵핑된 Tenant ID(식별자)를 가져옵니다. (ex. mcp-client-1)
|
||||||
String tenantId = validApiKeys.get(apiKey);
|
String tenantId = matchesSingleApiKey ? "dapms" : validApiKeys.get(apiKey);
|
||||||
|
|
||||||
// 4. 추출한 Tenant ID를 현재 스레드의 로깅 컨텍스트(MDC)에 저장합니다.
|
// 4. 추출한 Tenant ID를 현재 스레드의 로깅 컨텍스트(MDC)에 저장합니다.
|
||||||
// 이렇게 하면 이 요청이 끝날 때까지 찍히는 모든 로그에 어떤 테넌트가 호출했는지 자동으로 기록됩니다.
|
// 이렇게 하면 이 요청이 끝날 때까지 찍히는 모든 로그에 어떤 테넌트가 호출했는지 자동으로 기록됩니다.
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ import java.util.Map;
|
|||||||
@Component
|
@Component
|
||||||
@ConfigurationProperties(prefix = "mcp.security")
|
@ConfigurationProperties(prefix = "mcp.security")
|
||||||
public class SecurityProperties {
|
public class SecurityProperties {
|
||||||
|
// DAPMS가 X-Tool-Server-API-Key 헤더로 전달하는 단일 공통 Key
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
// API Key를 Key로, Tenant ID를 Value로 가지는 맵
|
// API Key를 Key로, Tenant ID를 Value로 가지는 맵
|
||||||
private Map<String, String> apiKeys = new HashMap<>();
|
private Map<String, String> apiKeys = new HashMap<>();
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
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.MockHttpServletRequest;
|
||||||
import org.springframework.mock.web.MockHttpServletResponse;
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
|
|
||||||
@@ -23,4 +26,40 @@ class ApiKeyInterceptorTest {
|
|||||||
assertThat(allowed).isTrue();
|
assertThat(allowed).isTrue();
|
||||||
assertThat(request.getAttribute("tenantId")).isEqualTo("dapms");
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ mcp:
|
|||||||
bundle-id: was-pro
|
bundle-id: was-pro
|
||||||
name-prefix: ""
|
name-prefix: ""
|
||||||
security:
|
security:
|
||||||
|
api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
|
||||||
tenant-domains:
|
tenant-domains:
|
||||||
TESTER-DEV: ALL
|
TESTER-DEV: ALL
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ mcp:
|
|||||||
# Set the AA-assigned prefix before MCP pull activation (for example: sal.).
|
# Set the AA-assigned prefix before MCP pull activation (for example: sal.).
|
||||||
name-prefix: ""
|
name-prefix: ""
|
||||||
security:
|
security:
|
||||||
|
api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
|
||||||
tenant-domains:
|
tenant-domains:
|
||||||
TESTER-DEV: ALL
|
TESTER-DEV: ALL
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ mcp:
|
|||||||
bundle-id: was-sys
|
bundle-id: was-sys
|
||||||
name-prefix: ""
|
name-prefix: ""
|
||||||
security:
|
security:
|
||||||
|
api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
|
||||||
tenant-domains:
|
tenant-domains:
|
||||||
TESTER-DEV: ALL
|
TESTER-DEV: ALL
|
||||||
|
|||||||
Reference in New Issue
Block a user