[Developer] #567 add VPD SQL execution evidence
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user