[Developer] #567 expose VPD effective SQL trace
This commit is contained in:
@@ -11,6 +11,9 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.net.URI;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
@@ -20,11 +23,15 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.springframework.jdbc.core.ConnectionCallback;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
@@ -36,6 +43,7 @@ public class OrdsProbeService {
|
||||
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
|
||||
};
|
||||
private static final Logger log = LoggerFactory.getLogger(OrdsProbeService.class);
|
||||
|
||||
private final BearerTokenService tokenService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
@@ -44,6 +52,7 @@ public class OrdsProbeService {
|
||||
private final RestTemplate ordsRestTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SettingService settingService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final Clock clock;
|
||||
|
||||
public OrdsProbeService(
|
||||
@@ -54,6 +63,7 @@ public class OrdsProbeService {
|
||||
RestTemplate ordsRestTemplate,
|
||||
ObjectMapper objectMapper,
|
||||
SettingService settingService,
|
||||
JdbcTemplate jdbcTemplate,
|
||||
Clock clock
|
||||
) {
|
||||
this.tokenService = tokenService;
|
||||
@@ -63,6 +73,7 @@ public class OrdsProbeService {
|
||||
this.ordsRestTemplate = ordsRestTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
this.settingService = settingService;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@@ -131,6 +142,9 @@ public class OrdsProbeService {
|
||||
prettyHeaders(response.getHeaders()),
|
||||
prettyJson(response.getBody())
|
||||
);
|
||||
if (!result.hasSqlTrace() && !isVectorSearchObject(object)) {
|
||||
result = addLocalSqlTrace(result, command.bearerToken(), object);
|
||||
}
|
||||
return auditAndReturn(command, result);
|
||||
} catch (HttpStatusCodeException e) {
|
||||
ProbeStatus status = errorClassifier.classify(e.getStatusCode(), e.getResponseBodyAsString());
|
||||
@@ -205,10 +219,93 @@ public class OrdsProbeService {
|
||||
requestHeaders,
|
||||
requestPayload,
|
||||
responseHeaders,
|
||||
responseBody
|
||||
responseBody,
|
||||
traceValue(root, "vpd_predicate"),
|
||||
traceValue(root, "effective_sql")
|
||||
);
|
||||
}
|
||||
|
||||
private String traceValue(JsonNode root, String fieldName) {
|
||||
JsonNode value = root == null ? null : root.get(fieldName);
|
||||
if (value == null || value.isNull() || !value.isValueNode()) {
|
||||
return null;
|
||||
}
|
||||
String text = value.asText();
|
||||
return text == null || text.isBlank() ? null : text;
|
||||
}
|
||||
|
||||
private ProbeResult addLocalSqlTrace(ProbeResult result, String bearerToken, ProtectedObject object) {
|
||||
String predicate = findVpdPredicate(bearerToken, object);
|
||||
if (predicate == null || predicate.isBlank()) {
|
||||
return result;
|
||||
}
|
||||
List<String> columns;
|
||||
try {
|
||||
columns = protectedObjectService.findColumns(object.objectId()).stream()
|
||||
.map(column -> "o." + column.columnName())
|
||||
.toList();
|
||||
} catch (RuntimeException ignored) {
|
||||
return result;
|
||||
}
|
||||
if (columns.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
String effectiveSql = "SELECT " + String.join(", ", columns)
|
||||
+ " FROM " + object.owner() + "." + object.objectName() + " o"
|
||||
+ " WHERE (" + predicate + ")"
|
||||
+ " AND ROWNUM <= LEAST(GREATEST(NVL(:row_limit, 50), 1), 500)";
|
||||
return result.withSqlTrace(predicate, effectiveSql);
|
||||
}
|
||||
|
||||
private String findVpdPredicate(String bearerToken, ProtectedObject object) {
|
||||
try {
|
||||
return jdbcTemplate.execute((ConnectionCallback<String>) connection -> {
|
||||
try {
|
||||
executeContextSetter(connection, bearerToken);
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT admin.cb_agent_doc_vpd_filter(?, ?) FROM dual")) {
|
||||
statement.setString(1, object.owner());
|
||||
statement.setString(2, object.objectName());
|
||||
try (ResultSet result = statement.executeQuery()) {
|
||||
return result.next() ? result.getString(1) : null;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
clearContext(connection);
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException exception) {
|
||||
// The backoffice may use a different DB account or a database without
|
||||
// the optional trace privilege. The ORDS response remains authoritative
|
||||
// when the Handler itself returned trace fields.
|
||||
log.debug("VPD SQL trace unavailable for {}.{}: {}",
|
||||
object.owner(), object.objectName(), exception.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void executeContextSetter(Connection connection, String bearerToken) throws java.sql.SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"BEGIN admin.cb_agent_ctx_pkg.set_user_by_bearer(?); END;")) {
|
||||
statement.setString(1, bearerToken);
|
||||
statement.execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearContext(Connection connection) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"BEGIN admin.cb_agent_ctx_pkg.clear_user; END;")) {
|
||||
statement.execute();
|
||||
} catch (java.sql.SQLException exception) {
|
||||
// Do not replace a successful ORDS result with a diagnostic cleanup error.
|
||||
log.debug("VPD context cleanup failed: {}", exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isVectorSearchObject(ProtectedObject object) {
|
||||
return "CB_VECTOR_SEARCH_DOCUMENTS".equalsIgnoreCase(object.objectName());
|
||||
}
|
||||
|
||||
private List<String> findMaskedColumns(long objectId, List<Map<String, Object>> rows) {
|
||||
if (rows.isEmpty()) {
|
||||
return List.of();
|
||||
|
||||
Reference in New Issue
Block a user