[Developer] #567 add VPD SQL execution evidence
This commit is contained in:
@@ -16,7 +16,9 @@ public record ProbeResult(
|
||||
String responseHeaders,
|
||||
String responseBody,
|
||||
String vpdPredicate,
|
||||
String effectiveSql
|
||||
String effectiveSql,
|
||||
SqlExecutionEvidence executionEvidence,
|
||||
String executionEvidenceMessage
|
||||
) {
|
||||
|
||||
public ProbeResult(
|
||||
@@ -45,6 +47,42 @@ public record ProbeResult(
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public ProbeResult(
|
||||
ProbeStatus status,
|
||||
List<String> columns,
|
||||
List<Map<String, Object>> rows,
|
||||
int rowCount,
|
||||
List<String> maskedColumns,
|
||||
String errorCode,
|
||||
String errorMessage,
|
||||
String requestHeaders,
|
||||
String requestPayload,
|
||||
String responseHeaders,
|
||||
String responseBody,
|
||||
String vpdPredicate,
|
||||
String effectiveSql
|
||||
) {
|
||||
this(
|
||||
status,
|
||||
columns,
|
||||
rows,
|
||||
rowCount,
|
||||
maskedColumns,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
vpdPredicate,
|
||||
effectiveSql,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
@@ -99,7 +137,37 @@ public record ProbeResult(
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
predicate,
|
||||
sql
|
||||
sql,
|
||||
executionEvidence,
|
||||
executionEvidenceMessage
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasExecutionEvidence() {
|
||||
return executionEvidence != null && executionEvidence.sqlId() != null
|
||||
&& !executionEvidence.sqlId().isBlank();
|
||||
}
|
||||
|
||||
public ProbeResult withExecutionEvidence(
|
||||
SqlExecutionEvidence evidence,
|
||||
String unavailableMessage
|
||||
) {
|
||||
return new ProbeResult(
|
||||
status,
|
||||
columns,
|
||||
rows,
|
||||
rowCount,
|
||||
maskedColumns,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
vpdPredicate,
|
||||
effectiveSql,
|
||||
evidence,
|
||||
unavailableMessage
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.probe;
|
||||
|
||||
/**
|
||||
* Database shared-pool evidence for the statement most recently executed for
|
||||
* a protected object. The SQL text is the statement submitted by ORDS before
|
||||
* VPD rewrite; Oracle exposes the applied predicate through the cursor plan.
|
||||
*/
|
||||
public record SqlExecutionEvidence(
|
||||
String sqlId,
|
||||
int childNumber,
|
||||
String originalSql,
|
||||
String lastActiveAt,
|
||||
long executions,
|
||||
long rowsProcessed,
|
||||
long elapsedMillis,
|
||||
long bufferGets,
|
||||
String predicatePlan
|
||||
) {
|
||||
|
||||
public boolean hasPredicatePlan() {
|
||||
return predicatePlan != null && !predicatePlan.isBlank();
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -43,4 +43,5 @@ public class OrdsHandlerController {
|
||||
}
|
||||
return "redirect:/ords-handlers";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -108,12 +108,51 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="result-section" th:if="${result.hasExecutionEvidence()}">
|
||||
<div class="section-heading compact-heading">
|
||||
<div>
|
||||
<h3>DB 실행 증적</h3>
|
||||
<p class="section-subtitle">최근 2분 내 같은 보호 대상에 실행된 ORDS SQL을 SQL_ID로 확인한 결과입니다.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-success" th:text="${'SQL_ID ' + result.executionEvidence().sqlId()}">SQL_ID</span>
|
||||
</div>
|
||||
<div class="effective-preview">
|
||||
<dl>
|
||||
<div><dt>Child cursor</dt><dd th:text="${result.executionEvidence().childNumber()}">0</dd></div>
|
||||
<div><dt>최근 실행</dt><dd th:text="${result.executionEvidence().lastActiveAt()} ?: '-'">2026-07-01T10:00</dd></div>
|
||||
<div><dt>Cursor 실행 횟수</dt><dd th:text="${result.executionEvidence().executions()}">1</dd></div>
|
||||
<div><dt>Cursor 처리 행</dt><dd th:text="${result.executionEvidence().rowsProcessed()}">10</dd></div>
|
||||
<div><dt>누적 경과 시간</dt><dd th:text="${result.executionEvidence().elapsedMillis() + ' ms'}">10 ms</dd></div>
|
||||
<div><dt>Buffer gets</dt><dd th:text="${result.executionEvidence().bufferGets()}">0</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="probe-exchange-grid mt-3">
|
||||
<section class="probe-exchange" data-sql-trace-field="executed_sql">
|
||||
<h3>DB가 기록한 원문 SQL</h3>
|
||||
<p class="form-hint">V$SQL의 원문입니다. Oracle VPD가 WHERE 조건을 내부적으로 주입하기 전 SQL 형태로 저장됩니다.</p>
|
||||
<pre th:text="${result.executionEvidence().originalSql()}">SELECT ...</pre>
|
||||
</section>
|
||||
<section class="probe-exchange" data-sql-trace-field="execution_plan" th:if="${result.executionEvidence().hasPredicatePlan()}">
|
||||
<h3>DBMS_XPLAN Predicate Information</h3>
|
||||
<p class="form-hint">이 cursor의 실행계획에 기록된 Access/Filter predicate입니다. VPD 적용 근거는 여기서 확인합니다.</p>
|
||||
<pre th:text="${result.executionEvidence().predicatePlan()}">Predicate Information</pre>
|
||||
</section>
|
||||
</div>
|
||||
<div class="alert alert-light mt-3 mb-0" th:if="${result.executionEvidenceMessage() != null}"
|
||||
th:text="${result.executionEvidenceMessage()}"></div>
|
||||
</section>
|
||||
|
||||
<div class="alert alert-light mb-0" th:if="${result.successLike() and !result.hasExecutionEvidence() and result.executionEvidenceMessage() != null}"
|
||||
th:text="${result.executionEvidenceMessage()}">
|
||||
최근 SQL_ID를 찾지 못했습니다.
|
||||
</div>
|
||||
|
||||
<section class="result-section sql-trace-section" th:if="${result.hasSqlTrace()}">
|
||||
<div class="section-heading compact-heading">
|
||||
<div>
|
||||
<h3>토큰 적용 후 SQL</h3>
|
||||
<h3>권한 조건 재현 SQL</h3>
|
||||
<p class="section-subtitle"
|
||||
th:text="${vectorSearch ? 'VECTOR_DISTANCE 유사도 검색과 현재 사용자의 역할 기반 VPD 권한 필터를 결합한 재현용 SQL입니다.' : 'ORDS Handler가 반환한 VPD predicate 또는 같은 토큰 컨텍스트를 재현해 조회한 predicate를 기본 조회문에 합친 형태입니다.'}">
|
||||
th:text="${vectorSearch ? 'VECTOR_DISTANCE 유사도 검색과 현재 사용자의 역할 기반 VPD 권한 필터를 결합한 설명용 SQL입니다.' : 'ORDS Handler가 반환한 VPD predicate 또는 같은 토큰 컨텍스트를 기본 조회문에 결합한 설명용 SQL입니다.'}">
|
||||
ORDS Handler가 반환한 VPD predicate 또는 같은 토큰 컨텍스트를 재현해 조회한 predicate를 기본 조회문에 합친 형태입니다.
|
||||
</p>
|
||||
</div>
|
||||
@@ -159,7 +198,7 @@ ROWNUM <= :row_limit</pre>
|
||||
</section>
|
||||
</div>
|
||||
<p class="form-hint mt-2 mb-0">
|
||||
Oracle 내부 optimizer의 실행계획이나 bind 값 치환 결과가 아니라, 이 토큰 컨텍스트에서 VPD 정책 함수가 실제로 반환한 행 조건을 표시합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.
|
||||
위 SQL은 권한 조건을 읽기 쉽게 재현한 표현입니다. 실제 실행 증적은 SQL_ID와 DBMS_XPLAN 영역에서 확인합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1>조회 연동</h1>
|
||||
<details class="explanation-details">
|
||||
<summary>도움말</summary>
|
||||
<p>Bearer Token을 DB 컨텍스트로 변환한 뒤 VPD가 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다. 실행 결과의 “토큰 적용 후 SQL”은 접근 검증 메뉴에서 확인할 수 있습니다.</p>
|
||||
<p>Bearer Token을 DB 컨텍스트로 변환한 뒤 VPD가 권한체계를 적용합니다. 아래 소스는 등록된 ORDS Handler가 실제로 실행하는 기술 세부 내용입니다. 실행 결과의 SQL_ID 증적과 권한 조건 재현은 접근 검증 메뉴에서 확인할 수 있습니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user