refs #723: externalize backoffice catalogs and MCP tools

This commit is contained in:
devmrko
2026-07-23 19:41:54 +09:00
parent 1917df09a2
commit 0d9028ef13
58 changed files with 1671 additions and 531 deletions

View File

@@ -1,11 +1,17 @@
package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.config.SecuritySqlScriptProperties;
import com.cloudhandson.vpdbackoffice.domain.securityscript.SecuritySqlScript;
import com.cloudhandson.vpdbackoffice.domain.securityscript.SecuritySqlScriptSummary;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
@@ -17,46 +23,21 @@ import org.springframework.stereotype.Service;
@Service
public class SecuritySqlScriptService {
private static final List<ScriptDefinition> CURATED_SCRIPTS = List.of(
new ScriptDefinition(
"aso-masking-metadata",
"ASO / 마스킹",
"62_kb_aso_masking_backoffice_metadata.sql",
"컬럼 마스킹 규칙 메타데이터",
"ASO 컬럼 마스킹 규칙·컬럼 연결·사용자 예외를 관리하는 백오피스 메타데이터를 생성합니다."
),
new ScriptDefinition(
"aso-masking-runtime",
"ASO / 마스킹",
"63_kb_aso_masking_rule_runtime.sql",
"ASO 마스킹 런타임 적용",
"백오피스 컬럼 마스킹 규칙을 Oracle Data Redaction 정책과 신뢰 컨텍스트에 반영합니다."
),
new ScriptDefinition(
"aso-masking-default-columns",
"ASO / 마스킹",
"64_kb_aso_masking_default_column_rules.sql",
"ASO 기본 대상 컬럼",
"주민번호·청구/지급금·타사보유 컬럼의 마스킹 블랙리스트 초기값을 연결합니다."
),
new ScriptDefinition(
"select-ai-vpd-api",
"Select AI / 행 접근",
"65_kb_select_ai_vpd_query_api.sql",
"행 접근 적용 Select AI 조회 API",
"생성 SQL을 KB 업무 테이블의 단일 읽기 전용 SELECT/WITH로 검증해 행 접근 컨텍스트에서 실행합니다."
),
new ScriptDefinition(
"select-ai-vpd-ords",
"ORDS / Select AI",
"66_kb_select_ai_vpd_query_ords.sql",
"Select AI 행 접근 ORDS Endpoint",
"Bearer 토큰을 검증해 행 접근 컨텍스트를 설정한 뒤 Select AI 조회 API를 노출합니다."
)
private static final Pattern SCRIPT_ID = Pattern.compile("[a-z][a-z0-9-]{0,63}");
private static final Pattern RESOURCE_PATH = Pattern.compile(
"(?:[A-Za-z0-9][A-Za-z0-9_-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*\\.sql"
);
private final List<ScriptDefinition> scripts;
public SecuritySqlScriptService(
SecuritySqlScriptProperties properties,
ObjectMapper objectMapper
) {
scripts = parse(properties.scripts(), objectMapper);
}
public List<SecuritySqlScriptSummary> list() {
return CURATED_SCRIPTS.stream()
return scripts.stream()
.map(definition -> new SecuritySqlScriptSummary(
definition.scriptId(),
definition.category(),
@@ -68,7 +49,7 @@ public class SecuritySqlScriptService {
}
public SecuritySqlScript find(String scriptId) {
ScriptDefinition definition = CURATED_SCRIPTS.stream()
ScriptDefinition definition = scripts.stream()
.filter(candidate -> candidate.scriptId().equals(scriptId))
.findFirst()
.orElseThrow(() -> new AppException("조회할 수 없는 보안 SQL 스크립트입니다."));
@@ -91,7 +72,48 @@ public class SecuritySqlScriptService {
}
}
private record ScriptDefinition(
private List<ScriptDefinition> parse(String raw, ObjectMapper objectMapper) {
if (raw == null || raw.isBlank()) {
return List.of();
}
try {
List<ScriptDefinition> parsed = objectMapper.readValue(raw, new TypeReference<>() {});
if (parsed.isEmpty()) {
throw new IllegalArgumentException("보안 SQL 목록이 비어 있습니다.");
}
Set<String> scriptIds = new HashSet<>();
Set<String> fileNames = new HashSet<>();
parsed.forEach(definition -> {
validate(definition);
if (!scriptIds.add(definition.scriptId())) {
throw new IllegalArgumentException("중복 scriptId");
}
if (!fileNames.add(definition.fileName())) {
throw new IllegalArgumentException("중복 fileName");
}
});
return List.copyOf(parsed);
} catch (Exception exception) {
throw new IllegalStateException("BACKOFFICE_SECURITY_SQL_SCRIPTS 설정을 확인하세요.", exception);
}
}
private void validate(ScriptDefinition definition) {
if (definition == null
|| definition.scriptId() == null || !SCRIPT_ID.matcher(definition.scriptId()).matches()
|| definition.fileName() == null || !RESOURCE_PATH.matcher(definition.fileName()).matches()
|| blank(definition.category())
|| blank(definition.title())
|| blank(definition.description())) {
throw new IllegalArgumentException("보안 SQL 정의가 올바르지 않습니다.");
}
}
private boolean blank(String value) {
return value == null || value.isBlank();
}
public record ScriptDefinition(
String scriptId,
String category,
String fileName,