[Developer] #567 record actual VPD execution with FGA
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.probe;
|
||||
|
||||
/**
|
||||
* Durable, database-generated FGA evidence for a protected-object SELECT.
|
||||
* SQL text and RLS information come from Oracle's audit trail, not from a
|
||||
* re-evaluation of the VPD policy function.
|
||||
*/
|
||||
public record FgaExecutionEvidence(
|
||||
String eventAt,
|
||||
String dbUser,
|
||||
String clientId,
|
||||
String statementType,
|
||||
String sqlText,
|
||||
String rlsInfo
|
||||
) {
|
||||
|
||||
public boolean hasRlsInfo() {
|
||||
return rlsInfo != null && !rlsInfo.isBlank();
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,9 @@ public record ProbeResult(
|
||||
String effectiveSql,
|
||||
SqlExecutionEvidence executionEvidence,
|
||||
String executionEvidenceMessage,
|
||||
List<SqlExecutionCandidate> executionCandidates
|
||||
List<SqlExecutionCandidate> executionCandidates,
|
||||
FgaExecutionEvidence fgaExecutionEvidence,
|
||||
String fgaExecutionEvidenceMessage
|
||||
) {
|
||||
|
||||
public ProbeResult(
|
||||
@@ -51,7 +53,9 @@ public record ProbeResult(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
List.of(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +90,9 @@ public record ProbeResult(
|
||||
effectiveSql,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
List.of(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,7 +149,9 @@ public record ProbeResult(
|
||||
sql,
|
||||
executionEvidence,
|
||||
executionEvidenceMessage,
|
||||
executionCandidates
|
||||
executionCandidates,
|
||||
fgaExecutionEvidence,
|
||||
fgaExecutionEvidenceMessage
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,7 +188,9 @@ public record ProbeResult(
|
||||
effectiveSql,
|
||||
evidence,
|
||||
unavailableMessage,
|
||||
candidates == null ? List.of() : List.copyOf(candidates)
|
||||
candidates == null ? List.of() : List.copyOf(candidates),
|
||||
fgaExecutionEvidence,
|
||||
fgaExecutionEvidenceMessage
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,6 +198,37 @@ public record ProbeResult(
|
||||
return executionCandidates != null && !executionCandidates.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasFgaExecutionEvidence() {
|
||||
return fgaExecutionEvidence != null && fgaExecutionEvidence.sqlText() != null
|
||||
&& !fgaExecutionEvidence.sqlText().isBlank();
|
||||
}
|
||||
|
||||
public ProbeResult withFgaExecutionEvidence(
|
||||
FgaExecutionEvidence evidence,
|
||||
String unavailableMessage
|
||||
) {
|
||||
return new ProbeResult(
|
||||
status,
|
||||
columns,
|
||||
rows,
|
||||
rowCount,
|
||||
maskedColumns,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody,
|
||||
vpdPredicate,
|
||||
effectiveSql,
|
||||
executionEvidence,
|
||||
executionEvidenceMessage,
|
||||
executionCandidates,
|
||||
evidence,
|
||||
unavailableMessage
|
||||
);
|
||||
}
|
||||
|
||||
public String title() {
|
||||
return switch (status) {
|
||||
case SUCCESS -> "권한에 따라 데이터를 볼 수 있습니다.";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user