feat: add dedicated DDS permission backoffice instance
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
package com.cloudhandson.ddsbackoffice.service;
|
||||
|
||||
import com.cloudhandson.ddsbackoffice.config.DdsProperties;
|
||||
import com.cloudhandson.ddsbackoffice.domain.DdsCustomerRow;
|
||||
import com.cloudhandson.ddsbackoffice.domain.DdsQueryResult;
|
||||
import com.cloudhandson.ddsbackoffice.domain.DdsSourceOption;
|
||||
import com.cloudhandson.ddsbackoffice.domain.DdsUserOption;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DdsQueryService {
|
||||
|
||||
private static final Pattern SAFE_SOURCE = Pattern.compile("PG|MY");
|
||||
private final DdsProperties properties;
|
||||
|
||||
public DdsQueryService(DdsProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public List<DdsUserOption> users() {
|
||||
return properties.users().entrySet().stream()
|
||||
.map(entry -> {
|
||||
var user = entry.getValue();
|
||||
return new DdsUserOption(
|
||||
entry.getKey(),
|
||||
valueOrDefault(user.label(), entry.getKey()),
|
||||
valueOrDefault(user.username(), "미설정"),
|
||||
valueOrDefault(user.description(), "DDS 보안 사용자"),
|
||||
user.configured()
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<DdsSourceOption> sources() {
|
||||
return List.of(
|
||||
new DdsSourceOption(
|
||||
"PG", "PostgreSQL 원본", properties.pgObject(),
|
||||
"DDS DATA GRANT가 허용한 PostgreSQL 데이터만 반환합니다."
|
||||
),
|
||||
new DdsSourceOption(
|
||||
"MY", "MySQL 원본", properties.myObject(),
|
||||
"DDS DATA GRANT가 허용한 MySQL 데이터만 반환합니다."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public DdsQueryResult query(String userKey, String sourceKey, String searchText, int requestedLimit) {
|
||||
var normalizedUserKey = normalizeUserKey(userKey);
|
||||
var normalizedSourceKey = normalizeSourceKey(sourceKey);
|
||||
var user = properties.users().get(normalizedUserKey);
|
||||
var source = sources().stream()
|
||||
.filter(option -> option.key().equals(normalizedSourceKey))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 DDS 데이터 소스입니다."));
|
||||
var userLabel = user == null ? normalizedUserKey : valueOrDefault(user.label(), normalizedUserKey);
|
||||
|
||||
if (user == null) {
|
||||
return failure(normalizedUserKey, normalizedSourceKey, userLabel, source,
|
||||
"사용자를 찾을 수 없습니다.", "선택한 DDS END USER가 설정에 없습니다.", null);
|
||||
}
|
||||
if (properties.dbUrl().isBlank()) {
|
||||
return failure(normalizedUserKey, normalizedSourceKey, userLabel, source,
|
||||
"DB 접속 정보가 없습니다.", "DDS_BACKOFFICE_DB_URL 또는 BACKOFFICE_DB_URL을 설정하세요.", null);
|
||||
}
|
||||
if (!user.configured()) {
|
||||
return failure(normalizedUserKey, normalizedSourceKey, userLabel, source,
|
||||
"DDS 사용자 자격증명이 설정되지 않았습니다.",
|
||||
"이 인스턴스의 환경 변수에 해당 END USER 비밀번호를 설정하세요.", null);
|
||||
}
|
||||
|
||||
var limit = Math.max(1, Math.min(requestedLimit, 100));
|
||||
var search = searchText == null ? "" : searchText.trim();
|
||||
try (Connection connection = DriverManager.getConnection(
|
||||
properties.dbUrl(), user.username(), user.password())) {
|
||||
connection.setReadOnly(true);
|
||||
var timeoutSeconds = timeoutSeconds(properties.queryTimeout());
|
||||
var sessionUser = readSingleValue(connection,
|
||||
"SELECT SYS_CONTEXT('USERENV', 'SESSION_USER') FROM dual", timeoutSeconds);
|
||||
var endUser = readSingleValue(connection,
|
||||
"SELECT ORA_END_USER_CONTEXT.username FROM dual", timeoutSeconds);
|
||||
|
||||
var sql = "SELECT customer_id, full_name, email, signup_date, region FROM "
|
||||
+ source.objectName()
|
||||
+ (search.isBlank() ? "" : " WHERE UPPER(full_name) LIKE ?")
|
||||
+ " ORDER BY customer_id FETCH FIRST " + limit + " ROWS ONLY";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setQueryTimeout(timeoutSeconds);
|
||||
if (!search.isBlank()) {
|
||||
statement.setString(1, "%" + search.toUpperCase(Locale.ROOT) + "%");
|
||||
}
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
var rows = new java.util.ArrayList<DdsCustomerRow>();
|
||||
while (resultSet.next()) {
|
||||
Timestamp timestamp = resultSet.getTimestamp("signup_date");
|
||||
rows.add(new DdsCustomerRow(
|
||||
resultSet.getLong("customer_id"),
|
||||
resultSet.getString("full_name"),
|
||||
resultSet.getString("email"),
|
||||
timestamp == null ? null : timestamp.toLocalDateTime(),
|
||||
resultSet.getString("region")
|
||||
));
|
||||
}
|
||||
return new DdsQueryResult(
|
||||
normalizedUserKey, normalizedSourceKey, userLabel, source.label(), source.objectName(),
|
||||
true, "DDS 조회가 완료되었습니다.",
|
||||
rows.isEmpty() ? "조건에 맞는 행이 없습니다." : rows.size() + "개 행이 DDS 정책을 통과했습니다.",
|
||||
sessionUser, endUser, null, rows
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
return failure(normalizedUserKey, normalizedSourceKey, userLabel, source,
|
||||
classifyTitle(e), classifyMessage(e), oracleCode(e));
|
||||
}
|
||||
}
|
||||
|
||||
private DdsQueryResult failure(
|
||||
String userKey,
|
||||
String sourceKey,
|
||||
String userLabel,
|
||||
DdsSourceOption source,
|
||||
String title,
|
||||
String message,
|
||||
Integer oracleCode
|
||||
) {
|
||||
return new DdsQueryResult(
|
||||
userKey, sourceKey, userLabel, source.label(), source.objectName(),
|
||||
false, title, message, null, null, oracleCode, List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private String readSingleValue(Connection connection, String sql, int timeoutSeconds)
|
||||
throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setQueryTimeout(timeoutSeconds);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
return resultSet.next() ? resultSet.getString(1) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeUserKey(String value) {
|
||||
return value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static String normalizeSourceKey(String value) {
|
||||
var normalized = value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
if (!SAFE_SOURCE.matcher(normalized).matches()) {
|
||||
throw new IllegalArgumentException("지원하지 않는 DDS 데이터 소스입니다.");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static int timeoutSeconds(Duration duration) {
|
||||
return Math.max(1, (int) Math.ceil(duration.toMillis() / 1000.0));
|
||||
}
|
||||
|
||||
private static String valueOrDefault(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
private static Integer oracleCode(SQLException exception) {
|
||||
SQLException current = exception;
|
||||
while (current != null) {
|
||||
if (current.getErrorCode() != 0) {
|
||||
return Math.abs(current.getErrorCode());
|
||||
}
|
||||
current = current.getNextException();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String flattenedMessage(SQLException exception) {
|
||||
var messages = new StringBuilder();
|
||||
SQLException current = exception;
|
||||
while (current != null) {
|
||||
if (messages.length() > 0) {
|
||||
messages.append("; ");
|
||||
}
|
||||
messages.append(Objects.toString(current.getMessage(), ""));
|
||||
current = current.getNextException();
|
||||
}
|
||||
return messages.toString().replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
|
||||
private static String classifyTitle(SQLException exception) {
|
||||
var message = flattenedMessage(exception).toUpperCase(Locale.ROOT);
|
||||
if (message.contains("ORA-00942")) {
|
||||
return "이 DDS 사용자에게 데이터가 허용되지 않았습니다.";
|
||||
}
|
||||
if (message.contains("ORA-01017")) {
|
||||
return "DDS 사용자 인증에 실패했습니다.";
|
||||
}
|
||||
if (message.contains("ORA-12154") || message.contains("ORA-12514")
|
||||
|| message.contains("IO ERROR") || message.contains("CONNECTION")) {
|
||||
return "DDS 데이터베이스에 연결할 수 없습니다.";
|
||||
}
|
||||
return "DDS 조회 중 데이터베이스 오류가 발생했습니다.";
|
||||
}
|
||||
|
||||
private static String classifyMessage(SQLException exception) {
|
||||
var message = flattenedMessage(exception);
|
||||
var upper = message.toUpperCase(Locale.ROOT);
|
||||
if (upper.contains("ORA-00942")) {
|
||||
return "해당 END USER에 연결된 DATA ROLE의 DATA GRANT가 없어 보호 객체가 보이지 않습니다. DDS의 default deny 결과입니다.";
|
||||
}
|
||||
if (upper.contains("ORA-01017")) {
|
||||
return "서버에 등록된 DDS END USER 비밀번호와 일치하지 않습니다.";
|
||||
}
|
||||
if (upper.contains("ORA-12154") || upper.contains("ORA-12514")) {
|
||||
return "TNS 별칭 또는 서비스 이름을 확인하세요.";
|
||||
}
|
||||
return "DB 접속 정보와 DDS DATA GRANT 설정을 확인하세요. 상세 코드는 운영 로그에서 확인합니다.";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user