Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,31 @@ ELSE pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)

private final JdbcTemplate jdbcTemplate;

/**
* Creates an operator probe backed by the primary PostgreSQL datasource.
*
* @param jdbcTemplate datasource client used to read replication-slot state
*/
public ReplicationSlotProbe(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

/**
* Reads the slot selected by {@code CDC_SLOT_NAME}, or the default slot when unset.
*
* @return finite operator status that never exposes database diagnostics
*/
public Map<String, Object> probeConfiguredSlot() {
String slotName = EnvUtils.getEnv("CDC_SLOT_NAME", "xtrmetl_slot");
return probeSlot(slotName);
}

/**
* Reads one logical replication slot and converts database failures into stable status data.
*
* @param slotName PostgreSQL replication-slot identifier to inspect
* @return slot state, not-found state, or a confidential fail-open classification
*/
public Map<String, Object> probeSlot(String slotName) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("slotName", slotName);
Expand All @@ -78,11 +94,12 @@ public Map<String, Object> probeSlot(String slotName) {
result.put("flushLagBytes", toLong(row.get("flush_lag_bytes")));
return result;
} catch (DataAccessException e) {
log.debug("Replication slot probe failed for {}: {}", slotName, e.toString());
log.debug("Replication slot probe query failed with classification {}",
e.getClass().getSimpleName());
result.put("available", false);
result.put("found", false);
result.put("error", "query_failed");
result.put("message", safeMessage(e));
result.put("message", "Replication slot state unavailable");
return result;
}
}
Expand All @@ -101,15 +118,4 @@ private static Long toLong(@Nullable Object value) {
return null;
}
}

private static String safeMessage(DataAccessException e) {
String msg = e.getMostSpecificCause() != null
? e.getMostSpecificCause().getMessage()
: e.getMessage();
if (msg == null || msg.isBlank()) {
return e.getClass().getSimpleName();
}
// Avoid leaking connection strings if drivers embed them.
return msg.length() > 200 ? msg.substring(0, 200) + "…" : msg;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.xtrmetl.cdc.service;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
* Verifies that operator replication-slot failures keep database diagnostics confidential.
*/
@ExtendWith(OutputCaptureExtension.class)
class ReplicationSlotProbeConfidentialityTest {

@Test
void failureResponseAndOrdinaryLogsExcludeDriverConnectionDiagnostics(CapturedOutput output) {
Logger probeLogger = assertInstanceOf(
Logger.class,
LoggerFactory.getLogger(ReplicationSlotProbe.class)
);
Level previous = probeLogger.getLevel();
// Classification is emitted at DEBUG; INFO-only capture would make secrecy asserts vacuous.
probeLogger.setLevel(Level.DEBUG);
try {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
String sensitiveDriverMessage =
"jdbc:postgresql://db.internal:5432/prod?user=admin&password=super-secret";
when(jdbc.queryForList(anyString(), eq("xtrmetl_slot")))
.thenThrow(new DataAccessResourceFailureException(sensitiveDriverMessage));

Map<String, Object> result = new ReplicationSlotProbe(jdbc).probeSlot("xtrmetl_slot");

assertFalse((Boolean) result.get("available"));
assertEquals("query_failed", result.get("error"));
assertEquals("Replication slot state unavailable", result.get("message"));
assertFalse(result.toString().contains("super-secret"));
assertFalse(result.toString().contains("jdbc:postgresql://"));
String logs = output.getOut() + output.getErr();
assertTrue(logs.contains("DataAccessResourceFailureException"));
assertFalse(logs.contains("super-secret"));
assertFalse(logs.contains("jdbc:postgresql://"));
} finally {
probeLogger.setLevel(previous);
}
}
}
Loading