[Developer] #567 record actual VPD execution with FGA

This commit is contained in:
devmrko
2026-07-01 16:02:03 +09:00
parent 57d77d2df2
commit ebcce66bf7
17 changed files with 532 additions and 220 deletions

View File

@@ -221,6 +221,16 @@ public class OrdsMetadataService {
p_param_type => 'STRING',
p_access_method => 'IN'
);
ORDS.DEFINE_PARAMETER(
p_module_name => ?,
p_pattern => ?,
p_method => 'POST',
p_name => 'X-VPD-Probe-Id',
p_bind_variable_name => 'probe_id',
p_source_type => 'HEADER',
p_param_type => 'STRING',
p_access_method => 'IN'
);
ORDS.DEFINE_PARAMETER(
p_module_name => ?,
p_pattern => ?,
@@ -239,6 +249,7 @@ public class OrdsMetadataService {
moduleName, template,
moduleName, template, source,
moduleName, template,
moduleName, template,
moduleName, template);
protectedObjectService.updateOrdsPath(object.objectId(), ordsPath);
return new OrdsObjectHandlerResult(object.objectId(), ordsPath, moduleName, template);
@@ -259,7 +270,7 @@ public class OrdsMetadataService {
v_vpd_predicate VARCHAR2(32767);
v_effective_sql VARCHAR2(32767);
BEGIN
cb_ords_handler_pkg.set_vpd_context(:auth_header);
cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id);
-- This is the same predicate function invoked by DBMS_RLS for the
-- SELECT below. It is returned only as diagnostic metadata.

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.FgaExecutionEvidence;
import com.cloudhandson.vpdbackoffice.domain.probe.SqlExecutionCandidate;
import com.cloudhandson.vpdbackoffice.domain.probe.SqlExecutionEvidence;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
@@ -25,6 +26,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.http.HttpEntity;
@@ -149,6 +151,8 @@ public class OrdsProbeService {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(command.bearerToken());
headers.setContentType(MediaType.APPLICATION_JSON);
String probeRequestId = UUID.randomUUID().toString();
headers.set("X-VPD-Probe-Id", probeRequestId);
String requestBody = command.requestBody() == null || command.requestBody().isBlank()
? "{}"
: command.requestBody().trim();
@@ -164,7 +168,7 @@ public class OrdsProbeService {
prettyHeaders(response.getHeaders()),
prettyJson(response.getBody())
);
result = attachRecentExecutionEvidence(result, object);
result = attachFgaExecutionEvidence(result, object, probeRequestId);
if (isVectorSearchObject(object)) {
result = addVectorSqlTrace(result, command.bearerToken(), object);
} else if (!result.hasSqlTrace()) {
@@ -259,6 +263,114 @@ public class OrdsProbeService {
return text == null || text.isBlank() ? null : text;
}
/**
* FGA records are durable database audit evidence. Unlike the optional
* V$SQL diagnostic view, SQL_TEXT and RLS_INFO originate from the SELECT
* that Oracle actually audited.
*/
private ProbeResult attachFgaExecutionEvidence(
ProbeResult result,
ProtectedObject object,
String probeRequestId
) {
String owner = object.owner().toUpperCase(Locale.ROOT);
String name = object.objectName().toUpperCase(Locale.ROOT);
FgaExecutionEvidence evidence = findUnifiedFgaEvidence(owner, name, probeRequestId);
if (evidence == null) {
evidence = findTraditionalFgaEvidence(owner, name, probeRequestId);
}
if (evidence == null) {
return result.withFgaExecutionEvidence(null,
"이 요청의 DB 감사 행을 찾지 못했습니다. FGA 실행 감사 정책과 ORDS Handler의 X-VPD-Probe-Id 연동을 적용한 뒤 다시 검증하세요.");
}
String message = evidence.hasRlsInfo() ? null
: "FGA 감사 SQL은 기록됐지만 RLS_INFO가 비어 있습니다. 감사 정책과 DB audit trail 설정을 확인하세요.";
return result.withFgaExecutionEvidence(evidence, message);
}
private FgaExecutionEvidence findUnifiedFgaEvidence(
String owner,
String name,
String probeRequestId
) {
return queryFgaEvidence("""
SELECT event_timestamp_utc AS event_at,
dbusername AS db_user,
client_identifier AS client_id,
action_name AS statement_type,
sql_text,
rls_info
FROM (
SELECT event_timestamp_utc,
dbusername,
client_identifier,
action_name,
sql_text,
rls_info
FROM unified_audit_trail
WHERE object_schema = ?
AND object_name = ?
AND action_name = 'SELECT'
AND client_identifier = ?
AND fga_policy_name IS NOT NULL
ORDER BY event_timestamp_utc DESC
)
WHERE ROWNUM = 1
""", owner, name, probeRequestId);
}
private FgaExecutionEvidence findTraditionalFgaEvidence(
String owner,
String name,
String probeRequestId
) {
return queryFgaEvidence("""
SELECT extended_timestamp AS event_at,
db_user,
client_id,
statement_type,
sql_text,
rls_info
FROM (
SELECT extended_timestamp,
db_user,
client_id,
statement_type,
sql_text,
rls_info
FROM dba_fga_audit_trail
WHERE object_schema = ?
AND object_name = ?
AND statement_type = 'SELECT'
AND client_id = ?
ORDER BY extended_timestamp DESC
)
WHERE ROWNUM = 1
""", owner, name, probeRequestId);
}
private FgaExecutionEvidence queryFgaEvidence(String sql, Object... arguments) {
try {
return jdbcTemplate.query(sql, resultSet -> {
if (!resultSet.next()) {
return null;
}
return new FgaExecutionEvidence(
resultSet.getTimestamp("event_at") == null
? null : resultSet.getTimestamp("event_at").toInstant().toString(),
resultSet.getString("db_user"),
resultSet.getString("client_id"),
resultSet.getString("statement_type"),
resultSet.getString("sql_text"),
resultSet.getString("rls_info")
);
}, arguments);
} catch (RuntimeException exception) {
log.debug("FGA audit trail query is unavailable: {}", exception.getMessage());
return null;
}
}
/**
* V$SQL keeps the statement submitted by ORDS, not Oracle's internally
* rewritten VPD text. DBMS_XPLAN is therefore the authoritative place to