125 lines
4.4 KiB
Java
125 lines
4.4 KiB
Java
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;
|
|
|
|
/**
|
|
* Read-only catalogue of security deployment SQL bundled from the Git-tracked
|
|
* sql/adb directory. Script ids are an application whitelist: request input
|
|
* never becomes a filesystem or classpath path.
|
|
*/
|
|
@Service
|
|
public class SecuritySqlScriptService {
|
|
|
|
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 scripts.stream()
|
|
.map(definition -> new SecuritySqlScriptSummary(
|
|
definition.scriptId(),
|
|
definition.category(),
|
|
definition.fileName(),
|
|
definition.title(),
|
|
definition.description()
|
|
))
|
|
.toList();
|
|
}
|
|
|
|
public SecuritySqlScript find(String scriptId) {
|
|
ScriptDefinition definition = scripts.stream()
|
|
.filter(candidate -> candidate.scriptId().equals(scriptId))
|
|
.findFirst()
|
|
.orElseThrow(() -> new AppException("조회할 수 없는 보안 SQL 스크립트입니다."));
|
|
return new SecuritySqlScript(
|
|
definition.scriptId(),
|
|
definition.category(),
|
|
definition.fileName(),
|
|
definition.title(),
|
|
definition.description(),
|
|
readSource(definition.fileName())
|
|
);
|
|
}
|
|
|
|
private String readSource(String fileName) {
|
|
ClassPathResource resource = new ClassPathResource("sql/adb/" + fileName);
|
|
try (InputStream input = resource.getInputStream()) {
|
|
return new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
|
} catch (IOException exception) {
|
|
throw new AppException("배포된 보안 SQL 스크립트를 읽을 수 없습니다: " + fileName);
|
|
}
|
|
}
|
|
|
|
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,
|
|
String title,
|
|
String description
|
|
) {
|
|
}
|
|
}
|