[Developer] #567 add VPD SQL execution evidence

This commit is contained in:
devmrko
2026-07-01 13:47:30 +09:00
parent 07c090f6b0
commit bf393bffa5
13 changed files with 398 additions and 11 deletions

View File

@@ -94,6 +94,16 @@ public class OrdsMetadataService {
.toList();
}
/**
* Uses the optional CB_ORDS metadata connection when configured. SQL cursor
* evidence must be read from the parsing schema's database context, rather
* than from an unrelated backoffice connection.
*/
public JdbcTemplate executionEvidenceJdbcTemplate() {
return ordsMetadataJdbcTemplate;
}
public String objectQueryHandlerSource(long objectId) {
ProtectedObject object = protectedObjectService.assertEnabled(objectId);
rejectGenericVectorHandler(object);

View File

@@ -4,6 +4,7 @@ import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus;
import com.cloudhandson.vpdbackoffice.domain.probe.SqlExecutionEvidence;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
@@ -53,6 +54,7 @@ public class OrdsProbeService {
private final ObjectMapper objectMapper;
private final SettingService settingService;
private final JdbcTemplate jdbcTemplate;
private final OrdsMetadataService ordsMetadataService;
private final Clock clock;
public OrdsProbeService(
@@ -64,6 +66,7 @@ public class OrdsProbeService {
ObjectMapper objectMapper,
SettingService settingService,
JdbcTemplate jdbcTemplate,
OrdsMetadataService ordsMetadataService,
Clock clock
) {
this.tokenService = tokenService;
@@ -74,6 +77,7 @@ public class OrdsProbeService {
this.objectMapper = objectMapper;
this.settingService = settingService;
this.jdbcTemplate = jdbcTemplate;
this.ordsMetadataService = ordsMetadataService;
this.clock = clock;
}
@@ -142,6 +146,7 @@ public class OrdsProbeService {
prettyHeaders(response.getHeaders()),
prettyJson(response.getBody())
);
result = attachRecentExecutionEvidence(result, object);
if (isVectorSearchObject(object)) {
result = addVectorSqlTrace(result, command.bearerToken(), object);
} else if (!result.hasSqlTrace()) {
@@ -236,6 +241,114 @@ public class OrdsProbeService {
return text == null || text.isBlank() ? null : text;
}
/**
* V$SQL keeps the statement submitted by ORDS, not Oracle's internally
* rewritten VPD text. DBMS_XPLAN is therefore the authoritative place to
* show the predicate that the matching cursor applied.
*/
private ProbeResult attachRecentExecutionEvidence(ProbeResult result, ProtectedObject object) {
try {
SqlExecutionEvidence evidence = findRecentExecutionEvidence(object);
if (evidence == null) {
return result.withExecutionEvidence(null,
"최근 2분 내 이 보호 대상의 SQL_ID를 shared pool에서 찾지 못했습니다. "
+ "ORDS 실행 계정의 V$SQL 보존 시간과 대상 SQL을 확인하세요.");
}
String message = evidence.hasPredicatePlan() ? null
: "SQL_ID는 찾았지만 DBMS_XPLAN Predicate Information을 읽지 못했습니다. "
+ "백오피스 DB 계정에 V$SQL/DBMS_XPLAN 조회 권한이 필요합니다.";
return result.withExecutionEvidence(evidence, message);
} catch (RuntimeException exception) {
log.debug("Recent SQL execution evidence unavailable for {}.{}: {}",
object.owner(), object.objectName(), exception.getMessage());
return result.withExecutionEvidence(null,
"DB 실행 증적을 읽지 못했습니다. 백오피스 DB 계정에 V$SQL과 DBMS_XPLAN 조회 권한이 필요합니다.");
}
}
private SqlExecutionEvidence findRecentExecutionEvidence(ProtectedObject object) {
JdbcTemplate evidenceJdbcTemplate = ordsMetadataService.executionEvidenceJdbcTemplate();
try {
SqlExecutionEvidence evidence = findRecentExecutionEvidence(evidenceJdbcTemplate, object);
if (evidence != null || evidenceJdbcTemplate == jdbcTemplate) {
return evidence;
}
} catch (RuntimeException exception) {
log.debug("ORDS parsing-schema cursor evidence is unavailable: {}", exception.getMessage());
}
return findRecentExecutionEvidence(jdbcTemplate, object);
}
private SqlExecutionEvidence findRecentExecutionEvidence(
JdbcTemplate evidenceJdbcTemplate,
ProtectedObject object
) {
String marker = "%FROM%" + object.owner().toUpperCase(Locale.ROOT)
+ "." + object.objectName().toUpperCase(Locale.ROOT) + "%";
return evidenceJdbcTemplate.query("""
SELECT sql_id,
child_number,
sql_fulltext,
last_active_time,
executions,
rows_processed,
elapsed_time,
buffer_gets
FROM (
SELECT sql_id,
child_number,
sql_fulltext,
last_active_time,
executions,
rows_processed,
elapsed_time,
buffer_gets
FROM v$sql
WHERE UPPER(sql_text) LIKE ?
AND last_active_time >= SYSTIMESTAMP - INTERVAL '2' MINUTE
ORDER BY last_active_time DESC
)
WHERE ROWNUM = 1
""", resultSet -> {
if (!resultSet.next()) {
return null;
}
String sqlId = resultSet.getString("sql_id");
int childNumber = resultSet.getInt("child_number");
String predicatePlan = findPredicatePlan(evidenceJdbcTemplate, sqlId, childNumber);
return new SqlExecutionEvidence(
sqlId,
childNumber,
resultSet.getString("sql_fulltext"),
resultSet.getTimestamp("last_active_time") == null
? null : resultSet.getTimestamp("last_active_time").toLocalDateTime().toString(),
resultSet.getLong("executions"),
resultSet.getLong("rows_processed"),
Math.round(resultSet.getLong("elapsed_time") / 1000.0d),
resultSet.getLong("buffer_gets"),
predicatePlan
);
}, marker);
}
private String findPredicatePlan(
JdbcTemplate evidenceJdbcTemplate,
String sqlId,
int childNumber
) {
try {
List<String> lines = evidenceJdbcTemplate.query("""
SELECT plan_table_output
FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(?, ?, 'ALLSTATS LAST +PREDICATE +ALIAS'))
""", (resultSet, rowNum) -> resultSet.getString(1), sqlId, childNumber);
return lines.isEmpty() ? null : String.join(System.lineSeparator(), lines);
} catch (RuntimeException exception) {
log.debug("DBMS_XPLAN is unavailable for SQL_ID {} child {}: {}",
sqlId, childNumber, exception.getMessage());
return null;
}
}
private ProbeResult addLocalSqlTrace(ProbeResult result, String bearerToken, ProtectedObject object) {
String predicate = findVpdPredicate(bearerToken, object);
if (predicate == null || predicate.isBlank()) {