use spate_core::config::ConfigError;
use std::collections::BTreeMap;
pub(crate) fn check_tls_feature(
rdkafka: &BTreeMap<String, String>,
scope: &str,
) -> Result<(), ConfigError> {
if cfg!(feature = "tls") {
return Ok(());
}
let wants_security = rdkafka
.get("security.protocol")
.is_some_and(|v| !v.eq_ignore_ascii_case("plaintext"))
|| rdkafka.keys().any(|k| {
k.starts_with("ssl.") || k.starts_with("sasl.") || k.starts_with("enable.ssl.")
});
if wants_security {
return Err(ConfigError::Validation(format!(
"{scope}.rdkafka requests TLS/SASL, but this build was compiled \
without it; rebuild with the `kafka-tls` feature \
(spate = {{ features = [\"kafka-tls\"] }})"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn map(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn plaintext_is_always_allowed() {
assert!(check_tls_feature(&map(&[]), "source.kafka").is_ok());
assert!(
check_tls_feature(&map(&[("security.protocol", "plaintext")]), "sink.kafka").is_ok()
);
assert!(check_tls_feature(&map(&[("linger.ms", "20")]), "sink.kafka").is_ok());
}
#[test]
fn security_request_tracks_the_feature() {
let secured = [
map(&[("security.protocol", "ssl")]),
map(&[
("security.protocol", "sasl_ssl"),
("sasl.mechanism", "SCRAM-SHA-256"),
]),
map(&[("ssl.ca.location", "/etc/kafka/ca.pem")]),
map(&[("sasl.username", "svc")]),
map(&[("enable.ssl.certificate.verification", "false")]),
];
for cfg in secured {
let result = check_tls_feature(&cfg, "source.kafka");
if cfg!(feature = "tls") {
assert!(result.is_ok(), "tls on: {cfg:?} should be accepted");
} else {
let err = result.expect_err("tls off: security config must be rejected");
assert!(err.to_string().contains("kafka-tls"), "actionable: {err}");
}
}
}
}